Skip to content

Commit

Permalink
feat(resolvers-composition): add support for glob patterns (#3132)
Browse files Browse the repository at this point in the history
* feat(resolvers-composition): add support for glob patterns

* fix(resolvers-composition): add typings for micromatch

* feat(resolvers-composition): add unit tests for glob patterns

* chore(resolver-compositions): cleanup

* feat(docs): added samples for resolver composition path matcher format

* chore: added changeset

* fix(docs): typo

* fix(changeset): change patch to minor

* Fix tests and cleanup

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
ntziolis and ardatan committed Jul 4, 2021
1 parent fd81e80 commit 1a81264
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-comics-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/resolvers-composition': minor
---

Added glob pattern support for composeResolver method
7 changes: 3 additions & 4 deletions packages/node-require/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
});
Expand All @@ -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;
}
}

Expand Down
10 changes: 6 additions & 4 deletions packages/node-require/test/node-require.spec.ts
Original file line number Diff line number Diff line change
@@ -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, ' ')
);
Expand Down
8 changes: 5 additions & 3 deletions packages/resolvers-composition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": {
Expand Down
106 changes: 41 additions & 65 deletions packages/resolvers-composition/src/resolvers-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, any, any> = GraphQLFieldResolver<any, any>
Expand All @@ -27,88 +28,63 @@ function isScalarTypeConfiguration(config: any): config is GraphQLScalarTypeConf

function resolveRelevantMappings<Resolvers extends Record<string, any> = Record<string, any>>(
resolvers: Resolvers,
path: string,
allMappings: ResolversComposerMapping<Resolvers>
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;
}

/**
Expand All @@ -129,15 +105,15 @@ export function composeResolvers<Resolvers extends Record<string, any>>(
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);
}
} 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);
Expand Down
57 changes: 57 additions & 0 deletions packages/resolvers-composition/tests/resolvers-composition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
11 changes: 11 additions & 0 deletions website/docs/resolvers-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit 1a81264

Please sign in to comment.