Skip to content

Commit 55518d0

Browse files
vlad-xbenjie
andauthored
Enables support for resolver extensions for compatibility with grafast, complexity, etc. (#2618)
* Enables support for resolver extensions for compatibility with grafast, complexity, etc. * Added an example on using resolver extensions to docs and added a changeset * Extension support misc. fixes from code review * Extensions should not cause a resolver to come into existence * Don't create resolvers unless necessary --------- Co-authored-by: Benjie Gillam <[email protected]>
1 parent ad1c323 commit 55518d0

File tree

4 files changed

+138
-6
lines changed

4 files changed

+138
-6
lines changed

.changeset/nice-eels-press.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'graphql-modules': minor
3+
'website': patch
4+
---
5+
6+
Enabled support for resolver extensions for compatibility with such libraries as grafast or graphql-query-complexity

packages/graphql-modules/src/module/resolvers.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
defaultFieldResolver,
66
FieldNode,
77
GraphQLResolveInfo,
8+
GraphQLFieldExtensions,
89
} from 'graphql';
910
import { Resolvers, ModuleConfig } from './types';
1011
import { ModuleMetadata } from './metadata';
@@ -102,6 +103,13 @@ export function createResolvers(
102103
});
103104
resolvers[typeName][fieldName].subscribe = resolver;
104105
}
106+
107+
if (isDefined((obj[fieldName] as any).extensions)) {
108+
// Do NOT add a resolve if one is not specified, it will cause
109+
// change in behavior in systems like `grafast`
110+
resolvers[typeName][fieldName].extensions =
111+
obj[fieldName].extensions;
112+
}
105113
}
106114
}
107115
}
@@ -298,6 +306,19 @@ function addObject({
298306
writeResolverMetadata(resolver.subscribe, config);
299307
container[typeName][fieldName].subscribe = resolver.subscribe;
300308
}
309+
310+
// extensions
311+
if (isDefined(resolver.extensions)) {
312+
if (container[typeName][fieldName].extensions) {
313+
throw new ResolverDuplicatedError(
314+
`Duplicated resolver of "${typeName}.${fieldName}" (extensions object)`,
315+
useLocation({ dirname: config.dirname, id: config.id })
316+
);
317+
}
318+
319+
writeResolverMetadata(resolver.extensions, config);
320+
container[typeName][fieldName].extensions = resolver.extensions;
321+
}
301322
}
302323
}
303324
}
@@ -399,7 +420,10 @@ function ensureImplements(metadata: ModuleMetadata) {
399420
};
400421
}
401422

402-
function writeResolverMetadata(resolver: Function, config: ModuleConfig): void {
423+
function writeResolverMetadata(
424+
resolver: Function | GraphQLFieldExtensions<any, any, any>,
425+
config: ModuleConfig
426+
): void {
403427
if (!resolver) {
404428
return;
405429
}
@@ -409,7 +433,9 @@ function writeResolverMetadata(resolver: Function, config: ModuleConfig): void {
409433
} as ResolverMetadata;
410434
}
411435

412-
export function readResolverMetadata(resolver: ResolveFn): ResolverMetadata {
436+
export function readResolverMetadata(
437+
resolver: ResolveFn | GraphQLFieldExtensions<any, any, any>
438+
): ResolverMetadata {
413439
return (resolver as any)[resolverMetadataProp];
414440
}
415441

@@ -501,10 +527,15 @@ function isResolveFn(value: any): value is ResolveFn {
501527
interface ResolveOptions {
502528
resolve?: ResolveFn;
503529
subscribe?: ResolveFn;
530+
extensions?: GraphQLFieldExtensions<any, any, any>;
504531
}
505532

506533
function isResolveOptions(value: any): value is ResolveOptions {
507-
return isDefined(value.resolve) || isDefined(value.subscribe);
534+
return (
535+
isDefined(value.resolve) ||
536+
isDefined(value.subscribe) ||
537+
isDefined(value.extensions)
538+
);
508539
}
509540

510541
function isScalarResolver(obj: any): obj is GraphQLScalarType {

packages/graphql-modules/tests/bootstrap.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'reflect-metadata';
22
import { makeExecutableSchema } from '@graphql-tools/schema';
33
import { createApplication, createModule, testkit, gql } from '../src';
44
import { NonDocumentNodeError } from '../src/shared/errors';
5+
import { defaultFieldResolver } from 'graphql';
56

67
test('fail when modules have non-unique ids', async () => {
78
const modFoo = createModule({
@@ -396,3 +397,63 @@ test('fail when modules have non-DocumentNode typeDefs', async () => {
396397
});
397398
}).toThrow(NonDocumentNodeError);
398399
});
400+
401+
describe('extensions', () => {
402+
const makeApp = () => {
403+
const m1 = createModule({
404+
id: 'test',
405+
typeDefs: gql`
406+
type Query {
407+
dummy: String!
408+
dummy2: String!
409+
}
410+
`,
411+
resolvers: {
412+
Query: {
413+
dummy: {
414+
resolve: () => '1',
415+
extensions: {
416+
test: 'test',
417+
},
418+
},
419+
dummy2: {
420+
extensions: {
421+
test2: 'test2',
422+
},
423+
},
424+
},
425+
},
426+
});
427+
const app = createApplication({
428+
modules: [m1],
429+
});
430+
return app;
431+
};
432+
433+
it('populates extensions', async () => {
434+
const app = makeApp();
435+
const schema = app.schema;
436+
const fields = schema.getQueryType()!.getFields();
437+
438+
expect(fields.dummy.extensions).toMatchObject({
439+
test: 'test',
440+
});
441+
expect(fields.dummy2.extensions).toMatchObject({
442+
test2: 'test2',
443+
});
444+
});
445+
446+
it('does not create a resolver if one was not specified', async () => {
447+
const app = makeApp();
448+
const schema = app.schema;
449+
const fields = schema.getQueryType()!.getFields();
450+
// Doesn't matter if it's the default resolver or unset, it simply must not
451+
// be a new function; otherwise `grafast` will interpret it as an
452+
// additional part of execution which will likely break the user's plan.
453+
if (fields.dummy2.resolve) {
454+
expect(fields.dummy2.resolve).toEqual(defaultFieldResolver);
455+
} else {
456+
expect(fields.dummy2.resolve).toBeFalsy();
457+
}
458+
});
459+
});

website/src/content/essentials/resolvers.mdx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@ npm i @graphql-tools/load-files
6060
Next, use it to load your files dynamically:
6161

6262
```ts
63-
import MyQueryType from './query.type.graphql'
6463
import { createModule } from 'graphql-modules'
65-
import { loadFilesSync } from '@graphql-tools/load-files'
66-
import { join } from 'path'
6764

6865
export const myModule = createModule({
6966
id: 'my-module',
@@ -72,3 +69,40 @@ export const myModule = createModule({
7269
resolvers: loadFilesSync(join(__dirname, './resolvers/*.ts'))
7370
})
7471
```
72+
73+
## Resolver Extensions
74+
75+
You can use resolver extensions to extend the functionality of your resolvers to make your modules work with such extensions as [Grafast Plan Resolver](https://grafast.org/grafast/plan-resolvers#specifying-a-field-plan-resolver) or [GraphQL Query Complexity](https://github.com/slicknode/graphql-query-complexity/blob/HEAD/src/estimators/fieldExtensions/README.md).
76+
77+
To use resolver extensions, you can use the `extensions` property in your resolvers.
78+
79+
```ts
80+
import { createModule, gql } from 'graphql-modules'
81+
import { constant } from "grafast";
82+
83+
export const myModule = createModule({
84+
id: 'my-module',
85+
dirname: __dirname,
86+
typeDefs: [
87+
gql`
88+
type Query {
89+
meaningOfLife: Int!
90+
}
91+
`
92+
],
93+
resolvers: {
94+
Query: {
95+
meaningOfLife: {
96+
extensions: {
97+
grafast: {
98+
plan() {
99+
return constant(42);
100+
},
101+
},
102+
},
103+
},
104+
}
105+
}
106+
})
107+
```
108+

0 commit comments

Comments
 (0)