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 + } +}