From 44c521b0134478984ada22740351d39a05ffcd7f Mon Sep 17 00:00:00 2001 From: zwei-wealth-dev <38549277-zwei-wealth-dev@users.noreply.gitlab.com> Date: Thu, 3 Sep 2026 08:37:53 +0000 Subject: [PATCH] fix(api): type ResolverWrapper against graphql's native GraphQLResolveInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@graphql-tools/utils` v12 augments `GraphQLResolveInfo` with two *required* members, `getAbortSignal` and `getAsyncHelpers`. graphql 17 declares both natively; graphql 16 declares neither. Because `ResolverWrapper` was expressed through that package's `IFieldResolver`, the utils v11 -> v12 bump made the type unimplementable on graphql 16 for any consumer whose resolvers come from graphql-codegen (its `ResolverFn` is typed with graphql's own resolve info): error TS2322: Type 'GraphQLResolveInfo' is missing the following properties from type 'GraphQLResolveInfo': getAbortSignal, getAsyncHelpers graphql 16 is inside our declared peer range and the bump ships as a patch, so this would have broken consumers silently. Express `ResolverWrapper` with graphql's own `GraphQLResolveInfo` instead — which is what every other resolver signature in the codebase already uses. `additionalResolvers` keeps using `IResolvers`; that direction stays assignable and is unaffected. The jest suite cannot catch this class of regression: ts-jest runs transpile-only under `isolatedModules`, so no test file is ever type-checked, and the only wrapper it exercises is an inferred `(resolver) => resolver` that never pins down the parameter type. Add a type-level test checked by tsc through a scoped `tsconfig.type-tests.json`, wired into `npm test` as `test:types` so it runs on both graphql majors of the CI matrix. Co-Authored-By: Claude Opus 5 (1M context) --- knip.json | 3 ++- package.json | 3 ++- src/api/execute.ts | 13 +++++++++---- tests/types/resolver-wrapper.ts | 29 +++++++++++++++++++++++++++++ tsconfig.type-tests.json | 11 +++++++++++ 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 tests/types/resolver-wrapper.ts create mode 100644 tsconfig.type-tests.json diff --git a/knip.json b/knip.json index d63f6e50..0f2b85da 100644 --- a/knip.json +++ b/knip.json @@ -5,7 +5,8 @@ "tests/**/*.ts" ], "ignore": [ - "**/generated/**" + "**/generated/**", + "tests/types/**" ], "ignoreDependencies": [ "conventional-changelog-conventionalcommits", diff --git a/package.json b/package.json index ab7402a2..3fff55bd 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "lint": "eslint src/**/*.ts", "lint:fix": "eslint src --fix", "deps": "docker compose up", - "test": "npm run lint && npm run test:all && npm run build", + "test": "npm run lint && npm run test:types && npm run test:all && npm run build", + "test:types": "tsc -p tsconfig.type-tests.json", "test:all": "jest tests --no-cache --no-watchman --setupFiles dotenv/config", "test:unit": "jest tests/unit --no-cache --no-watchman --setupFiles dotenv/config", "test:api": "jest tests/api --no-cache --no-watchman --setupFiles dotenv/config", diff --git a/src/api/execute.ts b/src/api/execute.ts index 4ab1b491..e714e45d 100644 --- a/src/api/execute.ts +++ b/src/api/execute.ts @@ -1,5 +1,5 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; -import { IFieldResolver, IResolvers } from '@graphql-tools/utils'; +import { IResolvers } from '@graphql-tools/utils'; import { DocumentNode, GraphQLResolveInfo, @@ -17,9 +17,14 @@ import { Context, generate, get, getResolvers } from '..'; import { Models } from '../models/models'; import { noIntrospection } from '../utils/rules'; -export type ResolverWrapper = ( - resolver: IFieldResolver, -) => IFieldResolver; +// Deliberately expressed with graphql's own `GraphQLResolveInfo` rather than `@graphql-tools/utils`' +// `IFieldResolver`: the latter augments the resolve info with members (`getAbortSignal`, +// `getAsyncHelpers`) that graphql 17 has natively but graphql 16 does not, which would make this +// type unimplementable on graphql 16 by consumers whose resolvers come from graphql-codegen. +// See tests/types/resolver-wrapper.ts. +export type FieldResolver = (source: unknown, args: unknown, context: unknown, info: GraphQLResolveInfo) => unknown; + +export type ResolverWrapper = (resolver: FieldResolver) => FieldResolver; export type ExecutorInput = { models: Models; diff --git a/tests/types/resolver-wrapper.ts b/tests/types/resolver-wrapper.ts new file mode 100644 index 00000000..e7461c8d --- /dev/null +++ b/tests/types/resolver-wrapper.ts @@ -0,0 +1,29 @@ +import type { GraphQLResolveInfo } from 'graphql'; +import type { ExecutorInput, ResolverWrapper } from '../../src'; + +// This file is never executed — `npm run test:types` type-checks it, and that is the assertion. +// +// graphql-codegen emits resolvers against graphql's own `GraphQLResolveInfo`, so a consumer's +// resolver wrapper is typed that way too. `ResolverWrapper` has to stay assignable from that shape +// on every graphql major our peer range allows. +// +// This regressed once: typing `ResolverWrapper` through `@graphql-tools/utils`' `IFieldResolver` +// pulled in that package's *augmented* `GraphQLResolveInfo`, which as of utils v12 requires +// `getAbortSignal` and `getAsyncHelpers`. graphql 17 declares both natively, graphql 16 declares +// neither — so on graphql 16 the assignments below stopped compiling for every consumer, while the +// jest suite stayed green (ts-jest runs transpile-only, and the one wrapper it passes is an +// inferred `(resolver) => resolver` that never pins down the parameter type). +type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => Promise | TResult; + +declare const consumerWrapper: ( + resolver: ResolverFn, +) => ResolverFn; + +export const wrapper: ResolverWrapper = consumerWrapper; + +export const executorInput: Pick = { resolverWrapper: consumerWrapper }; diff --git a/tsconfig.type-tests.json b/tsconfig.type-tests.json new file mode 100644 index 00000000..746d85c4 --- /dev/null +++ b/tsconfig.type-tests.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "include": ["tests/types/**/*.ts"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "rootDir": "./", + "noEmit": true, + "declaration": false, + "sourceMap": false + } +}