From 2cb8769a5ab00004234f8f1c96a964c310ebb63c Mon Sep 17 00:00:00 2001 From: Vlad Bondarenko Date: Thu, 10 Jul 2025 10:16:17 +0300 Subject: [PATCH 1/3] Enables support for resolver extensions for compatibility with grafast, complexity, etc. --- .../graphql-modules/src/module/resolvers.ts | 34 ++++++++++++++++++- .../graphql-modules/tests/bootstrap.spec.ts | 32 +++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/graphql-modules/src/module/resolvers.ts b/packages/graphql-modules/src/module/resolvers.ts index f74bebcc99..c2d59faed7 100644 --- a/packages/graphql-modules/src/module/resolvers.ts +++ b/packages/graphql-modules/src/module/resolvers.ts @@ -92,6 +92,18 @@ export function createResolvers( resolvers[typeName][fieldName].resolve = resolver; } + if (isDefined((obj[fieldName] as any).extensions)) { + // some extensions allow to omit the resolve function, e.g. grafast + const defaultResolver = (val: any) => val; + const resolver = wrapResolver({ + config, + resolver: (obj[fieldName] as any).resolve || defaultResolver, + middlewareMap, + path, + }); + resolvers[typeName][fieldName].resolve = resolver; + } + // { subscribe } if (isDefined((obj[fieldName] as any).subscribe)) { const resolver = wrapResolver({ @@ -286,6 +298,21 @@ function addObject({ container[typeName][fieldName].resolve = resolver.resolve; } + // extensions + if (isDefined(resolver.extensions)) { + if (container[typeName][fieldName].extensions) { + throw new ResolverDuplicatedError( + `Duplicated resolver of "${typeName}.${fieldName}" (extensions method)`, + useLocation({ dirname: config.dirname, id: config.id }) + ); + } + + (resolver.extensions as any)[resolverMetadataProp] = { + moduleId: config.id, + } as ResolverMetadata; + container[typeName][fieldName].extensions = resolver.extensions; + } + // subscribe if (isDefined(resolver.subscribe)) { if (container[typeName][fieldName].subscribe) { @@ -501,10 +528,15 @@ function isResolveFn(value: any): value is ResolveFn { interface ResolveOptions { resolve?: ResolveFn; subscribe?: ResolveFn; + extensions?: Record; } function isResolveOptions(value: any): value is ResolveOptions { - return isDefined(value.resolve) || isDefined(value.subscribe); + return ( + isDefined(value.resolve) || + isDefined(value.subscribe) || + isDefined(value.extensions) + ); } function isScalarResolver(obj: any): obj is GraphQLScalarType { diff --git a/packages/graphql-modules/tests/bootstrap.spec.ts b/packages/graphql-modules/tests/bootstrap.spec.ts index 0673766edb..8281641705 100644 --- a/packages/graphql-modules/tests/bootstrap.spec.ts +++ b/packages/graphql-modules/tests/bootstrap.spec.ts @@ -2,6 +2,7 @@ import 'reflect-metadata'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { createApplication, createModule, testkit, gql } from '../src'; import { NonDocumentNodeError } from '../src/shared/errors'; +import { resolve } from 'path'; test('fail when modules have non-unique ids', async () => { const modFoo = createModule({ @@ -396,3 +397,34 @@ test('fail when modules have non-DocumentNode typeDefs', async () => { }); }).toThrow(NonDocumentNodeError); }); + +test('should allow resolver extensions', async () => { + const m1 = createModule({ + id: 'test', + typeDefs: gql` + type Query { + dummy: String! + } + `, + resolvers: { + Query: { + dummy: { + resolve: () => '1', + extensions: { + test: 'test', + }, + }, + }, + }, + }); + + const app = createApplication({ + modules: [m1], + }); + + const schema = app.schema; + expect( + Object.keys(schema.getQueryType()?.getFields().dummy.extensions || {}) + .length + ).toBe(1); +}); From aa5b092874d1cde0557a3554f4adbcd342c3a481 Mon Sep 17 00:00:00 2001 From: Vlad Bondarenko Date: Thu, 10 Jul 2025 10:33:25 +0300 Subject: [PATCH 2/3] Added an example on using resolver extensions to docs and added a changeset --- .changeset/nice-eels-press.md | 6 +++ website/src/content/essentials/resolvers.mdx | 41 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .changeset/nice-eels-press.md diff --git a/.changeset/nice-eels-press.md b/.changeset/nice-eels-press.md new file mode 100644 index 0000000000..214ccec26c --- /dev/null +++ b/.changeset/nice-eels-press.md @@ -0,0 +1,6 @@ +--- +'graphql-modules': minor +'website': patch +--- + +Enabled support for resolver extensions for compatibility with such libraries as grafast or graphql-query-complexity diff --git a/website/src/content/essentials/resolvers.mdx b/website/src/content/essentials/resolvers.mdx index 22fe7b5ae0..9b69d09f96 100644 --- a/website/src/content/essentials/resolvers.mdx +++ b/website/src/content/essentials/resolvers.mdx @@ -60,10 +60,8 @@ npm i @graphql-tools/load-files Next, use it to load your files dynamically: ```ts -import MyQueryType from './query.type.graphql' import { createModule } from 'graphql-modules' -import { loadFilesSync } from '@graphql-tools/load-files' -import { join } from 'path' +import { constant } from "grafast"; export const myModule = createModule({ id: 'my-module', @@ -72,3 +70,40 @@ export const myModule = createModule({ resolvers: loadFilesSync(join(__dirname, './resolvers/*.ts')) }) ``` + +## Resolver Extensions + +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). + +To use resolver extensions, you can use the `extensions` property in your resolvers. + +```ts +import { createModule, gql } from 'graphql-modules' +import { constant } from "grafast"; + +export const myModule = createModule({ + id: 'my-module', + dirname: __dirname, + typeDefs: [ + gql` + type Query { + meaningOfLife: Int! + } + ` + ], + resolvers: { + Query: { + meaningOfLife: { + extensions: { + grafast: { + plan() { + return constant(42); + }, + }, + }, + }, + } + } +}) +``` + From 01dfcf034c086d21c1d8ecc9daa1f50e9f6c4097 Mon Sep 17 00:00:00 2001 From: Vlad Bondarenko Date: Tue, 5 Aug 2025 10:50:58 +0300 Subject: [PATCH 3/3] Extension support misc. fixes from code review --- packages/graphql-modules/tests/bootstrap.spec.ts | 1 - website/src/content/essentials/resolvers.mdx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/graphql-modules/tests/bootstrap.spec.ts b/packages/graphql-modules/tests/bootstrap.spec.ts index 8281641705..97411358cd 100644 --- a/packages/graphql-modules/tests/bootstrap.spec.ts +++ b/packages/graphql-modules/tests/bootstrap.spec.ts @@ -2,7 +2,6 @@ import 'reflect-metadata'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { createApplication, createModule, testkit, gql } from '../src'; import { NonDocumentNodeError } from '../src/shared/errors'; -import { resolve } from 'path'; test('fail when modules have non-unique ids', async () => { const modFoo = createModule({ diff --git a/website/src/content/essentials/resolvers.mdx b/website/src/content/essentials/resolvers.mdx index 9b69d09f96..0601935e54 100644 --- a/website/src/content/essentials/resolvers.mdx +++ b/website/src/content/essentials/resolvers.mdx @@ -61,7 +61,6 @@ Next, use it to load your files dynamically: ```ts import { createModule } from 'graphql-modules' -import { constant } from "grafast"; export const myModule = createModule({ id: 'my-module',