diff --git a/.changeset/silver-comics-check.md b/.changeset/silver-comics-check.md new file mode 100644 index 00000000000..38136f26cc3 --- /dev/null +++ b/.changeset/silver-comics-check.md @@ -0,0 +1,6 @@ +--- +'@graphql-tools/resolvers-composition': minor +'@graphql-tools/website': minor +--- + +Added glob pattern support for composeResolver method diff --git a/packages/node-require/src/index.ts b/packages/node-require/src/index.ts index d238c3b50d5..43b801ac65e 100644 --- a/packages/node-require/src/index.ts +++ b/packages/node-require/src/index.ts @@ -11,8 +11,7 @@ import { isSome } from '@graphql-tools/utils'; const VALID_EXTENSIONS = ['graphql', 'graphqls', 'gql', 'gqls']; -function handleModule(m: NodeModule, filename: string) { - console.log(m, filename); +export function handleModule(m: NodeModule, filename: string) { const sources = loadTypedefsSync(filename, { loaders: [new GraphQLFileLoader()], }); @@ -22,9 +21,9 @@ function handleModule(m: NodeModule, filename: string) { m.exports = mergedDoc; } -export function registerGraphQLExtensions(require: NodeRequire) { +export function registerGraphQLExtensions(nodeRequire: NodeRequire) { for (const ext of VALID_EXTENSIONS) { - require.extensions[`.${ext}`] = handleModule; + nodeRequire.extensions[`.${ext}`] = handleModule; } } diff --git a/packages/node-require/test/node-require.spec.ts b/packages/node-require/test/node-require.spec.ts index febaac67b37..d2dac6d6f2e 100644 --- a/packages/node-require/test/node-require.spec.ts +++ b/packages/node-require/test/node-require.spec.ts @@ -1,12 +1,14 @@ -import '../src'; +import { handleModule } from '../src'; import { print } from 'graphql'; import { readFileSync } from 'fs'; +import { join } from 'path'; describe('GraphQL Node Import', () => { - it.skip('should import correct definitions', () => { - console.log(require.main); + it('should import correct definitions', () => { const filePath = './fixtures/test.graphql'; - const typeDefs = require(filePath); + const m: any = {}; + handleModule(m, join(__dirname, filePath)); + const typeDefs = m.exports; expect(print(typeDefs).replace(/\s\s+/g, ' ')).toBe( readFileSync(require.resolve(filePath), 'utf8').replace(/\s\s+/g, ' ') ); diff --git a/packages/resolvers-composition/package.json b/packages/resolvers-composition/package.json index dcb0a1f4580..a1b3ef2fe45 100644 --- a/packages/resolvers-composition/package.json +++ b/packages/resolvers-composition/package.json @@ -11,8 +11,8 @@ "license": "MIT", "sideEffects": false, "main": "dist/index.js", - "module": "dist/index.mjs", - "exports": { + "module": "dist/index.mjs", + "exports": { ".": { "require": "./dist/index.js", "import": "./dist/index.mjs" @@ -30,11 +30,13 @@ "graphql": "^14.0.0 || ^15.0.0" }, "devDependencies": { - "@types/lodash": "4.14.170" + "@types/lodash": "4.14.170", + "@types/micromatch": "^4.0.1" }, "dependencies": { "@graphql-tools/utils": "^7.9.1", "lodash": "4.17.21", + "micromatch": "^4.0.4", "tslib": "~2.3.0" }, "publishConfig": { diff --git a/packages/resolvers-composition/src/resolvers-composition.ts b/packages/resolvers-composition/src/resolvers-composition.ts index f07bad3174d..a61e42ea26b 100644 --- a/packages/resolvers-composition/src/resolvers-composition.ts +++ b/packages/resolvers-composition/src/resolvers-composition.ts @@ -2,6 +2,7 @@ import { chainFunctions } from './chain-functions'; import _ from 'lodash'; import { GraphQLFieldResolver, GraphQLScalarTypeConfig } from 'graphql'; import { asArray } from '@graphql-tools/utils'; +import { matcher } from 'micromatch'; export type ResolversComposition< Resolver extends GraphQLFieldResolver = GraphQLFieldResolver @@ -27,88 +28,63 @@ function isScalarTypeConfiguration(config: any): config is GraphQLScalarTypeConf function resolveRelevantMappings = Record>( resolvers: Resolvers, - path: string, - allMappings: ResolversComposerMapping + path: string ): string[] { - const split = path.split('.'); + if (!resolvers) { + return []; + } - if (split.length === 2) { - const typeName = split[0]; + const [typeNameOrGlob, fieldNameOrGlob] = path.split('.'); + const isTypeMatch = matcher(typeNameOrGlob); - if (isScalarTypeConfiguration(resolvers[typeName])) { - return []; - } + let fixedFieldGlob = fieldNameOrGlob; + // convert single value OR `{singleField}` to `singleField` as matching will fail otherwise + if (fixedFieldGlob.includes('{') && !fixedFieldGlob.includes(',')) { + fixedFieldGlob = fieldNameOrGlob.replace('{', '').replace('}', ''); + } + fixedFieldGlob = fixedFieldGlob.replace(', ', ',').trim(); - const fieldName = split[1]; + const isFieldMatch = matcher(fixedFieldGlob); - if (typeName === '*') { - if (!resolvers) { - return []; - } - const mappings: string[] = []; - for (const typeName in resolvers) { - const relevantMappings = resolveRelevantMappings(resolvers, `${typeName}.${fieldName}`, allMappings); - for (const relevantMapping of relevantMappings) { - mappings.push(relevantMapping); - } - } - return mappings; + const mappings: string[] = []; + for (const typeName in resolvers) { + if (!isTypeMatch(typeName)) { + continue; } - if (fieldName === '*') { - const fieldMap = resolvers[typeName]; - if (!fieldMap) { - return []; - } - const mappings: string[] = []; - for (const field in fieldMap) { - const relevantMappings = resolveRelevantMappings(resolvers, `${typeName}.${field}`, allMappings); - for (const relevantMapping of relevantMappings) { - if (!allMappings[relevantMapping]) { - mappings.push(relevantMapping); - } - } - } - return mappings; - } else { - const paths = []; - - if (resolvers[typeName] && resolvers[typeName][fieldName]) { - if (resolvers[typeName][fieldName].subscribe) { - paths.push(path + '.subscribe'); - } - - if (resolvers[typeName][fieldName].resolve) { - paths.push(path + '.resolve'); - } - - if (typeof resolvers[typeName][fieldName] === 'function') { - paths.push(path); - } - } - - return paths; + if (isScalarTypeConfiguration(resolvers[typeName])) { + continue; } - } else if (split.length === 1) { - const typeName = split[0]; const fieldMap = resolvers[typeName]; if (!fieldMap) { return []; } - const mappings: string[] = []; + for (const field in fieldMap) { + if (!isFieldMatch(field)) { + continue; + } - for (const fieldName in fieldMap) { - const relevantMappings = resolveRelevantMappings(resolvers, `${typeName}.${fieldName}`, allMappings); - for (const relevantMapping of relevantMappings) { - mappings.push(relevantMapping); + const resolvedPath = `${typeName}.${field}`; + + if (resolvers[typeName] && resolvers[typeName][field]) { + if (resolvers[typeName][field].subscribe) { + mappings.push(resolvedPath + '.subscribe'); + } + + if (resolvers[typeName][field].resolve) { + mappings.push(resolvedPath + '.resolve'); + } + + if (typeof resolvers[typeName][field] === 'function') { + mappings.push(resolvedPath); + } } } - return mappings; } - return []; + return mappings; } /** @@ -129,7 +105,7 @@ export function composeResolvers>( const resolverPathMapping = mapping[resolverPath]; if (resolverPathMapping instanceof Array || typeof resolverPathMapping === 'function') { const composeFns = resolverPathMapping as ResolversComposition | ResolversComposition[]; - const relevantFields = resolveRelevantMappings(resolvers, resolverPath, mapping); + const relevantFields = resolveRelevantMappings(resolvers, resolverPath); for (const path of relevantFields) { mappingResult[path] = asArray(composeFns); @@ -137,7 +113,7 @@ export function composeResolvers>( } else if (resolverPathMapping) { for (const fieldName in resolverPathMapping) { const composeFns = resolverPathMapping[fieldName]; - const relevantFields = resolveRelevantMappings(resolvers, resolverPath + '.' + fieldName, mapping); + const relevantFields = resolveRelevantMappings(resolvers, resolverPath + '.' + fieldName); for (const path of relevantFields) { mappingResult[path] = asArray(composeFns); diff --git a/packages/resolvers-composition/tests/resolvers-composition.spec.ts b/packages/resolvers-composition/tests/resolvers-composition.spec.ts index 8449371c3ab..908b88dc3d5 100644 --- a/packages/resolvers-composition/tests/resolvers-composition.spec.ts +++ b/packages/resolvers-composition/tests/resolvers-composition.spec.ts @@ -290,6 +290,63 @@ describe('Resolvers composition', () => { }); + it('should support glob pattern for fields - Query.{foo, bar}', async () => { + const resolvers = { + Query: { + foo: async () => 0, + bar: async () => 1, + fooBar: async () => 2, + }, + Mutation: { + qux: async () => 3, + baz: async () => 4, + }, + }; + const resolversComposition = { + 'Query.{foo, bar}': [ + (next: any) => async (...args: any) => { + const result = await next(...args); + return result + 1; + } + ] + } + const composedResolvers = composeResolvers(resolvers, resolversComposition); + + expect(await composedResolvers.Query.foo()).toBe(1); + expect(await composedResolvers.Query.bar()).toBe(2); + expect(await composedResolvers.Query.fooBar()).toBe(2); + expect(await composedResolvers.Mutation.qux()).toBe(3); + expect(await composedResolvers.Mutation.baz()).toBe(4); + }); + + it('should support glob pattern for fields - Query.!{foo, bar}', async () => { + const resolvers = { + Query: { + foo: async () => 0, + bar: async () => 1, + fooBar: async () => 2, + }, + Mutation: { + qux: async () => 3, + baz: async () => 4, + }, + }; + const resolversComposition = { + 'Query.!{foo, bar}': [ + (next: any) => async (...args: any) => { + const result = await next(...args); + return result + 1; + } + ] + } + const composedResolvers = composeResolvers(resolvers, resolversComposition); + + expect(await composedResolvers.Query.foo()).toBe(0); + expect(await composedResolvers.Query.bar()).toBe(1); + expect(await composedResolvers.Query.fooBar()).toBe(3); + expect(await composedResolvers.Mutation.qux()).toBe(3); + expect(await composedResolvers.Mutation.baz()).toBe(4); + }); it('should handle nullish properties correctly', async () => { const getFoo = () => 'FOO'; const resolvers = { diff --git a/website/docs/resolvers-composition.md b/website/docs/resolvers-composition.md index 68513317ddc..0f814926a39 100644 --- a/website/docs/resolvers-composition.md +++ b/website/docs/resolvers-composition.md @@ -78,3 +78,14 @@ const composedResolvers = composeResolvers(resolvers, resolversComposition); ``` `composeResolvers` is a method in `@graphql-tools/resolvers-composition` package that accepts `IResolvers` object and mappings for composition functions that would be run before resolver itself. + +### Supported path matcher format +The paths for resolvers support `*` wildcard for types and glob patters for fields, eg: +- `*.*` - all types and all fields +- `Query.*` - all queries +- `Query.single` - only a single query +- `Query.{first, second}` - queries for first/second +- `Query.!first` - all queries but first +- `Query.!{first, second}` - all queries but first/second + + diff --git a/yarn.lock b/yarn.lock index 0bcbd991a68..54c054a76fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2715,7 +2715,7 @@ dependencies: "@types/unist" "*" -"@types/micromatch@latest": +"@types/micromatch@^4.0.1", "@types/micromatch@latest": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7" integrity sha1-k4FEndZZ/Dgj/SpBkM6syYUIO8c=