diff --git a/.changeset/angry-lamps-notice.md b/.changeset/angry-lamps-notice.md new file mode 100644 index 00000000000..857afd3b8a4 --- /dev/null +++ b/.changeset/angry-lamps-notice.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +BREAKING CHANGES: Do not generate \_\_isTypeOf for non-implementing types or non-union members diff --git a/.changeset/brave-meals-brake.md b/.changeset/brave-meals-brake.md new file mode 100644 index 00000000000..39ef01dec79 --- /dev/null +++ b/.changeset/brave-meals-brake.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/cli': major +--- + +Drop @graphql-tools/prisma-loader diff --git a/.changeset/loud-suits-admire.md b/.changeset/loud-suits-admire.md new file mode 100644 index 00000000000..3ecc3cbef0e --- /dev/null +++ b/.changeset/loud-suits-admire.md @@ -0,0 +1,10 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +Ensure Federation Interfaces have `__resolveReference` if they are resolvable entities + +BREAKING CHANGES: Deprecate `onlyResolveTypeForInterfaces` because majority of use cases cannot implement resolvers in Interfaces. +BREAKING CHANGES: Deprecate `generateInternalResolversIfNeeded.__resolveReference` because types do not have `__resolveReference` if they are not Federation entities or are not resolvable. Users should not have to manually set this option. This option was put in to wait for this major version. diff --git a/.changeset/lovely-snails-travel.md b/.changeset/lovely-snails-travel.md new file mode 100644 index 00000000000..2fbc76e0f5a --- /dev/null +++ b/.changeset/lovely-snails-travel.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +BREAKING CHANGE: Improve Federation Entity's resolvers' parent param type: These types were using reference types inline. This makes it hard to handle mappers. The Parent type now all comes from ParentResolverTypes to make handling mappers and parent types simpler. diff --git a/.changeset/many-pets-attend.md b/.changeset/many-pets-attend.md new file mode 100644 index 00000000000..0ed0e00bedb --- /dev/null +++ b/.changeset/many-pets-attend.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/plugin-helpers': minor +'@graphql-codegen/cli': major +--- + +Add `allowPartialOutputs` flag to partially write successful generation to files diff --git a/.changeset/small-fans-cross.md b/.changeset/small-fans-cross.md new file mode 100644 index 00000000000..729306c52a0 --- /dev/null +++ b/.changeset/small-fans-cross.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/plugin-helpers': patch +--- + +Update @requires type diff --git a/.changeset/thick-pianos-smoke.md b/.changeset/thick-pianos-smoke.md new file mode 100644 index 00000000000..569664420a9 --- /dev/null +++ b/.changeset/thick-pianos-smoke.md @@ -0,0 +1,11 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +Fix `mappers` usage with Federation + +`mappers` was previously used as `__resolveReference`'s first param (usually called "reference"). However, this is incorrect because `reference` interface comes directly from `@key` and `@requires` directives. This patch fixes the issue by creating a new `FederationTypes` type and use it as the base for federation entity types when being used to type entity references. + +BREAKING CHANGES: No longer generate `UnwrappedObject` utility type, as this was used to support the wrong previously generated type. diff --git a/.changeset/twenty-planets-complain.md b/.changeset/twenty-planets-complain.md new file mode 100644 index 00000000000..4d9a34fa45c --- /dev/null +++ b/.changeset/twenty-planets-complain.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/plugin-helpers': patch +--- + +Fix fields or object types marked with @external being wrongly generated diff --git a/.changeset/wicked-timers-eat.md b/.changeset/wicked-timers-eat.md new file mode 100644 index 00000000000..38674e51ce7 --- /dev/null +++ b/.changeset/wicked-timers-eat.md @@ -0,0 +1,22 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/graphql-modules-preset': major +'@graphql-codegen/plugin-helpers': major +'@graphql-codegen/cli': major +'@graphql-codegen/client-preset': major +'@graphql-codegen/core': major +'@graphql-codegen/add': major +'@graphql-codegen/fragment-matcher': major +'@graphql-codegen/introspection': major +'@graphql-codegen/schema-ast': major +'@graphql-codegen/time': major +'@graphql-codegen/typescript-document-nodes': major +'@graphql-codegen/gql-tag-operations': major +'@graphql-codegen/typescript-operations': major +'@graphql-codegen/typed-document-node': major +'@graphql-codegen/typescript': major +'@graphql-codegen/testing': major +--- + +Drop Node 18 support diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b337f5d2d4..1016358540d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - federation-fixes # FIXME: Remove this line after the PR is merged env: NODE_OPTIONS: '--max_old_space_size=4096' @@ -28,8 +29,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - name: Prettier Check run: yarn prettier:check dev-tests-old: @@ -45,8 +44,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - name: Build run: yarn build env: @@ -63,8 +60,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - name: Build run: yarn build env: @@ -87,8 +82,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1 with: toolchain: 1.65.0 @@ -120,8 +113,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - name: Build run: yarn build env: @@ -138,10 +129,10 @@ jobs: strategy: matrix: os: [ubuntu-latest] # remove windows to speed up the tests - node_version: [16, 18, 20] + node_version: [20, 22, 24] graphql_version: [15, 16] include: - - node-version: 14 + - node-version: 20 os: windows-latest graphql_version: 16 steps: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bf47152690b..12b77c7cc3e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,6 @@ jobs: with: npmTag: alpha buildScript: build - nodeVersion: 18 secrets: githubToken: ${{ secrets.GITHUB_TOKEN }} npmToken: ${{ secrets.NPM_TOKEN }} @@ -29,7 +28,6 @@ jobs: npmTag: rc restoreDeletedChangesets: true buildScript: build - nodeVersion: 18 secrets: githubToken: ${{ secrets.GITHUB_TOKEN }} npmToken: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eaf8c319040..d748632279e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,6 @@ jobs: uses: the-guild-org/shared-config/.github/workflows/release-stable.yml@main with: releaseScript: release - nodeVersion: 18 secrets: # githubToken: ${{ secrets.GUILD_BOT_TOKEN }} githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/website-integrity.yml b/.github/workflows/website-integrity.yml index 7b66eabae4c..fa3bf8a304b 100644 --- a/.github/workflows/website-integrity.yml +++ b/.github/workflows/website-integrity.yml @@ -15,8 +15,6 @@ jobs: - name: Setup env uses: the-guild-org/shared-config/setup@main - with: - nodeVersion: 18 - name: Build Packages run: yarn build diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 68d2bdc93e5..1d5400f2c2c 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -20,9 +20,6 @@ jobs: - uses: the-guild-org/shared-config/setup@main name: setup env - with: - nodeVersion: 18 - packageManager: yarn - uses: the-guild-org/shared-config/website-cf@main name: build and deploy website diff --git a/babel.config.js b/babel.config.js index 3e4899f4a74..fa522d8587c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,6 @@ module.exports = { presets: [ ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], - '@babel/preset-typescript', + ['@babel/preset-typescript', { allowDeclareFields: true }], ], }; diff --git a/dev-test/modules/types.ts b/dev-test/modules/types.ts index 4a4bf594d23..27f8fe89372 100644 --- a/dev-test/modules/types.ts +++ b/dev-test/modules/types.ts @@ -220,7 +220,6 @@ export type ArticleResolvers< id?: Resolver; text?: Resolver; title?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type CreditCardResolvers< @@ -241,7 +240,6 @@ export type DonationResolvers< id?: Resolver; recipient?: Resolver; sender?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type MutationResolvers< @@ -298,7 +296,6 @@ export type UserResolvers< id?: Resolver; lastName?: Resolver; paymentOptions?: Resolver>, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/dev-test/subpath-import/result.d.ts b/dev-test/subpath-import/result.d.ts index f066002aea9..bc93a06cac9 100644 --- a/dev-test/subpath-import/result.d.ts +++ b/dev-test/subpath-import/result.d.ts @@ -144,7 +144,6 @@ export type UserResolvers< name?: Resolver; password?: Resolver; updatedAt?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type MutationResolvers< @@ -157,7 +156,6 @@ export type MutationResolvers< FiedContextType, RequireFields >; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index d2c05ac9d13..1c52dab9c31 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -128,6 +128,20 @@ export type DirectiveResolverFn TResult | Promise; +/** Mapping of federation types */ +export type FederationTypes = { + User: User; +}; + +/** Mapping of federation reference types */ +export type FederationReferenceTypes = { + User: { __typename: 'User' } & ( + | GraphQLRecursivePick + | GraphQLRecursivePick + ) & + ({} | GraphQLRecursivePick); +}; + /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Address: ResolverTypeWrapper
; @@ -149,7 +163,7 @@ export type ResolversParentTypes = { ID: Scalars['ID']['output']; Lines: Lines; Query: {}; - User: User; + User: User | FederationReferenceTypes['User']; Int: Scalars['Int']['output']; Boolean: Scalars['Boolean']['output']; }; @@ -161,7 +175,6 @@ export type AddressResolvers< city?: Resolver, ParentType, ContextType>; lines?: Resolver; state?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type BookResolvers< @@ -169,7 +182,6 @@ export type BookResolvers< ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book'] > = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type LinesResolvers< @@ -178,7 +190,6 @@ export type LinesResolvers< > = { line1?: Resolver; line2?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type QueryResolvers< @@ -190,28 +201,15 @@ export type QueryResolvers< export type UserResolvers< ContextType = any, - ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], + FederationReferenceType extends FederationReferenceTypes['User'] = FederationReferenceTypes['User'] > = { __resolveReference?: ReferenceResolver< - Maybe, - { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ), + Maybe | FederationReferenceType, + FederationReferenceType, ContextType >; - - email?: Resolver< - ResolversTypes['String'], - { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ) & - GraphQLRecursivePick, - ContextType - >; - - __isTypeOf?: IsTypeOfResolverFn; + email?: Resolver; }; export type Resolvers = { diff --git a/dev-test/test-schema/resolvers-root.ts b/dev-test/test-schema/resolvers-root.ts index ca4941f0f61..ada62e8385f 100644 --- a/dev-test/test-schema/resolvers-root.ts +++ b/dev-test/test-schema/resolvers-root.ts @@ -141,7 +141,6 @@ export type QueryResolvers< ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] > = { someDummyField?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type QueryRootResolvers< @@ -172,7 +171,6 @@ export type UserResolvers< email?: Resolver; id?: Resolver; name?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/dev-test/test-schema/resolvers-stitching.ts b/dev-test/test-schema/resolvers-stitching.ts index 8954a721b9a..f7338232eda 100644 --- a/dev-test/test-schema/resolvers-stitching.ts +++ b/dev-test/test-schema/resolvers-stitching.ts @@ -162,7 +162,6 @@ export type UserResolvers< email?: Resolver; id?: Resolver; name?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/dev-test/test-schema/resolvers-types.ts b/dev-test/test-schema/resolvers-types.ts index 6bb026ac0c8..07dac436dc3 100644 --- a/dev-test/test-schema/resolvers-types.ts +++ b/dev-test/test-schema/resolvers-types.ts @@ -148,7 +148,6 @@ export type UserResolvers< email?: Resolver; id?: Resolver; name?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/dev-test/test-schema/typings.ts b/dev-test/test-schema/typings.ts index 707d55a9502..851d7d6e401 100644 --- a/dev-test/test-schema/typings.ts +++ b/dev-test/test-schema/typings.ts @@ -136,7 +136,6 @@ export type UserResolvers< email?: Resolver; id?: Resolver; name?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { diff --git a/packages/graphql-codegen-cli/package.json b/packages/graphql-codegen-cli/package.json index e857f0a573e..7f9ae742d8b 100644 --- a/packages/graphql-codegen-cli/package.json +++ b/packages/graphql-codegen-cli/package.json @@ -53,7 +53,6 @@ "@graphql-tools/graphql-file-loader": "^8.0.0", "@graphql-tools/json-file-loader": "^8.0.0", "@graphql-tools/load": "^8.1.0", - "@graphql-tools/prisma-loader": "^8.0.0", "@graphql-tools/url-loader": "^8.0.0", "@graphql-tools/utils": "^10.0.0", "@whatwg-node/fetch": "^0.10.0", diff --git a/packages/graphql-codegen-cli/src/codegen.ts b/packages/graphql-codegen-cli/src/codegen.ts index 10eaaa953d0..10e3e78ccc6 100644 --- a/packages/graphql-codegen-cli/src/codegen.ts +++ b/packages/graphql-codegen-cli/src/codegen.ts @@ -69,7 +69,9 @@ function createCache(): (namespace: string, key: string, factory: () => Promi }; } -export async function executeCodegen(input: CodegenContext | Types.Config): Promise { +export async function executeCodegen( + input: CodegenContext | Types.Config +): Promise<{ result: Types.FileOutput[]; error: Error | null }> { const context = ensureContext(input); const config = context.getConfig(); const pluginContext = context.getPluginContext(); @@ -431,13 +433,13 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom printLogs(); } + let error: Error | null = null; if (executedContext.errors.length > 0) { const errors = executedContext.errors.map(subErr => subErr.message || subErr.toString()); - const newErr = new AggregateError(executedContext.errors, String(errors.join('\n\n'))); + error = new AggregateError(executedContext.errors, String(errors.join('\n\n'))); // Best-effort to all stack traces for debugging - newErr.stack = `${newErr.stack}\n\n${executedContext.errors.map(subErr => subErr.stack).join('\n\n')}`; - throw newErr; + error.stack = `${error.stack}\n\n${executedContext.errors.map(subErr => subErr.stack).join('\n\n')}`; } - return result; + return { result, error }; } diff --git a/packages/graphql-codegen-cli/src/generate-and-save.ts b/packages/graphql-codegen-cli/src/generate-and-save.ts index b95511219b7..384b84e3a4e 100644 --- a/packages/graphql-codegen-cli/src/generate-and-save.ts +++ b/packages/graphql-codegen-cli/src/generate-and-save.ts @@ -1,5 +1,6 @@ import { createHash } from 'crypto'; import { dirname, isAbsolute, join } from 'path'; +import logSymbols from 'log-symbols'; import { Types } from '@graphql-codegen/plugin-helpers'; import { executeCodegen } from './codegen.js'; import { CodegenContext, ensureContext } from './config.js'; @@ -7,6 +8,7 @@ import { lifecycleHooks } from './hooks.js'; import { debugLog } from './utils/debugging.js'; import { mkdirp, readFile, unlinkFile, writeFile } from './utils/file-system.js'; import { createWatcher } from './utils/watcher.js'; +import { getLogger } from './utils/logger.js'; const hash = (content: string): string => createHash('sha1').update(content).digest('base64'); @@ -133,7 +135,27 @@ export async function generate( return createWatcher(context, writeOutput).runningWatcher; } - const outputFiles = await context.profiler.run(() => executeCodegen(context), 'executeCodegen'); + const { result: outputFiles, error } = await context.profiler.run(() => executeCodegen(context), 'executeCodegen'); + + if (error) { + // If all generation failed, just throw to return non-zero code. + if (outputFiles.length === 0) { + throw error; + } + + // If partial success, but partial output is not allowed, throw to return non-zero code. + if (!config.allowPartialOutputs) { + getLogger().error( + ` ${logSymbols.error} One or more errors occurred, no files were generated. To allow output on errors, set config.allowPartialOutputs=true` + ); + throw error; + } + + // If partial success, and partial output is allowed, warn and proceed to write to files. + getLogger().warn( + ` ${logSymbols.warning} One or more errors occurred, some files were generated. To prevent any output on errors, set config.allowPartialOutputs=false` + ); + } await context.profiler.run(() => writeOutput(outputFiles), 'writeOutput'); await context.profiler.run(() => lifecycleHooks(config.hooks).beforeDone(), 'Lifecycle: beforeDone'); diff --git a/packages/graphql-codegen-cli/src/graphql-config.ts b/packages/graphql-codegen-cli/src/graphql-config.ts index 8662ba922da..b3abba587da 100644 --- a/packages/graphql-codegen-cli/src/graphql-config.ts +++ b/packages/graphql-codegen-cli/src/graphql-config.ts @@ -2,7 +2,6 @@ import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; import { GitLoader } from '@graphql-tools/git-loader'; import { GithubLoader } from '@graphql-tools/github-loader'; -import { PrismaLoader } from '@graphql-tools/prisma-loader'; import { GraphQLConfig, GraphQLExtensionDeclaration, loadConfig } from 'graphql-config'; export const CodegenExtension: GraphQLExtensionDeclaration = (api: any) => { @@ -17,7 +16,6 @@ export const CodegenExtension: GraphQLExtensionDeclaration = (api: any) => { api.loaders.schema.register(new GitLoader()); api.loaders.schema.register(new GithubLoader()); api.loaders.schema.register(new ApolloEngineLoader()); - api.loaders.schema.register(new PrismaLoader()); // Documents api.loaders.documents.register( new CodeFileLoader({ diff --git a/packages/graphql-codegen-cli/src/load.ts b/packages/graphql-codegen-cli/src/load.ts index 74e97685a7d..8dec5e22e68 100644 --- a/packages/graphql-codegen-cli/src/load.ts +++ b/packages/graphql-codegen-cli/src/load.ts @@ -12,7 +12,6 @@ import { NoTypeDefinitionsFound, UnnormalizedTypeDefPointer, } from '@graphql-tools/load'; -import { PrismaLoader } from '@graphql-tools/prisma-loader'; import { UrlLoader } from '@graphql-tools/url-loader'; import { GraphQLError, GraphQLSchema } from 'graphql'; @@ -41,7 +40,6 @@ export async function loadSchema( new JsonFileLoader(), new UrlLoader(), new ApolloEngineLoader(), - new PrismaLoader(), ]; const schema = await loadSchemaToolkit(schemaPointers, { diff --git a/packages/graphql-codegen-cli/src/utils/watcher.ts b/packages/graphql-codegen-cli/src/utils/watcher.ts index cbe2cdaf4da..72a8039998e 100644 --- a/packages/graphql-codegen-cli/src/utils/watcher.ts +++ b/packages/graphql-codegen-cli/src/utils/watcher.ts @@ -78,7 +78,10 @@ export const createWatcher = ( const debouncedExec = debounce(() => { if (!isShutdown) { executeCodegen(initialContext) - .then(onNext, () => Promise.resolve()) + .then( + ({ result }) => onNext(result), + () => Promise.resolve() + ) .then(() => emitWatching(watchDirectory)); } }, 100); @@ -198,7 +201,10 @@ export const createWatcher = ( */ stopWatching.runningWatcher = new Promise((resolve, reject) => { executeCodegen(initialContext) - .then(onNext, () => Promise.resolve()) + .then( + ({ result }) => onNext(result), + () => Promise.resolve() + ) .then(() => runWatcher(abortController.signal)) .catch(err => { watcherSubscription.unsubscribe(); diff --git a/packages/graphql-codegen-cli/tests/codegen.spec.ts b/packages/graphql-codegen-cli/tests/codegen.spec.ts index 7eb7e5fe9ab..cae2ad05ce3 100644 --- a/packages/graphql-codegen-cli/tests/codegen.spec.ts +++ b/packages/graphql-codegen-cli/tests/codegen.spec.ts @@ -25,7 +25,7 @@ describe('Codegen Executor', () => { describe('Generator General Options', () => { it('Should output the correct filenames', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out1.ts': { plugins: ['typescript'] }, @@ -33,13 +33,13 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(2); - expect(output.map(f => f.filename)).toEqual(expect.arrayContaining(['out1.ts', 'out2.ts'])); + expect(result.length).toBe(2); + expect(result.map(f => f.filename)).toEqual(expect.arrayContaining(['out1.ts', 'out2.ts'])); }); it('Should load require extensions', async () => { expect((global as any).dummyWasLoaded).toBeFalsy(); - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: join(__dirname, './test-files/schema-dir/schema-object.js'), require: join(__dirname, './dummy-require.js'), generates: { @@ -48,7 +48,7 @@ describe('Codegen Executor', () => { cwd: __dirname, }); - expect(output.length).toBe(1); + expect(result.length).toBe(1); expect((global as any).dummyWasLoaded).toBeTruthy(); }); @@ -70,7 +70,7 @@ describe('Codegen Executor', () => { }); it('Should accept plugins as object', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -84,12 +84,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type RootQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type RootQuery'); }); it('Should accept plugins as array of objects', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -99,8 +99,8 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type RootQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type RootQuery'); }); it('Should throw when no output files has been specified', async () => { @@ -118,7 +118,7 @@ describe('Codegen Executor', () => { }); it('Should work with just schema', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out.ts': { @@ -127,12 +127,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); + expect(result.length).toBe(1); }); it('Should not throw when every output has a schema and there is no root schema', async () => { try { - const output = await executeCodegen({ + const { result } = await executeCodegen({ generates: { 'out.ts': { schema: SIMPLE_TEST_SCHEMA, @@ -141,7 +141,7 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); + expect(result.length).toBe(1); } catch (e) { expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING); expect(e.message).not.toMatch('Invalid Codegen Configuration!'); @@ -207,7 +207,7 @@ describe('Codegen Executor', () => { }); it('should handle extend keyword when GraphQLSchema is used', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: './tests/test-files/schema-dir/with-extend.js', generates: { 'out.ts': { @@ -216,15 +216,15 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].filename).toBe('out.ts'); - expect(output[0].content).toContain(`hello?: Maybe`); + expect(result.length).toBe(1); + expect(result[0].filename).toBe('out.ts'); + expect(result[0].content).toContain(`hello?: Maybe`); }); }); describe('Per-output options', () => { it('Should allow to specify schema extension for specific output', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out1.ts': { @@ -236,14 +236,14 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type Query'); - expect(output[0].content).toContain('export type MyType'); - expect(output[0].content).toContain('export type OtherType'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type Query'); + expect(result[0].content).toContain('export type MyType'); + expect(result[0].content).toContain('export type OtherType'); }); it('Should allow to specify documents extension for specific output', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out1.ts': { @@ -253,12 +253,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type QQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type QQuery'); }); it('Should extend existing documents', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -269,31 +269,26 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type QQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type QQuery'); }); - it('Should throw on duplicated names', async () => { - try { - await executeCodegen({ - schema: ` + it('Should return error on duplicated names', async () => { + const { error } = await executeCodegen({ + schema: ` type RootQuery { f: String } schema { query: RootQuery } `, - documents: [`query q { e }`, `query q { f }`], - generates: { - 'out1.ts': { plugins: ['typescript'] }, - }, - }); - throw SHOULD_NOT_THROW_STRING; - } catch (e) { - expect(e).not.toEqual(SHOULD_NOT_THROW_STRING); - expect(e.message).toContain('Not all operations have an unique name: q'); - } + documents: [`query q { e }`, `query q { f }`], + generates: { + 'out1.ts': { plugins: ['typescript'] }, + }, + }); + expect(error.message).toContain('Not all operations have an unique name: q'); }); it('should handle gql tag in ts with with nested fragment', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/my-fragment.ts', './tests/test-documents/query-with-my-fragment.ts'], generates: { @@ -307,7 +302,7 @@ describe('Codegen Executor', () => { }); it('should handle gql tag in ts with with multiple nested fragment', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/my-fragment.ts', './tests/test-documents/query-with-my-fragment.ts'], generates: { @@ -322,7 +317,7 @@ describe('Codegen Executor', () => { }); it('should handle gql tag in js with with nested fragment', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/js-query-with-my-fragment.js', './tests/test-documents/js-my-fragment.js'], generates: { @@ -337,7 +332,7 @@ describe('Codegen Executor', () => { }); it('should handle TypeScript features', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/ts-features-with-query.ts'], generates: { @@ -353,7 +348,7 @@ describe('Codegen Executor', () => { }); it('should handle multiple fragments with the same name, but one is commented out', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/query-with-commented-fragment.ts'], generates: { @@ -367,7 +362,7 @@ describe('Codegen Executor', () => { }); it('should handle graphql-tag and gatsby by default (documents)', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/gatsby-and-custom-parsers.ts'], generates: { @@ -382,7 +377,7 @@ describe('Codegen Executor', () => { }); it('should handle custom graphql string parsers (documents)', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['./tests/test-documents/schema.graphql'], documents: ['./tests/test-documents/gatsby-and-custom-parsers.ts'], generates: { @@ -446,7 +441,7 @@ describe('Codegen Executor', () => { describe('Plugin Configuration', () => { it('Should inherit root config', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, config: { @@ -459,13 +454,13 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type rootquery'); - expect(output[0].content).toContain('export type root'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type rootquery'); + expect(result[0].content).toContain('export type root'); }); it('Should accept config in per-output (override)', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -484,13 +479,13 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(2); - expect(output[0].content).toContain('export type rootquery'); - expect(output[1].content).toContain('export type ROOTQUERY'); + expect(result.length).toBe(2); + expect(result[0].content).toContain('export type rootquery'); + expect(result[1].content).toContain('export type ROOTQUERY'); }); it('Should accept config in per-plugin', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -506,13 +501,13 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type root'); - expect(output[0].content).toContain('export type rootquery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type root'); + expect(result[0].content).toContain('export type rootquery'); }); it('Should allow override of config in', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, config: { @@ -540,15 +535,15 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(2); - expect(output[0].content).toContain('export type ROOTQUERY'); - expect(output[1].content).toContain('export type RootQuery'); + expect(result.length).toBe(2); + expect(result[0].content).toContain('export type ROOTQUERY'); + expect(result[1].content).toContain('export type RootQuery'); }); }); describe('Plugin loading', () => { it('Should load custom plugin from local file', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out1.ts': { @@ -557,46 +552,36 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('plugin'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('plugin'); }); - it('Should throw when custom plugin is not valid', async () => { - try { - await executeCodegen({ - schema: SIMPLE_TEST_SCHEMA, - generates: { - 'out1.ts': { - plugins: ['./tests/custom-plugins/invalid.js'], - }, + it('Should return error when custom plugin is not valid', async () => { + const { error } = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + generates: { + 'out1.ts': { + plugins: ['./tests/custom-plugins/invalid.js'], }, - }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (e) { - expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING); - expect(e.message).toContain('Invalid Custom Plugin'); - } + }, + }); + expect(error.message).toContain('Invalid Custom Plugin'); }); - it('Should execute custom plugin validation and throw when it fails', async () => { - try { - await executeCodegen({ - schema: SIMPLE_TEST_SCHEMA, - generates: { - 'out1.ts': { - plugins: ['./tests/custom-plugins/validation.js'], - }, + it('Should execute custom plugin validation and return error when it fails', async () => { + const { error } = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + generates: { + 'out1.ts': { + plugins: ['./tests/custom-plugins/validation.js'], }, - }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (e) { - expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING); - expect(e.message).toContain('validation failed'); - } + }, + }); + expect(error.message).toContain('validation failed'); }); it('Should allow plugins to extend schema', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { 'out1.ts': { @@ -605,13 +590,13 @@ describe('Codegen Executor', () => { }, }); - expect(output[0].content).toContain('MyType,'); - expect(output[0].content).toContain('Extension'); - expect(output[0].content).toContain(`Should have the Extension type: 'Extension'`); + expect(result[0].content).toContain('MyType,'); + expect(result[0].content).toContain('Extension'); + expect(result[0].content).toContain(`Should have the Extension type: 'Extension'`); }); it('Should allow plugins to extend schema (using a function)', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, config: { test: 'MyType', @@ -623,7 +608,7 @@ describe('Codegen Executor', () => { }, }); - expect(output[0].content).toContain('MyType'); + expect(result[0].content).toContain('MyType'); }); }); @@ -708,7 +693,7 @@ describe('Codegen Executor', () => { } `; - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: [schemaA, schemaB], generates: { 'out1.ts': { @@ -717,8 +702,8 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toBeSimilarStringTo(`export type Scalars = { + expect(result.length).toBe(1); + expect(result[0].content).toBeSimilarStringTo(`export type Scalars = { ID: { input: string; output: string; } String: { input: string; output: string; } Boolean: { input: boolean; output: boolean; } @@ -766,72 +751,60 @@ describe('Codegen Executor', () => { expect((global as any).CUSTOM_SCHEMA_LOADER_CALLED).toBeTruthy(); }); - it('Should throw when invalid return value from loader', async () => { - try { - await executeCodegen({ - schema: [ - { - './tests/test-documents/schema.graphql': { - loader: './tests/custom-loaders/invalid-return-value-schema-loader.js', - }, + it('Should return error when invalid return value from loader', async () => { + const { error } = await executeCodegen({ + schema: [ + { + './tests/test-documents/schema.graphql': { + loader: './tests/custom-loaders/invalid-return-value-schema-loader.js', }, - ], - generates: { - 'out1.ts': { plugins: ['typescript'] }, }, - }); + ], + generates: { + 'out1.ts': { plugins: ['typescript'] }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Failed to load schema'); - } + expect(error.message).toContain('Failed to load schema'); }); - it('Should throw when invalid module specified as loader', async () => { - try { - await executeCodegen({ - schema: [ - { - './tests/test-documents/schema.graphql': { - loader: './tests/custom-loaders/non-existing.js', - }, - }, - ], - generates: { - 'out1.ts': { - plugins: ['typescript'], + it('Should return error when invalid module specified as loader', async () => { + const { error } = await executeCodegen({ + schema: [ + { + './tests/test-documents/schema.graphql': { + loader: './tests/custom-loaders/non-existing.js', }, }, - }); + ], + generates: { + 'out1.ts': { + plugins: ['typescript'], + }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Failed to load custom loader'); - } + expect(error.message).toContain('Failed to load custom loader'); }); - it('Should throw when invalid file declaration', async () => { - try { - await executeCodegen({ - schema: [ - { - './tests/test-documents/schema.graphql': { - loader: './tests/custom-loaders/invalid-export.js', - }, - }, - ], - generates: { - 'out1.ts': { - plugins: ['typescript'], + it('Should return error when invalid file declaration', async () => { + const { error } = await executeCodegen({ + schema: [ + { + './tests/test-documents/schema.graphql': { + loader: './tests/custom-loaders/invalid-export.js', }, }, - }); + ], + generates: { + 'out1.ts': { + plugins: ['typescript'], + }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Failed to load schema'); - expect(error.message).toContain('Failed to load custom loader'); - } + expect(error.message).toContain('Failed to load schema'); + expect(error.message).toContain('Failed to load custom loader'); }); }); @@ -876,76 +849,64 @@ describe('Codegen Executor', () => { expect((global as any).CUSTOM_DOCUMENT_LOADER_CALLED).toBeTruthy(); }); - it('Should throw when invalid return value from custom documents loader', async () => { - try { - await executeCodegen({ - schema: ['./tests/test-documents/schema.graphql'], - documents: [ - { - './tests/test-documents/valid.graphql': { - loader: './tests/custom-loaders/invalid-return-value-documents-loader.js', - }, - }, - ], - generates: { - 'out1.ts': { - plugins: ['typescript'], + it('Should return error when invalid return value from custom documents loader', async () => { + const { error } = await executeCodegen({ + schema: ['./tests/test-documents/schema.graphql'], + documents: [ + { + './tests/test-documents/valid.graphql': { + loader: './tests/custom-loaders/invalid-return-value-documents-loader.js', }, }, - }); + ], + generates: { + 'out1.ts': { + plugins: ['typescript'], + }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Unable to find any GraphQL type definitions for the following pointers'); - } + expect(error.message).toContain('Unable to find any GraphQL type definitions for the following pointers'); }); - it('Should throw when invalid module specified as loader', async () => { - try { - await executeCodegen({ - schema: ['./tests/test-documents/schema.graphql'], - documents: [ - { - './tests/test-documents/valid.graphql': { - loader: './tests/custom-loaders/non-existing.js', - }, - }, - ], - generates: { - 'out1.ts': { - plugins: ['typescript'], + it('Should return error when invalid module specified as loader', async () => { + const { error } = await executeCodegen({ + schema: ['./tests/test-documents/schema.graphql'], + documents: [ + { + './tests/test-documents/valid.graphql': { + loader: './tests/custom-loaders/non-existing.js', }, }, - }); + ], + generates: { + 'out1.ts': { + plugins: ['typescript'], + }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Failed to load custom loader'); - } + expect(error.message).toContain('Failed to load custom loader'); }); - it('Should throw when invalid file declaration', async () => { - try { - await executeCodegen({ - schema: ['./tests/test-documents/schema.graphql'], - documents: [ - { - './tests/test-documents/valid.graphql': { - loader: './tests/custom-loaders/invalid-export.js', - }, - }, - ], - generates: { - 'out1.ts': { - plugins: ['typescript'], + it('Should return error when invalid file declaration', async () => { + const { error } = await executeCodegen({ + schema: ['./tests/test-documents/schema.graphql'], + documents: [ + { + './tests/test-documents/valid.graphql': { + loader: './tests/custom-loaders/invalid-export.js', }, }, - }); + ], + generates: { + 'out1.ts': { + plugins: ['typescript'], + }, + }, + }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (error) { - expect(error.message).toContain('Failed to load custom loader'); - } + expect(error.message).toContain('Failed to load custom loader'); }); }); @@ -1016,7 +977,7 @@ describe('Codegen Executor', () => { it('Should allow plugins to extend schema with custom root', async () => { try { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: `schema { query: RootQuery } type MyType { f: String } type RootQuery { f: String }`, documents: `query root { f }`, generates: { @@ -1025,14 +986,14 @@ describe('Codegen Executor', () => { }, }, }); - expect(output.length).toBe(1); + expect(result.length).toBe(1); } catch (e) { expect(e.message).not.toBe('Query root type must be provided.'); } }); it('Should allow plugin context to be accessed and modified', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ { './tests/test-documents/schema.graphql': { @@ -1047,8 +1008,8 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('Hello world!'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('Hello world!'); }); it('Should sort the input schema', async () => { @@ -1069,7 +1030,7 @@ describe('Codegen Executor', () => { b: String } `; - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: [nonSortedSchema], generates: { 'out1.graphql': { @@ -1081,8 +1042,8 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toBeSimilarStringTo(/* GraphQL */ ` + expect(result.length).toBe(1); + expect(result[0].content).toBeSimilarStringTo(/* GraphQL */ ` type A { b: String s: String @@ -1133,8 +1094,8 @@ describe('Codegen Executor', () => { watch: false, }); const config = prj1.getConfig(); - const output = await executeCodegen(config); - expect(output[0].content).toContain('DocumentNode'); + const { result } = await executeCodegen(config); + expect(result[0].content).toContain('DocumentNode'); }); describe('Document Transform', () => { @@ -1156,7 +1117,7 @@ describe('Codegen Executor', () => { return newDocuments; }; - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query foo { f }`, generates: { @@ -1167,8 +1128,8 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type BarQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type BarQuery'); }); it('Should allow users to set config', async () => { @@ -1195,7 +1156,7 @@ describe('Codegen Executor', () => { }; }; - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query foo { f }`, generates: { @@ -1206,12 +1167,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type TestQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type TestQuery'); }); it('Should transform documents when specifying files', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -1222,12 +1183,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type BarQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type BarQuery'); }); it('Should allow users to set config when specifying files', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -1244,12 +1205,12 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('export type TestQuery'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('export type TestQuery'); }); it('Should allow plugin context to be accessed and modified', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query root { f }`, generates: { @@ -1267,34 +1228,30 @@ describe('Codegen Executor', () => { }, }); - expect(output.length).toBe(1); - expect(output[0].content).toContain('Hello world!'); + expect(result.length).toBe(1); + expect(result[0].content).toContain('Hello world!'); }); - it('should throw an understandable error if it fails.', async () => { - try { - await executeCodegen({ - schema: SIMPLE_TEST_SCHEMA, - documents: `query foo { f }`, - generates: { - 'out1.ts': { - plugins: ['typescript'], - documentTransforms: [ - { - transform: () => { - throw new Error('Something Wrong!'); - }, + it('should return error an understandable error if it fails.', async () => { + const { error } = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query foo { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript'], + documentTransforms: [ + { + transform: () => { + throw new Error('Something Wrong!'); }, - ], - }, + }, + ], }, - }); - throw new Error(SHOULD_NOT_THROW_STRING); - } catch (e) { - expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING); - expect(e.message).toContain('DocumentTransform "the element at index 0 of the documentTransforms" failed'); - expect(e.message).toContain('Something Wrong!'); - } + }, + }); + + expect(error.message).toContain('DocumentTransform "the element at index 0 of the documentTransforms" failed'); + expect(error.message).toContain('Something Wrong!'); }); it('Should transform documents with client-preset', async () => { @@ -1315,7 +1272,7 @@ describe('Codegen Executor', () => { return newDocuments; }; - const output = await executeCodegen({ + const { result } = await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, documents: `query foo { f }`, generates: { @@ -1326,13 +1283,13 @@ describe('Codegen Executor', () => { }, }); - const fileOutput = output.find(file => file.filename === './src/gql/graphql.ts'); + const fileOutput = result.find(file => file.filename === './src/gql/graphql.ts'); expect(fileOutput.content).toContain('export type BarQuery'); }); }); it('should not run out of memory when generating very complex types (issue #7720)', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: ['../../dev-test/gatsby/schema.graphql'], documents: ['../../dev-test/gatsby/fragments.ts'], config: { diff --git a/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts b/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts index 549ca460eca..730c6ca86f3 100644 --- a/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts +++ b/packages/graphql-codegen-cli/tests/generate-and-save.spec.ts @@ -1,10 +1,12 @@ import { dirname, join } from 'path'; +import logSymbols from 'log-symbols'; import { Types } from '@graphql-codegen/plugin-helpers'; import { useMonorepo } from '@graphql-codegen/testing'; import makeDir from 'make-dir'; import { createContext } from '../src/config.js'; import { generate } from '../src/generate-and-save.js'; import * as fs from '../src/utils/file-system.js'; +import { setLogger } from '../src/utils/logger.js'; const SIMPLE_TEST_SCHEMA = `type MyType { f: String } type Query { f: String }`; @@ -425,4 +427,150 @@ describe('generate-and-save', () => { expect(consoleErrorMock).not.toHaveBeenCalled(); }); }); + + describe('config.allowPartialOutputs', () => { + const mockLogger: any = { + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + setLogger(mockLogger); + }); + + test('when allowPartialOutputs=true - writes partial success and does not throw', async () => { + const invalidSchema = /* GraphQL */ ` + type A { + id: WRONG_TYPE! + } + `; + const validSchema = /* GraphQL */ ` + type B { + id: ID! + } + `; + const output = await generate( + { + allowPartialOutputs: true, + generates: { + 'src/a.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + 'src/b.ts': { + schema: validSchema, + plugins: ['typescript'], + }, + }, + }, + false + ); + + expect(output.length).toBe(1); + expect(output[0].filename).toBe('src/b.ts'); + expect(mockLogger.warn.mock.calls[0][0]).toBeSimilarStringTo( + `${logSymbols.warning} One or more errors occurred, some files were generated. To prevent any output on errors, set config.allowPartialOutputs=false` + ); + }); + + test('when allowPartialOutputs=true - complete failure throws', async () => { + expect.assertions(2); + + try { + const invalidSchema = /* GraphQL */ ` + type A { + id: WRONG_TYPE! + } + `; + await generate( + { + allowPartialOutputs: true, + generates: { + 'src/a.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + 'src/b.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + }, + }, + false + ); + } catch { + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + } + }); + + test('when allowPartialOutputs=false - does not write partial success and throws', async () => { + expect.assertions(1); + + const invalidSchema = /* GraphQL */ ` + type A { + id: WRONG_TYPE! + } + `; + const validSchema = /* GraphQL */ ` + type B { + id: ID! + } + `; + + try { + await generate( + { + allowPartialOutputs: false, + generates: { + 'src/a.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + 'src/b.ts': { + schema: validSchema, + plugins: ['typescript'], + }, + }, + }, + false + ); + } catch { + expect(mockLogger.error.mock.calls[0][0]).toBeSimilarStringTo( + `${logSymbols.error} One or more errors occurred, no files were generated. To allow output on errors, set config.allowPartialOutputs=true` + ); + } + }); + + test('when allowPartialOutputs=false - complete failure throws', async () => { + expect.assertions(2); + + try { + const invalidSchema = /* GraphQL */ ` + type A { + id: WRONG_TYPE! + } + `; + await generate( + { + allowPartialOutputs: false, + generates: { + 'src/a.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + 'src/b.ts': { + schema: invalidSchema, + plugins: ['typescript'], + }, + }, + }, + false + ); + } catch { + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + } + }); + }); }); diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index 1f883822077..9bcb56a6ed9 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -1,4 +1,4 @@ -import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers'; +import { ApolloFederation, type FederationMeta, getBaseType } from '@graphql-codegen/plugin-helpers'; import { getRootTypeNames } from '@graphql-tools/utils'; import autoBind from 'auto-bind'; import { @@ -6,9 +6,11 @@ import { DirectiveDefinitionNode, EnumTypeDefinitionNode, FieldDefinitionNode, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLSchema, + GraphQLUnionType, InputValueDefinitionNode, InterfaceTypeDefinitionNode, isEnumType, @@ -18,7 +20,6 @@ import { isUnionType, ListTypeNode, NamedTypeNode, - NameNode, NonNullTypeNode, ObjectTypeDefinitionNode, ScalarTypeDefinitionNode, @@ -33,8 +34,6 @@ import { ConvertOptions, DeclarationKind, EnumValuesMap, - type NormalizedGenerateInternalResolversIfNeededConfig, - type GenerateInternalResolversIfNeededConfig, NormalizedAvoidOptionalsConfig, NormalizedScalarsMap, ParsedEnumValuesMap, @@ -77,14 +76,20 @@ export interface ParsedResolversConfig extends ParsedConfig { resolverTypeSuffix: string; allResolversTypeName: string; internalResolversPrefix: string; - generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; - onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; avoidCheckingAbstractTypesRecursively: boolean; } -type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; +export interface FieldDefinitionResult { + node: FieldDefinitionNode; + printContent: FieldDefinitionPrintFn; +} + +type FieldDefinitionPrintFn = ( + parentNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + avoidResolverOptionals: boolean +) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } }; export interface RootResolver { content: string; generatedResolverTypes: { @@ -616,22 +621,6 @@ export interface RawResolversConfig extends RawConfig { * If you are using `mercurius-js`, please set this field to empty string for better compatibility. */ internalResolversPrefix?: string; - /** - * @type object - * @default { __resolveReference: false } - * @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met. - * Enabling this allows a more correct type generation for the resolvers. - * For example: - * - `__isTypeOf` is generated for implementing types and union members - * - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive - */ - generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig; - /** - * @type boolean - * @default false - * @description Turning this flag to `true` will generate resolver signature that has only `resolveType` for interfaces, forcing developers to write inherited type resolvers in the type itself. - */ - onlyResolveTypeForInterfaces?: boolean; /** * @description Makes `__typename` of resolver mappings non-optional without affecting the base types. * @default false @@ -702,7 +691,7 @@ export class BaseResolversVisitor< TRawConfig extends RawResolversConfig = RawResolversConfig, TPluginConfig extends ParsedResolversConfig = ParsedResolversConfig > extends BaseVisitor { - protected _parsedConfig: TPluginConfig; + protected declare _parsedConfig: TPluginConfig; protected _declarationBlockConfig: DeclarationBlockConfig = {}; protected _collectedResolvers: { [key: string]: { @@ -710,6 +699,31 @@ export class BaseResolversVisitor< baseGeneratedTypename?: string; }; } = {}; + protected _parsedSchemaMeta: { + types: { + interface: Record< + string, + { + type: GraphQLInterfaceType; + implementingTypes: Record; + } + >; + union: Record< + string, + { + type: GraphQLUnionType; + unionMembers: Record; + } + >; + }; + typesWithIsTypeOf: Record; + } = { + types: { + interface: {}, + union: {}, + }, + typesWithIsTypeOf: {}, + }; protected _collectedDirectiveResolvers: { [key: string]: string } = {}; protected _variablesTransformer: OperationVariablesToObject; protected _usedMappers: { [key: string]: boolean } = {}; @@ -718,7 +732,6 @@ export class BaseResolversVisitor< protected _hasReferencedResolversUnionTypes = false; protected _hasReferencedResolversInterfaceTypes = false; protected _resolversUnionTypes: Record = {}; - protected _resolversUnionParentTypes: Record = {}; protected _resolversInterfaceTypes: Record = {}; protected _rootTypeNames = new Set(); protected _globalDeclarations = new Set(); @@ -734,7 +747,8 @@ export class BaseResolversVisitor< rawConfig: TRawConfig, additionalConfig: TPluginConfig, private _schema: GraphQLSchema, - defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS + defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS, + federationMeta: FederationMeta = {} ) { super(rawConfig, { immutableTypes: getConfigValue(rawConfig.immutableTypes, false), @@ -748,7 +762,6 @@ export class BaseResolversVisitor< mapOrStr: rawConfig.enumValues, }), addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false), - onlyResolveTypeForInterfaces: getConfigValue(rawConfig.onlyResolveTypeForInterfaces, false), contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'), fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []), directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []), @@ -763,9 +776,7 @@ export class BaseResolversVisitor< mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix), scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars), internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'), - generateInternalResolversIfNeeded: { - __resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false, - }, + generateInternalResolversIfNeeded: {}, resolversNonOptionalTypename: normalizeResolversNonOptionalTypename( getConfigValue(rawConfig.resolversNonOptionalTypename, false) ), @@ -774,7 +785,11 @@ export class BaseResolversVisitor< } as TPluginConfig); autoBind(this); - this._federation = new ApolloFederation({ enabled: this.config.federation, schema: this.schema }); + this._federation = new ApolloFederation({ + enabled: this.config.federation, + schema: this.schema, + meta: federationMeta, + }); this._rootTypeNames = getRootTypeNames(_schema); this._variablesTransformer = new OperationVariablesToObject( this.scalars, @@ -782,11 +797,17 @@ export class BaseResolversVisitor< this.config.namespacedImportName ); + // 1. Parse schema meta at the start once, + // so we can use it in subsequent generate functions + this.parseSchemaMeta(); + + // 2. Generate types for resolvers this._resolversTypes = this.createResolversFields({ applyWrapper: type => this.applyResolverTypeWrapper(type), clearWrapper: type => this.clearResolverTypeWrapper(type), getTypeToUse: name => this.getTypeToUse(name), currentType: 'ResolversTypes', + onNotMappedObjectType: ({ initialType }) => initialType, }); this._resolversParentTypes = this.createResolversFields({ applyWrapper: type => type, @@ -794,6 +815,13 @@ export class BaseResolversVisitor< getTypeToUse: name => this.getParentTypeToUse(name), currentType: 'ResolversParentTypes', shouldInclude: namedType => !isEnumType(namedType), + onNotMappedObjectType: ({ typeName, initialType }) => { + let result = initialType; + if (this._federation.getMeta()[typeName]?.referenceSelectionSetsString) { + result += ` | ${this.convertName('FederationReferenceTypes')}['${typeName}']`; + } + return result; + }, }); this._resolversUnionTypes = this.createResolversUnionTypes(); this._resolversInterfaceTypes = this.createResolversInterfaceTypes(); @@ -866,12 +894,14 @@ export class BaseResolversVisitor< getTypeToUse, currentType, shouldInclude, + onNotMappedObjectType, }: { applyWrapper: (str: string) => string; clearWrapper: (str: string) => string; getTypeToUse: (str: string) => string; currentType: 'ResolversTypes' | 'ResolversParentTypes'; shouldInclude?: (type: GraphQLNamedType) => boolean; + onNotMappedObjectType: (params: { initialType: string; typeName: string }) => string; }): ResolverTypes { const allSchemaTypes = this._schema.getTypeMap(); const typeNames = this._federation.filterTypeNames(Object.keys(allSchemaTypes)); @@ -960,6 +990,11 @@ export class BaseResolversVisitor< if (this.config.mappers[typeName].type && hasPlaceholder(this.config.mappers[typeName].type)) { internalType = replacePlaceholder(this.config.mappers[typeName].type, internalType); } + } else { + internalType = onNotMappedObjectType({ + typeName, + initialType: internalType, + }); } prev[typeName] = applyWrapper(internalType); @@ -1033,22 +1068,20 @@ export class BaseResolversVisitor< return {}; } - const allSchemaTypes = this._schema.getTypeMap(); - const typeNames = this._federation.filterTypeNames(Object.keys(allSchemaTypes)); - - const unionTypes = typeNames.reduce>((res, typeName) => { - const schemaType = allSchemaTypes[typeName]; - - if (isUnionType(schemaType)) { - const { unionMember, excludeTypes } = this.config.resolversNonOptionalTypename; - res[typeName] = this.getAbstractMembersType({ - typeName, - memberTypes: schemaType.getTypes(), - isTypenameNonOptional: unionMember && !excludeTypes?.includes(typeName), - }); - } - return res; - }, {}); + const unionTypes = Object.entries(this._parsedSchemaMeta.types.union).reduce>( + (res, [typeName, { type: schemaType, unionMembers }]) => { + if (isUnionType(schemaType)) { + const { unionMember, excludeTypes } = this.config.resolversNonOptionalTypename; + res[typeName] = this.getAbstractMembersType({ + typeName, + memberTypes: Object.values(unionMembers), + isTypenameNonOptional: unionMember && !excludeTypes?.includes(typeName), + }); + } + return res; + }, + {} + ); return unionTypes; } @@ -1058,37 +1091,22 @@ export class BaseResolversVisitor< return {}; } - const allSchemaTypes = this._schema.getTypeMap(); - const typeNames = this._federation.filterTypeNames(Object.keys(allSchemaTypes)); - - const interfaceTypes = typeNames.reduce>((res, typeName) => { - const schemaType = allSchemaTypes[typeName]; - - if (isInterfaceType(schemaType)) { - const allTypesMap = this._schema.getTypeMap(); - const implementingTypes: GraphQLObjectType[] = []; - - for (const graphqlType of Object.values(allTypesMap)) { - if (graphqlType instanceof GraphQLObjectType) { - const allInterfaces = graphqlType.getInterfaces(); + const interfaceTypes = Object.entries(this._parsedSchemaMeta.types.interface).reduce>( + (res, [typeName, { type: schemaType, implementingTypes }]) => { + if (isInterfaceType(schemaType)) { + const { interfaceImplementingType, excludeTypes } = this.config.resolversNonOptionalTypename; - if (allInterfaces.some(int => int.name === schemaType.name)) { - implementingTypes.push(graphqlType); - } - } + res[typeName] = this.getAbstractMembersType({ + typeName, + memberTypes: Object.values(implementingTypes), + isTypenameNonOptional: interfaceImplementingType && !excludeTypes?.includes(typeName), + }); } - const { interfaceImplementingType, excludeTypes } = this.config.resolversNonOptionalTypename; - - res[typeName] = this.getAbstractMembersType({ - typeName, - memberTypes: implementingTypes, - isTypenameNonOptional: interfaceImplementingType && !excludeTypes?.includes(typeName), - }); - } - - return res; - }, {}); + return res; + }, + {} + ); return interfaceTypes; } @@ -1261,6 +1279,55 @@ export class BaseResolversVisitor< ).string; } + public buildFederationTypes(): string { + const federationMeta = this._federation.getMeta(); + + if (Object.keys(federationMeta).length === 0) { + return ''; + } + + const declarationKind = 'type'; + return new DeclarationBlock(this._declarationBlockConfig) + .export() + .asKind(declarationKind) + .withName(this.convertName('FederationTypes')) + .withComment('Mapping of federation types') + .withBlock( + Object.keys(federationMeta) + .map(typeName => { + return indent(`${typeName}: ${this.convertName(typeName)}${this.getPunctuation(declarationKind)}`); + }) + .join('\n') + ).string; + } + + public buildFederationReferenceTypes(): string { + const federationMeta = this._federation.getMeta(); + + if (Object.keys(federationMeta).length === 0) { + return ''; + } + + const declarationKind = 'type'; + return new DeclarationBlock(this._declarationBlockConfig) + .export() + .asKind(declarationKind) + .withName(this.convertName('FederationReferenceTypes')) + .withComment('Mapping of federation reference types') + .withBlock( + Object.entries(federationMeta) + .map(([typeName, { referenceSelectionSetsString }]) => { + if (!referenceSelectionSetsString) { + return undefined; + } + + return indent(`${typeName}: ${referenceSelectionSetsString}${this.getPunctuation(declarationKind)}`); + }) + .filter(v => v) + .join('\n') + ).string; + } + public get schema(): GraphQLSchema { return this._schema; } @@ -1374,7 +1441,9 @@ export class BaseResolversVisitor< const federationMeta = this._federation.getMeta()[schemaTypeName]; if (federationMeta) { - userDefinedTypes[schemaTypeName].federation = federationMeta; + userDefinedTypes[schemaTypeName].federation = { + hasResolveReference: federationMeta.hasResolveReference, + }; } } @@ -1425,10 +1494,6 @@ export class BaseResolversVisitor< return ''; } - Name(node: NameNode): string { - return node.value; - } - ListType(node: ListTypeNode): string { const asString = node.type as any as string; @@ -1442,7 +1507,7 @@ export class BaseResolversVisitor< } NamedType(node: NamedTypeNode): string { - const nameStr = node.name as any as string; + const nameStr = node.name.value; if (this.config.scalars[nameStr]) { return this._getScalar(nameStr); @@ -1481,133 +1546,135 @@ export class BaseResolversVisitor< return `ParentType extends ${parentType} = ${parentType}`; } - FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionPrintFn { + FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionResult { const hasArguments = node.arguments && node.arguments.length > 0; const declarationKind = 'type'; - return (parentName, avoidResolverOptionals) => { - const original: FieldDefinitionNode = parent[key]; - const parentType = this.schema.getType(parentName); - - if (this._federation.skipField({ fieldNode: original, parentType })) { - return null; - } - - const contextType = this.getContextType(parentName, node); - - let argsType = hasArguments - ? this.convertName( - parentName + - (this.config.addUnderscoreToArgsType ? '_' : '') + - this.convertName(node.name, { - useTypesPrefix: false, - useTypesSuffix: false, - }) + - 'Args', - { - useTypesPrefix: true, - }, - true - ) - : null; + const original: FieldDefinitionNode = parent[key]; - const avoidInputsOptionals = this.config.avoidOptionals.inputValue; + return { + node: original, + printContent: (parentNode, avoidResolverOptionals) => { + const parentName = parentNode.name.value; + const parentType = this.schema.getType(parentName); + const meta: ReturnType['meta'] = {}; + const typeName = node.name.value; + + const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node: parentNode }); + const shouldGenerateField = + fieldsToGenerate.some(field => field.name.value === typeName) || + this._federation.isResolveReferenceField(node); + + if (!shouldGenerateField) { + return { value: null, meta }; + } - if (argsType !== null) { - const argsToForceRequire = original.arguments.filter( - arg => !!arg.defaultValue || arg.type.kind === 'NonNullType' - ); + const contextType = this.getContextType(parentName, node); + + let argsType = hasArguments + ? this.convertName( + parentName + + (this.config.addUnderscoreToArgsType ? '_' : '') + + this.convertName(typeName, { + useTypesPrefix: false, + useTypesSuffix: false, + }) + + 'Args', + { + useTypesPrefix: true, + }, + true + ) + : null; + + const avoidInputsOptionals = this.config.avoidOptionals.inputValue; + + if (argsType !== null) { + const argsToForceRequire = original.arguments.filter( + arg => !!arg.defaultValue || arg.type.kind === 'NonNullType' + ); - if (argsToForceRequire.length > 0) { - argsType = this.applyRequireFields(argsType, argsToForceRequire); - } else if (original.arguments.length > 0 && avoidInputsOptionals !== true) { - argsType = this.applyOptionalFields(argsType, original.arguments); + if (argsToForceRequire.length > 0) { + argsType = this.applyRequireFields(argsType, argsToForceRequire); + } else if (original.arguments.length > 0 && avoidInputsOptionals !== true) { + argsType = this.applyOptionalFields(argsType, original.arguments); + } } - } - const parentTypeSignature = this._federation.transformParentType({ - fieldNode: original, - parentType, - parentTypeSignature: this.getParentTypeForSignature(node), - }); + const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => { + const baseType = getBaseTypeNode(original.type); + const realType = baseType.name.value; + const typeToUse = this.getTypeToUse(realType); + /** + * Turns GraphQL type to TypeScript types (`mappedType`) e.g. + * - String! -> ResolversTypes['String']> + * - String -> Maybe + * - [String] -> Maybe>> + * - [String!]! -> Array + */ + const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type); + + const subscriptionType = this._schema.getSubscriptionType(); + const isSubscriptionType = subscriptionType && subscriptionType.name === parentName; + + if (isSubscriptionType) { + return { + mappedTypeKey: `${mappedType}, "${typeName}"`, + resolverType: 'SubscriptionResolver', + }; + } + + const directiveMappings = + node.directives + ?.map(directive => this._directiveResolverMappings[directive.name.value]) + .filter(Boolean) + .reverse() ?? []; - const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => { - const baseType = getBaseTypeNode(original.type); - const realType = baseType.name.value; - const typeToUse = this.getTypeToUse(realType); - /** - * Turns GraphQL type to TypeScript types (`mappedType`) e.g. - * - String! -> ResolversTypes['String']> - * - String -> Maybe - * - [String] -> Maybe>> - * - [String!]! -> Array - */ - const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type); - - const subscriptionType = this._schema.getSubscriptionType(); - const isSubscriptionType = subscriptionType && subscriptionType.name === parentName; - - if (isSubscriptionType) { return { - mappedTypeKey: `${mappedType}, "${node.name}"`, - resolverType: 'SubscriptionResolver', + mappedTypeKey: mappedType, + resolverType: directiveMappings[0] ?? 'Resolver', }; - } - - const directiveMappings = - node.directives - ?.map(directive => this._directiveResolverMappings[directive.name as any]) - .filter(Boolean) - .reverse() ?? []; - - return { - mappedTypeKey: mappedType, - resolverType: directiveMappings[0] ?? 'Resolver', + })(); + + const signature: { + name: string; + modifier: string; + type: string; + genericTypes: string[]; + } = { + name: typeName, + modifier: avoidResolverOptionals ? '' : '?', + type: resolverType, + genericTypes: [mappedTypeKey, this.getParentTypeForSignature(node), contextType, argsType].filter(f => f), }; - })(); - - const signature: { - name: string; - modifier: string; - type: string; - genericTypes: string[]; - } = { - name: node.name as any, - modifier: avoidResolverOptionals ? '' : '?', - type: resolverType, - genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f), - }; - - if (this._federation.isResolveReferenceField(node)) { - if (this.config.generateInternalResolversIfNeeded.__resolveReference) { - const federationDetails = checkObjectTypeFederationDetails( - parentType.astNode as ObjectTypeDefinitionNode, - this._schema - ); - if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) { - return ''; + if (this._federation.isResolveReferenceField(node)) { + if (!this._federation.getMeta()[parentType.name].hasResolveReference) { + return { value: '', meta }; } - } + const resultType = `${mappedTypeKey} | FederationReferenceType`; + const referenceType = 'FederationReferenceType'; - this._federation.setMeta(parentType.name, { hasResolveReference: true }); - signature.type = 'ReferenceResolver'; - if (signature.genericTypes.length >= 3) { - signature.genericTypes = signature.genericTypes.slice(0, 3); + signature.type = 'ReferenceResolver'; + signature.genericTypes = [resultType, referenceType, contextType]; + meta.federation = { isResolveReference: true }; } - } - return indent( - `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( - ', ' - )}>${this.getPunctuation(declarationKind)}` - ); + return { + value: indent( + `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( + ', ' + )}>${this.getPunctuation(declarationKind)}` + ), + meta, + }; + }, }; } private getFieldContextType(parentName: string, node: FieldDefinitionNode): string { - if (this._fieldContextTypeMap[`${parentName}.${node.name}`]) { - return this._fieldContextTypeMap[`${parentName}.${node.name}`].type; + if (this._fieldContextTypeMap[`${parentName}.${node.name.value}`]) { + return this._fieldContextTypeMap[`${parentName}.${node.name.value}`].type; } return 'ContextType'; } @@ -1616,7 +1683,7 @@ export class BaseResolversVisitor< let contextType = this.getFieldContextType(parentName, node); for (const directive of node.directives) { - const name = directive.name as unknown as string; + const name = directive.name.value; const directiveMap = this._directiveContextTypesMap[name]; if (directiveMap) { contextType = `${directiveMap.type}<${contextType}>`; @@ -1625,6 +1692,46 @@ export class BaseResolversVisitor< return contextType; } + private parseSchemaMeta(): void { + const allSchemaTypes = this._schema.getTypeMap(); + const typeNames = this._federation.filterTypeNames(Object.keys(allSchemaTypes)); + + for (const typeName of typeNames) { + const schemaType = allSchemaTypes[typeName]; + + if (isUnionType(schemaType)) { + this._parsedSchemaMeta.types.union[schemaType.name] = { + type: schemaType, + unionMembers: {}, + }; + + const unionMemberTypes = schemaType.getTypes(); + for (const type of unionMemberTypes) { + this._parsedSchemaMeta.types.union[schemaType.name].unionMembers[type.name] = type; + this._parsedSchemaMeta.typesWithIsTypeOf[type.name] = true; + } + } + + if (isInterfaceType(schemaType)) { + this._parsedSchemaMeta.types.interface[schemaType.name] = { + type: schemaType, + implementingTypes: {}, + }; + + for (const graphqlType of Object.values(allSchemaTypes)) { + if (graphqlType instanceof GraphQLObjectType) { + const allInterfaces = graphqlType.getInterfaces(); + + if (allInterfaces.some(int => int.name === schemaType.name)) { + this._parsedSchemaMeta.types.interface[schemaType.name].implementingTypes[graphqlType.name] = graphqlType; + this._parsedSchemaMeta.typesWithIsTypeOf[graphqlType.name] = true; + } + } + } + } + } + } + protected applyRequireFields(argsType: string, fields: InputValueDefinitionNode[]): string { this._globalDeclarations.add(REQUIRE_FIELDS_TYPE); return `RequireFields<${argsType}, ${fields.map(f => `'${f.name.value}'`).join(' | ')}>`; @@ -1634,12 +1741,17 @@ export class BaseResolversVisitor< return `Partial<${argsType}>`; } - ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string { + ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | null { + const typeName = node.name.value; + const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node }); + if (fieldsToGenerate.length === 0) { + return null; + } + const declarationKind = 'type'; const name = this.convertName(node, { suffix: this.config.resolverTypeSuffix, }); - const typeName = node.name as any as string; const parentType = this.getParentTypeToUse(typeName); const rootType = ((): false | 'query' | 'mutation' | 'subscription' => { @@ -1655,17 +1767,19 @@ export class BaseResolversVisitor< return false; })(); - const fieldsContent = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f => { - return f( - typeName, - (rootType === 'query' && this.config.avoidOptionals.query) || - (rootType === 'mutation' && this.config.avoidOptionals.mutation) || - (rootType === 'subscription' && this.config.avoidOptionals.subscription) || - (rootType === false && this.config.avoidOptionals.resolvers) - ); - }); + const fieldsContent = (node.fields as unknown as FieldDefinitionResult[]) + .map(({ printContent }) => { + return printContent( + node, + (rootType === 'query' && this.config.avoidOptionals.query) || + (rootType === 'mutation' && this.config.avoidOptionals.mutation) || + (rootType === 'subscription' && this.config.avoidOptionals.subscription) || + (rootType === false && this.config.avoidOptionals.resolvers) + ).value; + }) + .filter(v => v); - if (!rootType) { + if (!rootType && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) { fieldsContent.push( indent( `${ @@ -1675,13 +1789,27 @@ export class BaseResolversVisitor< ); } + if (fieldsContent.length === 0) { + return null; + } + + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationReferenceTypes'), + typeName, + }); + const block = new DeclarationBlock(this._declarationBlockConfig) .export() .asKind(declarationKind) - .withName(name, ``) + .withName(name, `<${genericTypes.join(', ')}>`) .withBlock(fieldsContent.join('\n')); - this._collectedResolvers[node.name as any] = { + this._collectedResolvers[node.name.value] = { typename: name + '', baseGeneratedTypename: name, }; @@ -1700,11 +1828,11 @@ export class BaseResolversVisitor< .map(f => `'${f}'`) .join(' | '); - this._collectedResolvers[node.name as any] = { + this._collectedResolvers[node.name.value] = { typename: name + '', baseGeneratedTypename: name, }; - const parentType = this.getParentTypeToUse(node.name as any as string); + const parentType = this.getParentTypeToUse(node.name.value); return new DeclarationBlock(this._declarationBlockConfig) .export() @@ -1720,7 +1848,7 @@ export class BaseResolversVisitor< } ScalarTypeDefinition(node: ScalarTypeDefinitionNode): string { - const nameAsString = node.name as any as string; + const nameAsString = node.name.value; const baseName = this.getTypeToUse(nameAsString); if (this._federation.skipScalar(nameAsString)) { @@ -1728,7 +1856,7 @@ export class BaseResolversVisitor< } this._hasScalars = true; - this._collectedResolvers[node.name as any] = { + this._collectedResolvers[node.name.value] = { typename: 'GraphQLScalarType', }; @@ -1746,11 +1874,11 @@ export class BaseResolversVisitor< }), ` extends GraphQLScalarTypeConfig<${baseName}, any>` ) - .withBlock(indent(`name: '${node.name}'${this.getPunctuation('interface')}`)).string; + .withBlock(indent(`name: '${node.name.value}'${this.getPunctuation('interface')}`)).string; } DirectiveDefinition(node: DirectiveDefinitionNode, key: string | number, parent: any): string { - if (this._federation.skipDirective(node.name as any)) { + if (this._federation.skipDirective(node.name.value)) { return null; } @@ -1760,7 +1888,7 @@ export class BaseResolversVisitor< const sourceNode = parent[key] as DirectiveDefinitionNode; const hasArguments = sourceNode.arguments && sourceNode.arguments.length > 0; - this._collectedDirectiveResolvers[node.name as any] = directiveName + ''; + this._collectedDirectiveResolvers[node.name.value] = directiveName + ''; const directiveArgsTypeName = this.convertName(node, { suffix: 'DirectiveArgs', @@ -1809,7 +1937,7 @@ export class BaseResolversVisitor< } EnumTypeDefinition(node: EnumTypeDefinitionNode): string { - const rawTypeName = node.name as any; + const rawTypeName = node.name.value; // If we have enumValues set, and it's point to an external enum - we need to allow internal values resolvers // In case we have enumValues set but as explicit values, no need to to do mapping since it's already @@ -1842,45 +1970,53 @@ export class BaseResolversVisitor< suffix: this.config.resolverTypeSuffix, }); const declarationKind = 'type'; - const allTypesMap = this._schema.getTypeMap(); - const implementingTypes: string[] = []; - - const typeName = node.name as any as string; + const typeName = node.name.value; + const implementingTypes = Object.keys(this._parsedSchemaMeta.types.interface[typeName].implementingTypes); this._collectedResolvers[typeName] = { typename: name + '', baseGeneratedTypename: name, }; - for (const graphqlType of Object.values(allTypesMap)) { - if (graphqlType instanceof GraphQLObjectType) { - const allInterfaces = graphqlType.getInterfaces(); - if (allInterfaces.find(int => int.name === typeName)) { - implementingTypes.push(graphqlType.name); - } - } - } - const parentType = this.getParentTypeToUse(typeName); + + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationReferenceTypes'), + typeName, + }); + const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null'; - const fields = this.config.onlyResolveTypeForInterfaces ? [] : node.fields || []; + + // An Interface has __resolveType resolver, and no other fields. + const blockFields: string[] = [ + indent( + `${this.config.internalResolversPrefix}resolveType${ + this.config.optionalResolveType ? '?' : '' + }: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}` + ), + ]; + + // An Interface in Federation may have the additional __resolveReference resolver, if resolvable. + // So, we filter out the normal fields declared on the Interface and add the __resolveReference resolver. + const fields = (node.fields as unknown as FieldDefinitionResult[]).map(({ printContent }) => + printContent(node, this.config.avoidOptionals.resolvers) + ); + for (const field of fields) { + if (field.meta.federation?.isResolveReference) { + blockFields.push(field.value); + } + } return new DeclarationBlock(this._declarationBlockConfig) .export() .asKind(declarationKind) - .withName(name, ``) - .withBlock( - [ - indent( - `${this.config.internalResolversPrefix}resolveType${ - this.config.optionalResolveType ? '?' : '' - }: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}` - ), - ...(fields as unknown as FieldDefinitionPrintFn[]).map(f => - f(typeName, this.config.avoidOptionals.resolvers) - ), - ].join('\n') - ).string; + .withName(name, `<${genericTypes.join(', ')}>`) + .withBlock(blockFields.join('\n')).string; } SchemaDefinition() { diff --git a/packages/plugins/other/visitor-plugin-common/src/types.ts b/packages/plugins/other/visitor-plugin-common/src/types.ts index 16f64e0f029..88f01fe14ed 100644 --- a/packages/plugins/other/visitor-plugin-common/src/types.ts +++ b/packages/plugins/other/visitor-plugin-common/src/types.ts @@ -138,8 +138,3 @@ export interface CustomDirectivesConfig { */ apolloUnmask?: boolean; } - -export interface GenerateInternalResolversIfNeededConfig { - __resolveReference?: boolean; -} -export type NormalizedGenerateInternalResolversIfNeededConfig = Required; diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 0dbe840d1cd..0ccb1ce01aa 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -75,12 +75,20 @@ export type Resolver${capitalizedDirectiveName}WithResolve = { const stitchingResolverUsage = `StitchingResolver`; if (visitor.hasFederation()) { - if (visitor.config.wrapFieldDefinitions) { - defsToInclude.push(`export type UnwrappedObject = { - [P in keyof T]: T[P] extends infer R | Promise | (() => infer R2 | Promise) - ? R & R2 : T[P] - };`); - } - defsToInclude.push( `export type ReferenceResolver = ( reference: TReference, @@ -248,6 +249,8 @@ export type DirectiveResolverFn TResult | Promise; `; + const federationTypes = visitor.buildFederationTypes(); + const federationReferenceTypes = visitor.buildFederationReferenceTypes(); const resolversTypeMapping = visitor.buildResolversTypes(); const resolversParentTypeMapping = visitor.buildResolversParentTypes(); const resolversUnionTypesMapping = visitor.buildResolversUnionTypes(); @@ -291,6 +294,8 @@ export type DirectiveResolverFn { - constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema) { + constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema, federationMeta: FederationMeta) { super( pluginConfig, { @@ -41,7 +36,9 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< allowParentTypeOverride: getConfigValue(pluginConfig.allowParentTypeOverride, false), optionalInfoArgument: getConfigValue(pluginConfig.optionalInfoArgument, false), } as ParsedTypeScriptResolversConfig, - schema + schema, + DEFAULT_SCALARS, + federationMeta ); autoBind(this); this.setVariablesTransformer( @@ -96,13 +93,6 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< return `${this.config.immutableTypes ? 'ReadonlyArray' : 'Array'}<${str}>`; } - protected getParentTypeForSignature(node: FieldDefinitionNode) { - if (this._federation.isResolveReferenceField(node) && this.config.wrapFieldDefinitions) { - return 'UnwrappedObject'; - } - return 'ParentType'; - } - NamedType(node: NamedTypeNode): string { return `Maybe<${super.NamedType(node)}>`; } @@ -119,7 +109,7 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< protected buildEnumResolverContentBlock(node: EnumTypeDefinitionNode, mappedEnumType: string): string { const valuesMap = `{ ${(node.values || []) - .map(v => `${v.name as any as string}${this.config.avoidOptionals.resolvers ? '' : '?'}: any`) + .map(v => `${v.name.value}${this.config.avoidOptionals.resolvers ? '' : '?'}: any`) .join(', ')} }`; this._globalDeclarations.add(ENUM_RESOLVERS_SIGNATURE); @@ -133,7 +123,7 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< ): string { return `{ ${(node.values || []) .map(v => { - const valueName = v.name as any as string; + const valueName = v.name.value; const mappedValue = valuesMapping[valueName]; return `${valueName}: ${typeof mappedValue === 'number' ? mappedValue : `'${mappedValue}'`}`; diff --git a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap index 77135b282ba..148befbea90 100644 --- a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap +++ b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap @@ -166,6 +166,8 @@ export type DirectiveResolverFn TResult | Promise; + + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -283,7 +285,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -293,19 +294,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ @@ -437,6 +433,8 @@ export type DirectiveResolverFn TResult | Promise; + + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -554,7 +552,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -564,19 +561,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ @@ -794,6 +786,8 @@ export type DirectiveResolverFn TResult | Promise; + + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -911,7 +905,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -921,19 +914,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts index ff7b7730a36..42918d7b710 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts @@ -53,7 +53,6 @@ describe('TypeScript Resolvers Plugin - config.avoidOptionals', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.customDirectives.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.customDirectives.spec.ts index c7ceeb86040..49f221c71ac 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.customDirectives.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.customDirectives.spec.ts @@ -62,7 +62,6 @@ describe('customDirectives.sematicNonNull', () => { nonNullableListWithNonNullableItemLevel0?: Resolver, ParentType, ContextType>; nonNullableListWithNonNullableItemLevel1?: Resolver, ParentType, ContextType>; nonNullableListWithNonNullableItemBothLevels?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts new file mode 100644 index 00000000000..6ac6bdb0cca --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -0,0 +1,213 @@ +import '@graphql-codegen/testing'; +import { generate } from './utils'; + +describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { + it('generates __resolveReference for Interfaces with @key', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + me: Person + } + + interface Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type User implements Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type Admin implements Person @key(fields: "id") { + id: ID! + name: PersonName! + canImpersonate: Boolean! + } + + type PersonName { + first: String! + last: String! + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, + }); + + expect(content).toMatchInlineSnapshot(` + "import { GraphQLResolveInfo } from 'graphql'; + + + export type ResolverTypeWrapper = Promise | T; + + export type ReferenceResolver = ( + reference: TReference, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + type ScalarCheck = S extends true ? T : NullableCheck; + type NullableCheck = Maybe extends T ? Maybe, S>> : ListCheck; + type ListCheck = T extends (infer U)[] ? NullableCheck[] : GraphQLRecursivePick; + export type GraphQLRecursivePick = { [K in keyof T & keyof S]: ScalarCheck }; + + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = ResolverFn | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => AsyncIterable | Promise>; + + export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; + } + + export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; + } + + export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + + export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + + export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo + ) => Maybe | Promise>; + + export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + + export type NextResolverFn = () => Promise; + + export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + /** Mapping of federation types */ + export type FederationTypes = { + Person: Person; + User: User; + Admin: Admin; + }; + + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + Person: + ( { __typename: 'Person' } + & GraphQLRecursivePick ); + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Admin: + ( { __typename: 'Admin' } + & GraphQLRecursivePick ); + }; + + + /** Mapping of interface types */ + export type ResolversInterfaceTypes<_RefType extends Record> = { + Person: + | ( User ) + | ( Admin ) + ; + }; + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>; + Person: ResolverTypeWrapper['Person']>; + ID: ResolverTypeWrapper; + User: ResolverTypeWrapper; + Admin: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + PersonName: ResolverTypeWrapper; + String: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + Query: {}; + Person: ResolversInterfaceTypes['Person']; + ID: Scalars['ID']['output']; + User: User | FederationReferenceTypes['User']; + Admin: Admin | FederationReferenceTypes['Admin']; + Boolean: Scalars['Boolean']['output']; + PersonName: PersonName; + String: Scalars['String']['output']; + }; + + export type QueryResolvers = { + me?: Resolver, ParentType, ContextType>; + }; + + export type PersonResolvers = { + __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type AdminResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver; + canImpersonate?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type PersonNameResolvers = { + first?: Resolver; + last?: Resolver; + }; + + export type Resolvers = { + Query?: QueryResolvers; + Person?: PersonResolvers; + User?: UserResolvers; + Admin?: AdminResolvers; + PersonName?: PersonNameResolvers; + }; + + " + `); + }); +}); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts new file mode 100644 index 00000000000..a6f58d073ac --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -0,0 +1,195 @@ +import '@graphql-codegen/testing'; +import { generate } from './utils'; + +describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { + it('generates FederationTypes and use it for reference type', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + + type UserProfile { + id: ID! + user: User! + } + + type Account @key(fields: "id") { + id: ID! + name: String! @external + displayName: String! @requires(fields: "name") + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + mappers: { + User: './mappers#UserMapper', + Account: './mappers#AccountMapper', + }, + }, + }); + + // User should have it + expect(content).toMatchInlineSnapshot(` + "import { GraphQLResolveInfo } from 'graphql'; + import { UserMapper, AccountMapper } from './mappers'; + export type Omit = Pick>; + + + export type ResolverTypeWrapper = Promise | T; + + export type ReferenceResolver = ( + reference: TReference, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + type ScalarCheck = S extends true ? T : NullableCheck; + type NullableCheck = Maybe extends T ? Maybe, S>> : ListCheck; + type ListCheck = T extends (infer U)[] ? NullableCheck[] : GraphQLRecursivePick; + export type GraphQLRecursivePick = { [K in keyof T & keyof S]: ScalarCheck }; + + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = ResolverFn | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => AsyncIterable | Promise>; + + export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; + } + + export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; + } + + export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + + export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + + export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo + ) => Maybe | Promise>; + + export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + + export type NextResolverFn = () => Promise; + + export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + /** Mapping of federation types */ + export type FederationTypes = { + User: User; + Account: Account; + }; + + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick ) ); + }; + + + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + String: ResolverTypeWrapper; + UserProfile: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + Account: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + Query: {}; + User: UserMapper; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + UserProfile: Omit & { user: ResolversParentTypes['User'] }; + Account: AccountMapper; + Boolean: Scalars['Boolean']['output']; + }; + + export type QueryResolvers = { + me?: Resolver, ParentType, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + }; + + export type UserProfileResolvers = { + id?: Resolver; + user?: Resolver; + }; + + export type AccountResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + displayName?: Resolver; + }; + + export type Resolvers = { + Query?: QueryResolvers; + User?: UserResolvers; + UserProfile?: UserProfileResolvers; + Account?: AccountResolvers; + }; + + " + `); + }); +}); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts index 1cae62b4c31..54301d35578 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -24,7 +24,7 @@ function generate({ schema, config }: { schema: string; config: TypeScriptResolv } describe('TypeScript Resolvers Plugin + Apollo Federation', () => { - describe('adds __resolveReference', () => { + it('generates __resolveReference for object types with resolvable @key', async () => { const federatedSchema = /* GraphQL */ ` type Query { allUsers: [User] @@ -76,207 +76,116 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { } `; - it('when generateInternalResolversIfNeeded.__resolveReference = false, generates optional __resolveReference for object types with @key', async () => { - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - }, - }); - - expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type SingleNonResolvableResolvers = { - __resolveReference?: ReferenceResolver, ParentType, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type MultipleNonResolvableResolvers = { - __resolveReference?: ReferenceResolver, ParentType, ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - // Book does NOT have __resolveReference because it doesn't have @key - expect(content).toBeSimilarStringTo(` - export type BookResolvers = { - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, }); - it('when generateInternalResolversIfNeeded.__resolveReference = true, generates required __resolveReference for object types with resolvable @key', async () => { - const federatedSchema = /* GraphQL */ ` - type Query { - allUsers: [User] - } - - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - - type Book { - id: ID! - } - - type SingleResolvable @key(fields: "id", resolvable: true) { - id: ID! - } - - type SingleNonResolvable @key(fields: "id", resolvable: false) { - id: ID! - } - - type AtLeastOneResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - - type MixedResolvable - @key(fields: "id") - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } + expect(content).toBeSimilarStringTo(` + export type FederationTypes = { + User: User; + SingleResolvable: SingleResolvable; + AtLeastOneResolvable: AtLeastOneResolvable; + MixedResolvable: MixedResolvable; + }; + `); - type MultipleNonResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: false) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - `; + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + SingleResolvable: + ( { __typename: 'SingleResolvable' } + & GraphQLRecursivePick ); + AtLeastOneResolvable: + ( { __typename: 'AtLeastOneResolvable' } + & GraphQLRecursivePick ); + MixedResolvable: + ( { __typename: 'MixedResolvable' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - generateInternalResolversIfNeeded: { __resolveReference: true }, - }, - }); + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + Book: Book; + SingleResolvable: SingleResolvable | FederationReferenceTypes['SingleResolvable']; + SingleNonResolvable: SingleNonResolvable; + AtLeastOneResolvable: AtLeastOneResolvable | FederationReferenceTypes['AtLeastOneResolvable']; + MixedResolvable: MixedResolvable | FederationReferenceTypes['MixedResolvable']; + MultipleNonResolvable: MultipleNonResolvable; + Boolean: Scalars['Boolean']['output']; + }; + `); - // User should have __resolveReference because it has resolvable @key (by default) - expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + // User should have __resolveReference because it has resolvable @key (by default) + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + }; + `); - // SingleResolvable has __resolveReference because it has resolvable: true - expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + // SingleResolvable has __resolveReference because it has resolvable: true + expect(content).toBeSimilarStringTo(` + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + }; + `); - // SingleNonResolvable does NOT have __resolveReference because it has resolvable: false - expect(content).toBeSimilarStringTo(` + // SingleNonResolvable does NOT have __resolveReference because it has resolvable: false + expect(content).toBeSimilarStringTo(` export type SingleNonResolvableResolvers = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); - // AtLeastOneResolvable has __resolveReference because it at least one resolvable - expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + // AtLeastOneResolvable has __resolveReference because it at least one resolvable + expect(content).toBeSimilarStringTo(` + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); - // MixedResolvable has __resolveReference and references for resolvable keys - expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + // MixedResolvable has __resolveReference and references for resolvable keys + expect(content).toBeSimilarStringTo(` + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); - // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable - expect(content).toBeSimilarStringTo(` + // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable + expect(content).toBeSimilarStringTo(` export type MultipleNonResolvableResolvers = { id?: Resolver; id2?: Resolver; id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); - // Book does NOT have __resolveReference because it doesn't have @key - expect(content).toBeSimilarStringTo(` + // Book does NOT have __resolveReference because it doesn't have @key + expect(content).toBeSimilarStringTo(` export type BookResolvers = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); - }); }); it('should support extend keyword', async () => { @@ -303,13 +212,21 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + // User should have it expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; `); // Foo shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, FederationReferenceType, ContextType>; `); }); @@ -345,21 +262,30 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver, ContextType>; - name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; - } + export type FederationReferenceTypes = { + Name: + ( { __typename: 'Name' } + & GraphQLRecursivePick ); + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; `); expect(content).toBeSimilarStringTo(` - export type NameResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'Name' } & GraphQLRecursivePick, ContextType>; - first?: Resolver, ContextType>; - last?: Resolver, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; - } + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type NameResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + first?: Resolver; + last?: Resolver; + }; `); }); @@ -369,11 +295,25 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { users: [User] } + type Account @key(fields: "id") { + id: ID! + key: String! + } + type User @key(fields: "id") { id: ID! - name: String @external - age: Int! @external - username: String @requires(fields: "name age") + + a: String @external + aRequires: String @requires(fields: "a") + + b: String! @external + bRequires: String! @requires(fields: "b") + + c: String! @external + cRequires: String! @requires(fields: "c") + + d: String! @external + dRequires: String! @requires(fields: "d") } `; @@ -384,13 +324,52 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + Account: Account | FederationReferenceTypes['Account']; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + User: User | FederationReferenceTypes['User']; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick ); + User: + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + aRequires?: Resolver, ParentType, ContextType>; + bRequires?: Resolver; + cRequires?: Resolver; + dRequires?: Resolver; }; `); }); @@ -403,6 +382,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { extend type User @key(fields: "id") { id: ID! @external + + favouriteColor: String! @external + favouriteColorHex: String! @requires(fields: "favouriteColor") + name: String @external age: Int! @external address: Address! @external @@ -423,10 +406,34 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + Int: Scalars['Int']['output']; + Address: Address; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + favouriteColorHex?: Resolver; + username?: Resolver, ParentType, ContextType>; }; `); }); @@ -456,28 +463,49 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; + String: Scalars['String']['output']; + Name: Name; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + username?: Resolver, ParentType, ContextType>; }; `); }); - it('should not apply key/requires fields restriction for base federated types', async () => { + it('handles a mix of @key and @requires directives', async () => { const federatedSchema = /* GraphQL */ ` type Query { users: [User] } - type User @key(fields: "name { first last }") { - name: Name! - username: String + type User @key(fields: "id") @key(fields: "uuid") @key(fields: "legacyId { oldId1 oldId2 }") { + id: ID! + uuid: ID! + legacyId: LegacyId! @external + name: String! @external + username: String! @requires(fields: "id name") + usernameLegacy: String! @requires(fields: "legacyId { oldId1 } name") } - type Name { - first: String! - last: String! + type LegacyId { + oldId1: ID! @external + oldId2: ID! @external } `; @@ -489,41 +517,55 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - name?: Resolver; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + LegacyId: LegacyId; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + uuid?: Resolver; + username?: Resolver; + usernameLegacy?: Resolver; }; `); }); - it.skip('should handle interface types', async () => { + it('should not apply key/requires fields restriction for base federated types', async () => { const federatedSchema = /* GraphQL */ ` type Query { - people: [Person] - } - - extend interface Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name") + users: [User] } - extend type User implements Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name { first last }") + type User @key(fields: "name { first last }") { + name: Name! username: String } - type Admin implements Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name { first last }") - permissions: [String!]! - } - - extend type Name { - first: String! @external - last: String! @external + type Name { + first: String! + last: String! } `; @@ -535,14 +577,23 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type PersonResolvers = { - __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - age?: Resolver, { __typename: 'User' | 'Admin' } & GraphQLRecursivePick, ContextType>; - }; + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + name?: Resolver; + username?: Resolver, ParentType, ContextType>; + }; `); }); - it('should skip to generate resolvers of fields with @external directive', async () => { + it('should skip to generate resolvers of fields or object types with @external directive', async () => { const federatedSchema = /* GraphQL */ ` type Query { users: [User] @@ -550,12 +601,38 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { type Book { author: User @provides(fields: "name") + editor: User @provides(fields: "company { taxCode }") } type User @key(fields: "id") { id: ID! name: String @external username: String @external + address: Address + dateOfBirth: DateOfBirth + placeOfBirth: PlaceOfBirth + company: Company + } + + type Address { + street: String! @external + zip: String! + } + + type DateOfBirth { + day: Int! @external + month: Int! @external + year: Int! @external + } + + type PlaceOfBirth @external { + city: String! + country: String! + } + + type Company @external { + name: String! + taxCode: String! } `; @@ -566,15 +643,47 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); - // UserResolver should not have a resolver function of name field expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver, ContextType>; - name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); }; `); + + // `UserResolvers` should not have `username` resolver because it is marked with `@external` + // `UserResolvers` should have `name` resolver because whilst it is marked with `@external`, it is provided by `Book.author` + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + address?: Resolver, ParentType, ContextType>; + dateOfBirth?: Resolver, ParentType, ContextType>; + placeOfBirth?: Resolver, ParentType, ContextType>; + company?: Resolver, ParentType, ContextType>; + }; + `); + + // `AddressResolvers` should only have fields not marked with @external + expect(content).toBeSimilarStringTo(` + export type AddressResolvers = { + zip?: Resolver; + }; + `); + + // `DateOfBirthResolvers` should not be generated because every field is marked with @external + expect(content).not.toBeSimilarStringTo('export type DateOfBirthResolvers'); + + // `PlaceOfBirthResolvers` should not be generated because the type is marked with @external, even if `User.placeOfBirth` is not marked with @external + expect(content).not.toBeSimilarStringTo('export type PlaceOfBirthResolvers'); + + // FIXME: `CompanyResolvers` should only have taxCode resolver because it is part of the `@provides` directive in `Book.editor`, even if the whole `Company` type is marked with @external + // expect(content).toBeSimilarStringTo(` + // export type CompanyResolvers = { + // taxCode?: Resolver; + // }; + // `); }); it('should not include _FieldSet scalar', async () => { @@ -693,13 +802,21 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - name?: Resolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - username?: Resolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - __isTypeOf?: IsTypeOfResolverFn; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; }; `); }); @@ -763,175 +880,68 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).not.toContain('GraphQLScalarType'); }); - describe('When field definition wrapping is enabled', () => { - it('should add the UnwrappedObject type', async () => { + describe('meta', () => { + it('generates federation meta correctly', async () => { const federatedSchema = /* GraphQL */ ` - type User @key(fields: "id") { - id: ID! - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - wrapFieldDefinitions: true, - }, - }); + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - expect(content).toBeSimilarStringTo(`type UnwrappedObject = {`); - }); + type Query { + user: UserPayload! + allUsers: [User] + } - it('should add UnwrappedObject around ParentType for __resloveReference', async () => { - const federatedSchema = /* GraphQL */ ` type User @key(fields: "id") { id: ID! + name: String + username: String } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - wrapFieldDefinitions: true, - }, - }); - - // __resolveReference should be unwrapped - expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, {"id":true}>, ContextType>; - `); - // but ID should not - expect(content).toBeSimilarStringTo(`id?: Resolver`); - }); - }); - describe('meta - generates federation meta correctly', () => { - const federatedSchema = /* GraphQL */ ` - scalar _FieldSet - directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - - type Query { - user: UserPayload! - allUsers: [User] - } - - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - - interface Node { - id: ID! - } - - type UserOk { - id: ID! - } - type UserError { - message: String! - } - union UserPayload = UserOk | UserError - - enum Country { - FR - US - } + interface Node { + id: ID! + } - type NotResolvable @key(fields: "id", resolvable: false) { - id: ID! - } + type UserOk { + id: ID! + } + type UserError { + message: String! + } + union UserPayload = UserOk | UserError - type Resolvable @key(fields: "id", resolvable: true) { - id: ID! - } + enum Country { + FR + US + } - type MultipleResolvable - @key(fields: "id") - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } + type NotResolvable @key(fields: "id", resolvable: false) { + id: ID! + } - type MultipleNonResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: false) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - `; + type Resolvable @key(fields: "id", resolvable: true) { + id: ID! + } - it('when generateInternalResolversIfNeeded.__resolveReference = false', async () => { - const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); + type MultipleResolvable + @key(fields: "id") + @key(fields: "id2", resolvable: true) + @key(fields: "id3", resolvable: false) { + id: ID! + id2: ID! + id3: ID! + } - expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` - Object { - "resolversMap": Object { - "name": "Resolvers", - }, - "userDefined": Object { - "MultipleNonResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "MultipleNonResolvableResolvers", - }, - "MultipleResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "MultipleResolvableResolvers", - }, - "Node": Object { - "name": "NodeResolvers", - }, - "NotResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "NotResolvableResolvers", - }, - "Query": Object { - "name": "QueryResolvers", - }, - "Resolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "ResolvableResolvers", - }, - "User": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "UserResolvers", - }, - "UserError": Object { - "name": "UserErrorResolvers", - }, - "UserOk": Object { - "name": "UserOkResolvers", - }, - "UserPayload": Object { - "name": "UserPayloadResolvers", - }, - }, + type MultipleNonResolvable + @key(fields: "id", resolvable: false) + @key(fields: "id2", resolvable: false) + @key(fields: "id3", resolvable: false) { + id: ID! + id2: ID! + id3: ID! } - `); - }); + `; - it('when generateInternalResolversIfNeeded.__resolveReference = true', async () => { - const result = await plugin( - buildSchema(federatedSchema), - [], - { federation: true, generateInternalResolversIfNeeded: { __resolveReference: true } }, - { outputFile: '' } - ); + const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` Object { diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts index 332216111f6..72c4c41844c 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts @@ -162,7 +162,6 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn; - id?: Resolver; }; `); }); @@ -461,4 +460,48 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => { }; `); }); + + it('generates __isTypeOf for only implementing object types', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Node { + id: ID! + } + type Cat implements Node { + id: ID! + name: String! + } + type Dog implements Node { + id: ID! + isGoodBoy: Boolean! + } + type Human { + _id: ID! + } + `); + + const result = await plugin(schema, [], {}, { outputFile: '' }); + + expect(result.content).toBeSimilarStringTo(` + export type CatResolvers = { + id?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + } + `); + + expect(result.content).toBeSimilarStringTo(` + export type DogResolvers = { + id?: Resolver; + isGoodBoy?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // Human does not implement Node, so it does not have __isTypeOf + expect(result.content).toBeSimilarStringTo(` + export type HumanResolvers = { + _id?: Resolver; + }; + `); + }); }); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts index c595ebe7041..17e67670276 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts @@ -1265,7 +1265,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1348,7 +1347,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1429,7 +1427,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1499,7 +1496,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1569,7 +1565,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts index a1d13fe829e..8028e1522af 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts @@ -85,19 +85,6 @@ describe('TypeScript Resolvers Plugin', () => { }); describe('Config', () => { - it('onlyResolveTypeForInterfaces - should allow to have only resolveType for interfaces', async () => { - const config = { - onlyResolveTypeForInterfaces: true, - }; - const result = await plugin(resolversTestingSchema, [], config, { outputFile: '' }); - const content = await resolversTestingValidate(result, config, resolversTestingSchema); - - expect(content).toBeSimilarStringTo(` - export type NodeResolvers = { - __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - };`); - }); - it('optionalInfoArgument - should allow to have optional info argument', async () => { const config = { noSchemaStitching: true, @@ -655,7 +642,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType?: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); }); @@ -705,7 +691,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -778,7 +763,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -869,7 +853,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -944,7 +927,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1018,7 +1000,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1093,7 +1074,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -2360,7 +2340,6 @@ export type ResolverFn = ( expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts index 72580745c32..7dd3d3db299 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.union.spec.ts @@ -236,4 +236,56 @@ describe('TypeScript Resolvers Plugin - Union', () => { }; `); }); + + it('generates __isTypeOf for only union members', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MemberOne { + id: ID! + } + type MemberTwo { + id: ID! + name: String! + } + type MemberThree { + id: ID! + isMember: Boolean! + } + union Union = MemberOne | MemberTwo | MemberThree + type Normal { + id: ID! + } + `); + + const result = await plugin(schema, [], {}, { outputFile: '' }); + + expect(result.content).toBeSimilarStringTo(` + export type MemberOneResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + } + `); + + expect(result.content).toBeSimilarStringTo(` + export type MemberTwoResolvers = { + id?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(result.content).toBeSimilarStringTo(` + export type MemberThreeResolvers = { + id?: Resolver; + isMember?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // Normal type is not a union member, so it does not have __isTypeOf + expect(result.content).toBeSimilarStringTo(` + export type NormalResolvers = { + id?: Resolver; + }; + `); + }); }); diff --git a/packages/plugins/typescript/resolvers/tests/utils.ts b/packages/plugins/typescript/resolvers/tests/utils.ts new file mode 100644 index 00000000000..20f77f1ac05 --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/utils.ts @@ -0,0 +1,15 @@ +import { codegen } from '@graphql-codegen/core'; +import { parse } from 'graphql'; +import { TypeScriptResolversPluginConfig } from '../src/config.js'; +import { plugin } from '../src/index.js'; + +export function generate({ schema, config }: { schema: string; config: TypeScriptResolversPluginConfig }) { + return codegen({ + filename: 'graphql.ts', + schema: parse(schema), + documents: [], + plugins: [{ 'typescript-resolvers': {} }], + config, + pluginMap: { 'typescript-resolvers': { plugin } }, + }); +} diff --git a/packages/presets/client/tests/client-preset.nullability.spec.ts b/packages/presets/client/tests/client-preset.nullability.spec.ts index 26ddb962b63..ad1f9834dce 100644 --- a/packages/presets/client/tests/client-preset.nullability.spec.ts +++ b/packages/presets/client/tests/client-preset.nullability.spec.ts @@ -63,7 +63,7 @@ const document = /* GraphQL */ ` describe('client-preset - nullability', () => { it('converts semanticNonNull to non-null when nullability.errorHandlingClient=true', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema, documents: [document], generates: { @@ -112,7 +112,7 @@ describe('client-preset - nullability', () => { }); it('leave semanticNonNull as null when nullability.errorHandlingClient=false', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema, documents: [document], generates: { diff --git a/packages/presets/client/tests/client-preset.spec.ts b/packages/presets/client/tests/client-preset.spec.ts index b68d8d17939..64db5f96bb7 100644 --- a/packages/presets/client/tests/client-preset.spec.ts +++ b/packages/presets/client/tests/client-preset.spec.ts @@ -9,7 +9,7 @@ import { addTypenameSelectionDocumentTransform, preset } from '../src/index.js'; describe('client-preset', () => { it('can generate simple examples uppercase names', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -102,7 +102,7 @@ export * from "./gql";`); }); it('can generate simple examples lowercase names', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -195,7 +195,7 @@ export * from "./gql";`); }); it('generates \\n regardless of whether the source contains LF or CRLF', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -276,7 +276,7 @@ export * from "./gql";`); }); it("follows 'useTypeImports': true", async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -412,7 +412,7 @@ export * from "./gql";`); }); it("follows 'nonOptionalTypename': true", async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -543,7 +543,7 @@ export * from "./gql";`); }); it('supports Apollo fragment masking', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: /* GraphQL */ ` type Query { me: User @@ -632,7 +632,7 @@ export * from "./gql";`); }); it('prevent duplicate operations', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -740,7 +740,7 @@ export * from "./gql";`); describe('fragment masking', () => { it('fragmentMasking: false', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -833,7 +833,7 @@ export * from "./gql";`); }); it('fragmentMasking: {}', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -858,7 +858,7 @@ export * from "./gql";`); }); it('fragmentMasking.unmaskFunctionName', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -977,7 +977,7 @@ export * from "./gql";`); it('can accept null in useFragment', async () => { const docPath = path.join(__dirname, 'fixtures/with-fragment.ts'); - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1016,7 +1016,7 @@ export * from "./gql";`); it('can accept list in useFragment', async () => { const docPath = path.join(__dirname, 'fixtures/with-fragment.ts'); - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1056,7 +1056,7 @@ export * from "./gql";`); it('useFragment preserves ReadonlyArray type', async () => { const docPath = path.join(__dirname, 'fixtures/with-fragment.ts'); - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1096,7 +1096,7 @@ export * from "./gql";`); }); it('generates correct named imports for ESM', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1196,7 +1196,7 @@ export * from "./gql.js";`); }; const docPath = path.join(__dirname, 'fixtures/reused-fragment.ts'); - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1256,7 +1256,7 @@ export * from "./gql.js";`); }; const docPath = path.join(__dirname, 'fixtures/reused-fragment.ts'); - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1312,7 +1312,7 @@ export * from "./gql.js";`); describe('when no operations are found', () => { it('still generates the helper `graphql()` (or under another `presetConfig.gqlTagName` name) function', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1371,7 +1371,7 @@ export * from "./gql.js";`); }); it('embed metadata in executable document node', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1452,7 +1452,7 @@ export * from "./gql.js";`); describe('persisted operations', () => { it('apply default settings', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1531,7 +1531,7 @@ export * from "./gql.js";`); }); it('mode="replaceDocumentWithHash"', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1612,7 +1612,7 @@ export * from "./gql.js";`); }); it('hashPropertyName="custom_property_name"', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1693,7 +1693,7 @@ export * from "./gql.js";`); }); it('embed metadata in executable document node', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1774,7 +1774,7 @@ export * from "./gql.js";`); }); it('hashAlgorithm="sha256"', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1857,7 +1857,7 @@ export * from "./gql.js";`); // This test serves to demonstrate that the custom hash function can perform arbitrary logic // Removing whitespace has no real-world application but clearly shows the custom hash function is being used it('custom hash remove whitespace', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -1941,7 +1941,7 @@ export * from "./gql.js";`); // Tests that the custom hash function can replicate the logic and behavior by re-implementing the existing hash function (for sha256) it('custom hash sha256', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2027,7 +2027,7 @@ export * from "./gql.js";`); // Custom hash example used in `preset-client.mdx` docs it('custom hash docs sha512', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2113,7 +2113,7 @@ export * from "./gql.js";`); }); it('correctly handle fragment references', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: /* GraphQL */ ` type Query { a: A! @@ -2215,7 +2215,7 @@ export * from "./gql.js";`); describe('handles @defer directive', () => { it('generates correct types and metadata', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2297,7 +2297,7 @@ export * from "./gql.js";`); }); it('works with persisted documents', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2382,7 +2382,7 @@ export * from "./gql.js";`); }); it('works with documentMode: string', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2524,7 +2524,7 @@ export * from "./gql.js";`); }); it('works with documentMode: string and persisted documents', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2671,7 +2671,7 @@ export * from "./gql.js";`); describe('documentMode: "string"', () => { it('generates correct types', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2789,7 +2789,7 @@ export * from "./gql.js";`); }); it('graphql overloads have a nice result type', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -2864,7 +2864,7 @@ export * from "./gql.js";`); }); it('correctly resolves nested fragments', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` scalar Date @@ -2953,7 +2953,7 @@ export * from "./gql.js";`); }); it('correctly skips the typename addition for the root node for subscriptions', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` schema { @@ -3076,7 +3076,7 @@ export * from "./gql.js";`); }); it('support enumsAsConst option', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` type Query { @@ -3148,7 +3148,7 @@ export * from "./gql.js";`); }); it('support enumValues option', async () => { - const result = await executeCodegen({ + const { result } = await executeCodegen({ schema: [ /* GraphQL */ ` enum Color { diff --git a/packages/presets/graphql-modules/tests/integration.spec.ts b/packages/presets/graphql-modules/tests/integration.spec.ts index 8a9c13d3dc4..2f967c9cca8 100644 --- a/packages/presets/graphql-modules/tests/integration.spec.ts +++ b/packages/presets/graphql-modules/tests/integration.spec.ts @@ -33,27 +33,27 @@ describe('Integration', () => { // In this test, we make sure executeCodegen passes on a list of Sources as an extension // This is very important test('should generate a base output and 4 for modules', async () => { - const output = await executeCodegen(options); - - expect(output.length).toBe(5); - expect(normalize(output[0].filename)).toMatch(normalize(`/modules/global-types.ts`)); - expect(normalize(output[1].filename)).toMatch(normalize(`/modules/blog/module-types.ts`)); - expect(normalize(output[2].filename)).toMatch(normalize(`/modules/common/module-types.ts`)); - expect(normalize(output[3].filename)).toMatch(normalize(`/modules/dotanions/module-types.ts`)); - expect(normalize(output[4].filename)).toMatch(normalize(`/modules/users/module-types.ts`)); + const { result } = await executeCodegen(options); + + expect(result.length).toBe(5); + expect(normalize(result[0].filename)).toMatch(normalize(`/modules/global-types.ts`)); + expect(normalize(result[1].filename)).toMatch(normalize(`/modules/blog/module-types.ts`)); + expect(normalize(result[2].filename)).toMatch(normalize(`/modules/common/module-types.ts`)); + expect(normalize(result[3].filename)).toMatch(normalize(`/modules/dotanions/module-types.ts`)); + expect(normalize(result[4].filename)).toMatch(normalize(`/modules/users/module-types.ts`)); }); test('should not duplicate type even if type and extend type are in the same module', async () => { - const output = await executeCodegen(options); + const { result } = await executeCodegen(options); const userResolversStr = `export type UserResolvers = Pick;`; - const nbOfTimeUserResolverFound = output[4].content.split(userResolversStr).length - 1; + const nbOfTimeUserResolverFound = result[4].content.split(userResolversStr).length - 1; expect(nbOfTimeUserResolverFound).toBe(1); }); test('should allow to override importBaseTypesFrom correctly', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ generates: { './tests/test-files/modules': { schema: './tests/test-files/modules/*/types/*.graphql', @@ -70,15 +70,15 @@ describe('Integration', () => { }); const importStatement = `import * as Types from "@types";`; - expect(output.length).toBe(5); - expect(output[1].content).toMatch(importStatement); - expect(output[2].content).toMatch(importStatement); - expect(output[3].content).toMatch(importStatement); - expect(output[4].content).toMatch(importStatement); + expect(result.length).toBe(5); + expect(result[1].content).toMatch(importStatement); + expect(result[2].content).toMatch(importStatement); + expect(result[3].content).toMatch(importStatement); + expect(result[4].content).toMatch(importStatement); }); test('should import with respect of useTypeImports config correctly', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ generates: { './tests/test-files/modules': { schema: './tests/test-files/modules/*/types/*.graphql', @@ -99,15 +99,15 @@ describe('Integration', () => { const importStatement = `import type * as Types from "@types";`; - expect(output.length).toBe(5); - expect(output[1].content).toMatch(importStatement); - expect(output[2].content).toMatch(importStatement); - expect(output[3].content).toMatch(importStatement); - expect(output[4].content).toMatch(importStatement); + expect(result.length).toBe(5); + expect(result[1].content).toMatch(importStatement); + expect(result[2].content).toMatch(importStatement); + expect(result[3].content).toMatch(importStatement); + expect(result[4].content).toMatch(importStatement); }); test('should allow to disable graphql-modules', async () => { - const output = await executeCodegen({ + const { result } = await executeCodegen({ generates: { './tests/test-files/modules': { schema: './tests/test-files/modules/*/types/*.graphql', @@ -124,45 +124,45 @@ describe('Integration', () => { }, }); - for (const record of output) { + for (const record of result) { expect(record).not.toContain(`graphql-modules`); expect(record).not.toContain(`gm.`); } }); test('each module-types should include a relative import to glob-types module', async () => { - const output = await executeCodegen(options); + const { result } = await executeCodegen(options); const importStatement = `import * as Types from "../global-types";`; - expect(output.length).toBe(5); - expect(output[1].content).toMatch(importStatement); - expect(output[2].content).toMatch(importStatement); - expect(output[3].content).toMatch(importStatement); - expect(output[4].content).toMatch(importStatement); + expect(result.length).toBe(5); + expect(result[1].content).toMatch(importStatement); + expect(result[2].content).toMatch(importStatement); + expect(result[3].content).toMatch(importStatement); + expect(result[4].content).toMatch(importStatement); }); test('each module-types should export Resolvers', async () => { - const output = await executeCodegen(options); + const { result } = await executeCodegen(options); const exportStatemment = `export interface Resolvers `; - expect(output.length).toBe(5); - expect(output[1].content).toMatch(exportStatemment); - expect(output[2].content).toMatch(exportStatemment); - expect(output[3].content).toMatch(exportStatemment); - expect(output[4].content).toMatch(exportStatemment); + expect(result.length).toBe(5); + expect(result[1].content).toMatch(exportStatemment); + expect(result[2].content).toMatch(exportStatemment); + expect(result[3].content).toMatch(exportStatemment); + expect(result[4].content).toMatch(exportStatemment); }); test('dotanions module should export DefinedFields, Schema Types with Picks and resolvers', async () => { - const output = await executeCodegen(options); + const { result } = await executeCodegen(options); - expect(output.length).toBe(5); - expect(output[3].content).toMatchSnapshot(); + expect(result.length).toBe(5); + expect(result[3].content).toMatchSnapshot(); }); test('should NOT produce required root-level resolvers in Resolvers interface by default', async () => { - const output = await executeCodegen(options); + const { result } = await executeCodegen(options); - const usersModuleOutput = output.find(o => o.filename.includes('users'))!; + const usersModuleOutput = result.find(o => o.filename.includes('users'))!; expect(usersModuleOutput).toBeDefined(); expect(usersModuleOutput.content).toContain( @@ -180,9 +180,9 @@ describe('Integration', () => { useGraphQLModules: false, }; - const output = await executeCodegen(optionsCopy); + const { result } = await executeCodegen(optionsCopy); - const usersModuleOutput = output.find(o => o.filename.includes('users'))!; + const usersModuleOutput = result.find(o => o.filename.includes('users'))!; expect(usersModuleOutput).toBeDefined(); @@ -204,13 +204,13 @@ describe('Integration', () => { ...options, emitLegacyCommonJSImports: false, }; - const output = await executeCodegen(emitLegacyCommonJSImports); + const { result } = await executeCodegen(emitLegacyCommonJSImports); const esmImportStatement = `import * as Types from "../global-types.js";`; - expect(output.length).toBe(5); - expect(output[1].content).toMatch(esmImportStatement); - expect(output[2].content).toMatch(esmImportStatement); - expect(output[3].content).toMatch(esmImportStatement); - expect(output[4].content).toMatch(esmImportStatement); + expect(result.length).toBe(5); + expect(result[1].content).toMatch(esmImportStatement); + expect(result[2].content).toMatch(esmImportStatement); + expect(result[3].content).toMatch(esmImportStatement); + expect(result[4].content).toMatch(esmImportStatement); }); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index d77277629e7..1038b514ced 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,18 +1,20 @@ -import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { astFromInterfaceType, astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; +import type { FieldDefinitionResult } from '@graphql-codegen/visitor-plugin-common'; import { DefinitionNode, DirectiveNode, FieldDefinitionNode, - GraphQLNamedType, + GraphQLFieldConfigMap, + GraphQLInterfaceType, GraphQLObjectType, GraphQLSchema, + InterfaceTypeDefinitionNode, isObjectType, ObjectTypeDefinitionNode, OperationDefinitionNode, parse, StringValueNode, } from 'graphql'; -import merge from 'lodash/merge.js'; import { oldVisit } from './index.js'; import { getBaseType } from './utils.js'; @@ -29,14 +31,262 @@ export const federationSpec = parse(/* GraphQL */ ` `); /** - * Adds `__resolveReference` in each ObjectType involved in Federation. + * ReferenceSelectionSet + * @description Each is a collection of fields that are available in a reference payload (originated from the Router) + * @example + * - resolvable fields marked with `@key` + * - fields declared in `@provides` + * - fields declared in `@requires` + */ +interface DirectiveSelectionSet { + name: string; + selection: boolean | DirectiveSelectionSet[]; +} + +type ReferenceSelectionSet = Record; // TODO: handle nested + +interface TypeMeta { + hasResolveReference: boolean; + resolvableKeyDirectives: readonly DirectiveNode[]; + /** + * referenceSelectionSets + * @description Each element can be `ReferenceSelectionSet[]`. + * Elements at the root level are combined with `&` and nested elements are combined with `|`. + * + * @example: + * - [[A, B], [C], [D]] -> (A | B) & C & D + * - [[A, B], [C, D], [E]] -> (A | B) & (C | D) & E + */ + referenceSelectionSets: { directive: '@key' | '@requires'; selectionSets: ReferenceSelectionSet[] }[]; + referenceSelectionSetsString: string; +} + +export type FederationMeta = { [typeName: string]: TypeMeta }; + +/** + * Adds `__resolveReference` in each ObjectType and InterfaceType involved in Federation. + * We do this to utilise the existing FieldDefinition logic of the plugin, which includes many logic: + * - mapper + * - return type * @param schema */ -export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema, { +export function addFederationReferencesToSchema(schema: GraphQLSchema): { + transformedSchema: GraphQLSchema; + federationMeta: FederationMeta; +} { + const setFederationMeta = ({ + meta, + typeName, + update, + }: { + meta: FederationMeta; + typeName: string; + update: TypeMeta; + }): void => { + meta[typeName] = { + ...(meta[typeName] || + ({ + hasResolveReference: false, + resolvableKeyDirectives: [], + referenceSelectionSets: [], + referenceSelectionSetsString: '', + } satisfies TypeMeta)), + ...update, + }; + }; + + const getReferenceSelectionSets = ({ + resolvableKeyDirectives, + fields, + }: { + resolvableKeyDirectives: readonly DirectiveNode[]; + fields: GraphQLFieldConfigMap; + }): TypeMeta['referenceSelectionSets'] => { + const referenceSelectionSets: TypeMeta['referenceSelectionSets'] = []; + + // @key() @key() - "primary keys" in Federation + // A reference may receive one primary key combination at a time, so they will be combined with `|` + const primaryKeys = resolvableKeyDirectives.map(extractReferenceSelectionSet); + referenceSelectionSets.push({ directive: '@key', selectionSets: [...primaryKeys] }); + + const requiresPossibleTypes: ReferenceSelectionSet[] = []; + for (const fieldNode of Object.values(fields)) { + // Look for @requires and see what the service needs and gets + const directives = getDirectivesByName('requires', fieldNode.astNode); + for (const directive of directives) { + const requires = extractReferenceSelectionSet(directive); + requiresPossibleTypes.push(requires); + } + } + referenceSelectionSets.push({ directive: '@requires', selectionSets: requiresPossibleTypes }); + + return referenceSelectionSets; + }; + + /** + * Function to find all combinations of selection sets and push them into the `result` + * This is used for `@requires` directive because depending on the operation selection set, different + * combination of fields are sent from the router. + * + * @example + * Input: [ + * { a: true }, + * { b: true }, + * { c: true }, + * { d: true}, + * ] + * Output: [ + * { a: true }, + * { a: true, b: true }, + * { a: true, c: true }, + * { a: true, d: true }, + * { a: true, b: true, c: true }, + * { a: true, b: true, d: true }, + * { a: true, c: true, d: true }, + * { a: true, b: true, c: true, d: true } + * + * { b: true }, + * { b: true, c: true }, + * { b: true, d: true }, + * { b: true, c: true, d: true } + * + * { c: true }, + * { c: true, d: true }, + * + * { d: true }, + * ] + * ``` + */ + function findAllSelectionSetCombinations( + selectionSets: ReferenceSelectionSet[], + result: ReferenceSelectionSet[] + ): void { + if (selectionSets.length === 0) { + return; + } + + for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { + const base = selectionSets.slice(0, baseIndex + 1); + const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); + + const currentSelectionSet = base.reduce((acc, selectionSet) => { + acc = { ...acc, ...selectionSet }; + return acc; + }, {}); + + if (baseIndex === 0) { + result.push(currentSelectionSet); + } + + for (const selectionSet of rest) { + result.push({ ...currentSelectionSet, ...selectionSet }); + } + } + + const next = selectionSets.slice(1, selectionSets.length); + + if (next.length > 0) { + findAllSelectionSetCombinations(next, result); + } + } + + const printReferenceSelectionSets = ({ + typeName, + baseFederationType, + referenceSelectionSets, + }: { + typeName: string; + baseFederationType: string; + referenceSelectionSets: TypeMeta['referenceSelectionSets']; + }): string => { + const referenceSelectionSetStrings = referenceSelectionSets.reduce( + (acc, { directive, selectionSets: originalSelectionSets }) => { + const result: string[] = []; + + let selectionSets = originalSelectionSets; + if (directive === '@requires') { + selectionSets = []; + findAllSelectionSetCombinations(originalSelectionSets, selectionSets); + if (selectionSets.length > 0) { + result.push('{}'); + } + } + + for (const referenceSelectionSet of selectionSets) { + result.push(`GraphQLRecursivePick<${baseFederationType}, ${JSON.stringify(referenceSelectionSet)}>`); + } + + if (result.length === 0) { + return acc; + } + + if (result.length === 1) { + acc.push(result.join(' | ')); + return acc; + } + + acc.push(`( ${result.join('\n | ')} )`); + return acc; + }, + [] + ); + + return `\n ( { __typename: '${typeName}' }\n & ${referenceSelectionSetStrings.join('\n & ')} )`; + }; + + const federationMeta: FederationMeta = {}; + + const transformedSchema = mapSchema(schema, { + [MapperKind.INTERFACE_TYPE]: type => { + const node = astFromInterfaceType(type, schema); + const federationDetails = checkTypeFederationDetails(node, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { + const typeConfig = type.toConfig(); + typeConfig.fields = { + [resolveReferenceFieldName]: { + type, + }, + ...typeConfig.fields, + }; + + const referenceSelectionSets = getReferenceSelectionSets({ + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + fields: typeConfig.fields, + }); + + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + referenceSelectionSets, + referenceSelectionSetsString, + }, + }); + + return new GraphQLInterfaceType(typeConfig); + } + + return type; + }, [MapperKind.OBJECT_TYPE]: type => { - if (checkObjectTypeFederationDetails(type, schema)) { + const node = astFromObjectType(type, schema); + const federationDetails = checkTypeFederationDetails(node, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { const typeConfig = type.toConfig(); + + const referenceSelectionSets = getReferenceSelectionSets({ + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + fields: typeConfig.fields, + }); + typeConfig.fields = { [resolveReferenceFieldName]: { type, @@ -44,11 +294,33 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLS ...typeConfig.fields, }; + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + referenceSelectionSets, + referenceSelectionSetsString, + }, + }); + return new GraphQLObjectType(typeConfig); } return type; }, }); + + return { + transformedSchema, + federationMeta, + }; } /** @@ -82,20 +354,23 @@ export function removeFederation(schema: GraphQLSchema): GraphQLSchema { const resolveReferenceFieldName = '__resolveReference'; -interface TypeMeta { - hasResolveReference: boolean; -} - export class ApolloFederation { private enabled = false; private schema: GraphQLSchema; private providesMap: Record; - protected meta: { [typename: string]: TypeMeta } = {}; + /** + * `fieldsToGenerate` is a meta object where the keys are object type names + * and the values are fields that must be generated for that object. + */ + private fieldsToGenerate: Record; + protected meta: FederationMeta = {}; - constructor({ enabled, schema }: { enabled: boolean; schema: GraphQLSchema }) { + constructor({ enabled, schema, meta }: { enabled: boolean; schema: GraphQLSchema; meta: FederationMeta }) { this.enabled = enabled; this.schema = schema; this.providesMap = this.createMapOfProvides(); + this.fieldsToGenerate = {}; + this.meta = meta; } /** @@ -131,138 +406,106 @@ export class ApolloFederation { } /** - * Decides if field should not be generated - * @param data + * findFieldNodesToGenerate + * @description Function to find field nodes to generate. + * In a normal setup, all fields must be generated. + * However, in a Federatin setup, a field should not be generated if: + * - The field is marked as `@external` and there is no `@provides` path to the field + * - The parent object is marked as `@external` and there is no `@provides` path to the field */ - skipField({ fieldNode, parentType }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType }): boolean { - if (!this.enabled || !isObjectType(parentType) || !checkObjectTypeFederationDetails(parentType, this.schema)) { - return false; + findFieldNodesToGenerate({ + node, + }: { + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; + }): readonly FieldDefinitionNode[] { + const nodeName = node.name.value; + if (this.fieldsToGenerate[nodeName]) { + return this.fieldsToGenerate[nodeName]; } - return this.isExternalAndNotProvided(fieldNode, parentType); - } + const fieldNodes = ((node.fields || []) as unknown as FieldDefinitionResult[]).map(field => field.node); - isResolveReferenceField(fieldNode: FieldDefinitionNode): boolean { - const name = typeof fieldNode.name === 'string' ? fieldNode.name : fieldNode.name.value; - return this.enabled && name === resolveReferenceFieldName; - } + if (!this.enabled) { + return fieldNodes; + } - /** - * Transforms ParentType signature in ObjectTypes involved in Federation - * @param data - */ - transformParentType({ - fieldNode, - parentType, - parentTypeSignature, - }: { - fieldNode: FieldDefinitionNode; - parentType: GraphQLNamedType; - parentTypeSignature: string; - }) { - if ( - this.enabled && - isObjectType(parentType) && - (isTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName) - ) { - const objectTypeFederationDetails = checkObjectTypeFederationDetails(parentType, this.schema); - if (!objectTypeFederationDetails) { - return parentTypeSignature; - } + // If the object is marked with `@external`, fields to generate are those with `@provides` + if (this.isExternal(node)) { + const fieldNodesWithProvides = fieldNodes.reduce((acc, fieldNode) => { + if (this.hasProvides(node, fieldNode.name.value)) { + acc.push(fieldNode); + return acc; + } + return acc; + }, []); - const { resolvableKeyDirectives } = objectTypeFederationDetails; + this.fieldsToGenerate[nodeName] = fieldNodesWithProvides; - if (resolvableKeyDirectives.length) { - const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; + return fieldNodesWithProvides; + } - // Look for @requires and see what the service needs and gets - const requires = getDirectivesByName('requires', fieldNode).map(this.extractFieldSet); - const requiredFields = this.translateFieldSet(merge({}, ...requires), parentTypeSignature); + // If the object is not marked with `@external`, fields to generate are: + // - the fields without `@external` + // - the `@external` fields with `@provides` + const fieldNodesWithoutExternalOrHasProvides = fieldNodes.reduce((acc, fieldNode) => { + if (!this.isExternal(fieldNode)) { + acc.push(fieldNode); + return acc; + } - // @key() @key() - "primary keys" in Federation - const primaryKeys = resolvableKeyDirectives.map(def => { - const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, parentTypeSignature); - }); + if (this.isExternal(fieldNode) && this.hasProvides(node, fieldNode.name.value)) { + acc.push(fieldNode); + return acc; + } - const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; + return acc; + }, []); - outputs.push([open, primaryKeys.join(' | '), close].join('')); + this.fieldsToGenerate[nodeName] = fieldNodesWithoutExternalOrHasProvides; - // include required fields - if (requires.length) { - outputs.push(`& ${requiredFields}`); - } + return fieldNodesWithoutExternalOrHasProvides; + } - return outputs.join(' '); - } + isResolveReferenceField(fieldNode: FieldDefinitionNode): boolean { + const name = typeof fieldNode.name === 'string' ? fieldNode.name : fieldNode.name.value; + return this.enabled && name === resolveReferenceFieldName; + } + + addFederationTypeGenericIfApplicable({ + genericTypes, + typeName, + federationTypesType, + }: { + genericTypes: string[]; + typeName: string; + federationTypesType: string; + }): void { + if (!this.getMeta()[typeName]) { + return; } - return parentTypeSignature; + const typeRef = `${federationTypesType}['${typeName}']`; + genericTypes.push(`FederationReferenceType extends ${typeRef} = ${typeRef}`); } - setMeta(typename: string, update: Partial): void { - this.meta[typename] = { ...(this.meta[typename] || { hasResolveReference: false }), ...update }; - } getMeta() { return this.meta; } - private isExternalAndNotProvided(fieldNode: FieldDefinitionNode, objectType: GraphQLObjectType): boolean { - return this.isExternal(fieldNode) && !this.hasProvides(objectType, fieldNode); - } - - private isExternal(node: FieldDefinitionNode): boolean { + private isExternal(node: FieldDefinitionNode | ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode): boolean { return getDirectivesByName('external', node).length > 0; } - private hasProvides(objectType: ObjectTypeDefinitionNode | GraphQLObjectType, node: FieldDefinitionNode): boolean { - const fields = this.providesMap[isObjectType(objectType) ? objectType.name : objectType.name.value]; + private hasProvides(node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, fieldName: string): boolean { + const fields = this.providesMap[node.name.value]; if (fields?.length) { - return fields.includes(node.name.value); + return fields.includes(fieldName); } return false; } - private translateFieldSet(fields: any, parentTypeRef: string): string { - return `GraphQLRecursivePick<${parentTypeRef}, ${JSON.stringify(fields)}>`; - } - - private extractFieldSet(directive: DirectiveNode): any { - const arg = directive.arguments.find(arg => arg.name.value === 'fields'); - const { value } = arg.value as StringValueNode; - - type SelectionSetField = { - name: string; - selection: boolean | SelectionSetField[]; - }; - - return oldVisit(parse(`{${value}}`), { - leave: { - SelectionSet(node) { - return (node.selections as any as SelectionSetField[]).reduce((accum, field) => { - accum[field.name] = field.selection; - return accum; - }, {}); - }, - Field(node) { - return { - name: node.name.value, - selection: node.selectionSet || true, - } as SelectionSetField; - }, - Document(node) { - return node.definitions.find( - (def: DefinitionNode): def is OperationDefinitionNode => - def.kind === 'OperationDefinition' && def.operation === 'query' - ).selectionSet; - }, - }, - }); - } - private createMapOfProvides() { const providesMap: Record = {}; @@ -272,8 +515,8 @@ export class ApolloFederation { if (isObjectType(objectType)) { for (const field of Object.values(objectType.getFields())) { const provides = getDirectivesByName('provides', field.astNode) - .map(this.extractFieldSet) - .reduce((prev, curr) => [...prev, ...Object.keys(curr)], []); + .map(extractReferenceSelectionSet) + .reduce((prev, curr) => [...prev, ...Object.keys(curr)], []); // FIXME: this is not taking into account nested selection sets e.g. `company { taxCode }` const ofType = getBaseType(field.type); providesMap[ofType.name] ||= []; @@ -291,14 +534,12 @@ export class ApolloFederation { * Checks if Object Type is involved in Federation. Based on `@key` directive * @param node Type */ -export function checkObjectTypeFederationDetails( - node: ObjectTypeDefinitionNode | GraphQLObjectType, +function checkTypeFederationDetails( + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, schema: GraphQLSchema ): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { - const { - name: { value: name }, - directives, - } = isObjectType(node) ? astFromObjectType(node, schema) : node; + const name = node.name.value; + const directives = node.directives; const rootTypeNames = getRootTypeNames(schema); const isNotRoot = !rootTypeNames.has(name); @@ -330,25 +571,35 @@ export function checkObjectTypeFederationDetails( */ function getDirectivesByName( name: string, - node: ObjectTypeDefinitionNode | GraphQLObjectType | FieldDefinitionNode + node: ObjectTypeDefinitionNode | FieldDefinitionNode | InterfaceTypeDefinitionNode ): readonly DirectiveNode[] { - let astNode: ObjectTypeDefinitionNode | FieldDefinitionNode; - - if (isObjectType(node)) { - astNode = node.astNode; - } else { - astNode = node; - } - - return astNode?.directives?.filter(d => d.name.value === name) || []; + return node?.directives?.filter(d => d.name.value === name) || []; } -/** - * Checks if the Object Type extends a federated type from a remote schema. - * Based on if any of its fields contain the `@external` directive - * @param node Type - */ -function isTypeExtension(node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema): boolean { - const definition = isObjectType(node) ? node.astNode || astFromObjectType(node, schema) : node; - return definition.fields?.some(field => getDirectivesByName('external', field).length); +function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelectionSet { + const arg = directive.arguments.find(arg => arg.name.value === 'fields'); + const { value } = arg.value as StringValueNode; + + return oldVisit(parse(`{${value}}`), { + leave: { + SelectionSet(node) { + return (node.selections as any as DirectiveSelectionSet[]).reduce((accum, field) => { + accum[field.name] = field.selection; + return accum; + }, {}); + }, + Field(node) { + return { + name: node.name.value, + selection: node.selectionSet || true, + } as DirectiveSelectionSet; + }, + Document(node) { + return node.definitions.find( + (def: DefinitionNode): def is OperationDefinitionNode => + def.kind === 'OperationDefinition' && def.operation === 'query' + ).selectionSet; + }, + }, + }); } diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index 4b993aefc95..66d5821de96 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -552,6 +552,10 @@ export namespace Types { * @description Alows to raise errors if any matched files are not valid GraphQL. Default: false. */ noSilentErrors?: boolean; + /** + * @description If `true`, write to files whichever `generates` block succeeds. If `false`, one failed `generates` means no output is written to files. Default: false + */ + allowPartialOutputs?: boolean; } export type ComplexPluginOutput> = { diff --git a/tsconfig.json b/tsconfig.json index 6379f114723..0a3e1fc5c5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,8 @@ "importHelpers": true, "experimentalDecorators": true, "module": "esnext", - "target": "es2021", - "lib": ["es6", "esnext", "es2021", "dom"], + "target": "es2022", + "lib": ["es6", "esnext", "es2023", "dom"], "moduleResolution": "node", "emitDecoratorMetadata": true, "sourceMap": true, diff --git a/website/public/config.schema.json b/website/public/config.schema.json index 9478ddf73f2..96165a25eb1 100644 --- a/website/public/config.schema.json +++ b/website/public/config.schema.json @@ -15,8 +15,14 @@ "description": "A path to a file which defines custom Node.JS require() handlers for custom file extensions.\nThis is essential if the code generator has to go through files which require other files in an unsupported format (by default).\n\nFor more details: https://graphql-code-generator.com/docs/config-reference/require-field\nSee more information about require.extensions: https://gist.github.com/jamestalmage/df922691475cff66c7e6.\n\nNote: values that specified in your .yml file will get loaded after loading the config .yml file." }, "customFetch": { - "description": "Name for a library that implements `fetch`.\nUse this to tell codegen to use that to fetch schemas in a custom way.", - "type": "string" + "description": "Specify a Node module name, a custom file, or a function, to be used instead of a standard `fetch`.", + "anyOf": [ + { + "$ref": "#/definitions/Types.CustomSchemaFetcher", + "description": "A function to use for fetching the schema." + }, + { "type": "string" } + ] }, "documents": { "$ref": "#/definitions/Types.InstanceOrArray_1", @@ -25,7 +31,7 @@ "config": { "type": "object", "additionalProperties": true, - "description": "Configuration object containing key => value that will be passes to the plugins.\nSpecifying configuration in this level of your configuration file will pass it to all plugins, in all outputs.\n\nThe options may vary depends on what plugins you are using.\n\nFor more details: https://graphql-code-generator.com/docs/config-reference/config-field" + "description": "Configuration object containing key => value that will be passed to the plugins.\nSpecifying configuration in this level of your configuration file will pass it to all plugins, in all outputs.\n\nThe options may vary depends on what plugins you are using.\n\nFor more details: https://graphql-code-generator.com/docs/config-reference/config-field" }, "generates": { "description": "A map where the key represents an output path for the generated code and the value represents a set of options which are relevant for that specific file.\n\nFor more details: https://graphql-code-generator.com/docs/config-reference/codegen-config", @@ -100,6 +106,10 @@ "noSilentErrors": { "description": "Alows to raise errors if any matched files are not valid GraphQL. Default: false.", "type": "boolean" + }, + "allowPartialOutputs": { + "description": "If `true`, write to files whichever `generates` block succeeds. If `false`, one failed `generates` means no output is written to files. Default: false", + "type": "boolean" } } }, @@ -648,12 +658,24 @@ "type": "boolean" }, "inlineFragmentTypes": { - "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\nDefault value: \"inline\"", + "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\n\"mask\" transforms the types for use with fragment masking. Useful when masked types are needed when not using the \"client\" preset e.g. such as combining it with Apollo Client's data masking feature.\nDefault value: \"inline\"", "type": "string" }, "emitLegacyCommonJSImports": { "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" + }, + "extractAllFieldsToTypes": { + "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", + "type": "boolean" + }, + "printFieldsOnNewLines": { + "description": "If you prefer to have each field in generated types printed on a new line, set this to true.\nThis can be useful for improving readability of the resulting types,\nwithout resorting to running tools like Prettier on the output.\nDefault value: \"false\"", + "type": "boolean" + }, + "includeExternalFragments": { + "description": "Whether to include external fragments in the generated code. External fragments are not defined\nin the same location as the operation definition.\nDefault value: \"false\"", + "type": "boolean" } } }, @@ -698,6 +720,11 @@ "description": "Adds undefined as a possible type for query variables\nDefault value: \"false\"", "type": "boolean" }, + "nullability": { + "description": "Options related to handling nullability", + "type": "object", + "properties": { "errorHandlingClient": { "type": "boolean" } } + }, "preResolveTypes": { "description": "Uses primitive types where possible.\nSet to `false` in order to use `Pick` and take use the types generated by `typescript` plugin.\nDefault value: \"true\"", "type": "boolean" @@ -730,6 +757,10 @@ "description": "If set to true, merge equal fragment interfaces.\nDefault value: \"false\"", "type": "boolean" }, + "customDirectives": { + "$ref": "#/definitions/CustomDirectivesConfig", + "description": "Configures behavior for use with custom directives from\nvarious GraphQL libraries." + }, "addUnderscoreToArgsType": { "description": "Adds `_` to generated `Args` types in order to avoid duplicate identifiers.", "type": "boolean" @@ -824,12 +855,24 @@ "type": "boolean" }, "inlineFragmentTypes": { - "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\nDefault value: \"inline\"", + "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\n\"mask\" transforms the types for use with fragment masking. Useful when masked types are needed when not using the \"client\" preset e.g. such as combining it with Apollo Client's data masking feature.\nDefault value: \"inline\"", "type": "string" }, "emitLegacyCommonJSImports": { "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" + }, + "extractAllFieldsToTypes": { + "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", + "type": "boolean" + }, + "printFieldsOnNewLines": { + "description": "If you prefer to have each field in generated types printed on a new line, set this to true.\nThis can be useful for improving readability of the resulting types,\nwithout resorting to running tools like Prettier on the output.\nDefault value: \"false\"", + "type": "boolean" + }, + "includeExternalFragments": { + "description": "Whether to include external fragments in the generated code. External fragments are not defined\nin the same location as the operation definition.\nDefault value: \"false\"", + "type": "boolean" } } }, @@ -1559,6 +1602,11 @@ "description": "Allow you to disable suffixing for generated enums, works in combination with `typesSuffix`.\nDefault value: \"true\"", "type": "boolean" }, + "customDirectives": { + "description": "Configures behavior for custom directives from various GraphQL libraries.", + "type": "object", + "properties": { "semanticNonNull": { "type": "boolean" } } + }, "optionalResolveType": { "description": "Sets the `__resolveType` field as optional field.\nDefault value: \"false\"", "type": "boolean" @@ -1583,6 +1631,10 @@ "type": "string", "description": "Defines the prefix value used for `__resolveType` and `__isTypeOf` resolvers.\nIf you are using `mercurius-js`, please set this field to empty string for better compatibility.\nDefault value: \"'__'\"" }, + "generateInternalResolversIfNeeded": { + "type": "object", + "description": "If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met.\nEnabling this allows a more correct type generation for the resolvers.\nFor example:\n- `__isTypeOf` is generated for implementing types and union members\n- `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive\nDefault value: \"{ __resolveReference: false }\"" + }, "onlyResolveTypeForInterfaces": { "type": "boolean", "description": "Turning this flag to `true` will generate resolver signature that has only `resolveType` for interfaces, forcing developers to write inherited type resolvers in the type itself.\nDefault value: \"false\"" @@ -1591,6 +1643,10 @@ "description": "Makes `__typename` of resolver mappings non-optional without affecting the base types.\nDefault value: \"false\"", "anyOf": [{ "$ref": "#/definitions/ResolversNonOptionalTypenameConfig" }, { "type": "boolean" }] }, + "avoidCheckingAbstractTypesRecursively": { + "type": "boolean", + "description": "If true, recursively goes through all object type's fields, checks if they have abstract types and generates expected types correctly.\nThis may not work for cases where provided default mapper types are also nested e.g. `defaultMapper: DeepPartial<{T}>` or `defaultMapper: Partial<{T}>`.\nDefault value: \"false\"" + }, "strictScalars": { "description": "Makes scalars strict.\n\nIf scalars are found in the schema that are not defined in `scalars`\nan error will be thrown during codegen.\nDefault value: \"false\"", "type": "boolean" @@ -1626,12 +1682,24 @@ "type": "boolean" }, "inlineFragmentTypes": { - "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\nDefault value: \"inline\"", + "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\n\"mask\" transforms the types for use with fragment masking. Useful when masked types are needed when not using the \"client\" preset e.g. such as combining it with Apollo Client's data masking feature.\nDefault value: \"inline\"", "type": "string" }, "emitLegacyCommonJSImports": { "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" + }, + "extractAllFieldsToTypes": { + "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", + "type": "boolean" + }, + "printFieldsOnNewLines": { + "description": "If you prefer to have each field in generated types printed on a new line, set this to true.\nThis can be useful for improving readability of the resulting types,\nwithout resorting to running tools like Prettier on the output.\nDefault value: \"false\"", + "type": "boolean" + }, + "includeExternalFragments": { + "description": "Whether to include external fragments in the generated code. External fragments are not defined\nin the same location as the operation definition.\nDefault value: \"false\"", + "type": "boolean" } } }, @@ -2697,7 +2765,7 @@ "description": "Declares how DocumentNode are created:\n\n- `graphQLTag`: `graphql-tag` or other modules (check `gqlImport`) will be used to generate document nodes. If this is used, document nodes are generated on client side i.e. the module used to generate this will be shipped to the client\n- `documentNode`: document nodes will be generated as objects when we generate the templates.\n- `documentNodeImportFragments`: Similar to documentNode except it imports external fragments instead of embedding them.\n- `external`: document nodes are imported from an external file. To be used with `importDocumentNodeExternallyFrom`\n\nNote that some plugins (like `typescript-graphql-request`) also supports `string` for this parameter.\nDefault value: \"graphQLTag\"" }, "optimizeDocumentNode": { - "description": "If you are using `documentNode: documentMode | documentNodeImportFragments`, you can set this to `true` to apply document optimizations for your GraphQL document.\nThis will remove all \"loc\" and \"description\" fields from the compiled document, and will remove all empty arrays (such as `directives`, `arguments` and `variableDefinitions`).\nDefault value: \"true\"", + "description": "If you are using `documentMode: documentNode | documentNodeImportFragments`, you can set this to `true` to apply document optimizations for your GraphQL document.\nThis will remove all \"loc\" and \"description\" fields from the compiled document, and will remove all empty arrays (such as `directives`, `arguments` and `variableDefinitions`).\nDefault value: \"true\"", "type": "boolean" }, "importOperationTypesFrom": { @@ -2747,12 +2815,24 @@ "type": "boolean" }, "inlineFragmentTypes": { - "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\nDefault value: \"inline\"", + "description": "Whether fragment types should be inlined into other operations.\n\"inline\" is the default behavior and will perform deep inlining fragment types within operation type definitions.\n\"combine\" is the previous behavior that uses fragment type references without inlining the types (and might cause issues with deeply nested fragment that uses list types).\n\"mask\" transforms the types for use with fragment masking. Useful when masked types are needed when not using the \"client\" preset e.g. such as combining it with Apollo Client's data masking feature.\nDefault value: \"inline\"", "type": "string" }, "emitLegacyCommonJSImports": { "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" + }, + "extractAllFieldsToTypes": { + "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", + "type": "boolean" + }, + "printFieldsOnNewLines": { + "description": "If you prefer to have each field in generated types printed on a new line, set this to true.\nThis can be useful for improving readability of the resulting types,\nwithout resorting to running tools like Prettier on the output.\nDefault value: \"false\"", + "type": "boolean" + }, + "includeExternalFragments": { + "description": "Whether to include external fragments in the generated code. External fragments are not defined\nin the same location as the operation definition.\nDefault value: \"false\"", + "type": "boolean" } } }, @@ -3404,7 +3484,11 @@ "description": "Create an explicit type based on your schema. This can help IDEs autofill your fragment matcher. This is mostly useful if you do more with your fragment matcher than just pass it to an Apollo-Client.\nDefault value: \"false\"", "type": "boolean" }, - "federation": { "type": "boolean" } + "federation": { "type": "boolean" }, + "deterministic": { + "description": "When enabled sorts the fragment types lexicographically. This is useful for deterministic output.\nDefault value: \"false\"", + "type": "boolean" + } } }, "UrqlIntrospectionConfig": { @@ -3601,8 +3685,14 @@ "additionalProperties": { "type": "string" } }, "customFetch": { - "description": "Specify a Node module name, or a custom file, to be used instead of standard `fetch`", - "type": "string" + "description": "Specify a Node module name, a custom file, or a function, to be used instead of a standard `fetch`.", + "anyOf": [ + { + "$ref": "#/definitions/Types.CustomSchemaFetcher", + "description": "A function to use for fetching the schema." + }, + { "type": "string" } + ] }, "method": { "description": "HTTP Method to use, either POST (default) or GET.", "type": "string" }, "handleAsSDL": { @@ -3611,6 +3701,8 @@ } } }, + "__type": { "description": "A function to use for fetching the schema.", "type": "object" }, + "Types.CustomSchemaFetcher": { "$ref": "#/definitions/__type" }, "Types.LocalSchemaPathWithOptions": { "type": "object", "additionalProperties": { "$ref": "#/definitions/Types.LocalSchemaPathOptions" } @@ -3686,8 +3778,8 @@ "includeSources": { "type": "boolean" } } }, - "__type": { "type": "object" }, - "Record": { "$ref": "#/definitions/__type" }, + "__type_1": { "type": "object" }, + "Record": { "$ref": "#/definitions/__type_1" }, "Types.GitHubSchemaOptions": { "type": "object", "additionalProperties": { "type": "object", "properties": { "token": { "type": "string" } } } @@ -4027,14 +4119,14 @@ } ] }, - "__type_1": { + "__type_2": { "type": "object", "properties": { "buildGeneratesSection": { "type": "object" }, "prepareDocuments": { "type": "object" } } }, - "Types.OutputPreset": { "$ref": "#/definitions/__type_1" }, - "__type_2": { "type": "object", "additionalProperties": { "$ref": "#/definitions/T" } }, - "Types.PluginConfig": { "$ref": "#/definitions/__type_2" }, - "__type_3": { + "Types.OutputPreset": { "$ref": "#/definitions/__type_2" }, + "__type_3": { "type": "object", "additionalProperties": { "$ref": "#/definitions/T" } }, + "Types.PluginConfig": { "$ref": "#/definitions/__type_3" }, + "__type_4": { "type": "object", "properties": { "afterStart": { @@ -4081,8 +4173,8 @@ { "type": "string" } ] }, - "__type_4": { "type": "object" }, - "Types.HookFunction": { "$ref": "#/definitions/__type_4" }, + "__type_5": { "type": "object" }, + "Types.HookFunction": { "$ref": "#/definitions/__type_5" }, "Types.LifeCycleAlterHookValue": { "anyOf": [ { "$ref": "#/definitions/Types.HookFunction" }, @@ -4100,9 +4192,9 @@ { "type": "string" } ] }, - "__type_5": { "type": "object" }, - "Types.HookAlterFunction": { "$ref": "#/definitions/__type_5" }, - "Partial": { "$ref": "#/definitions/__type_3" }, + "__type_6": { "type": "object" }, + "Types.HookAlterFunction": { "$ref": "#/definitions/__type_6" }, + "Partial": { "$ref": "#/definitions/__type_4" }, "Types.OutputDocumentTransform": { "anyOf": [ { "$ref": "#/definitions/Types.DocumentTransformObject" }, @@ -4110,18 +4202,18 @@ { "type": "string" } ] }, - "__type_6": { + "__type_7": { "type": "object", "properties": { "transform": { "$ref": "#/definitions/Types.DocumentTransformFunction" } } }, - "__type_7": { "type": "object" }, - "Types.DocumentTransformFunction": { "$ref": "#/definitions/__type_7" }, - "Types.DocumentTransformObject": { "$ref": "#/definitions/__type_6" }, - "__type_8": { "type": "object", "additionalProperties": { "$ref": "#/definitions/T_1" } }, + "__type_8": { "type": "object" }, + "Types.DocumentTransformFunction": { "$ref": "#/definitions/__type_8" }, + "Types.DocumentTransformObject": { "$ref": "#/definitions/__type_7" }, + "__type_9": { "type": "object", "additionalProperties": { "$ref": "#/definitions/T_1" } }, "T_1": { "type": "object" }, - "Types.DocumentTransformFileConfig": { "$ref": "#/definitions/__type_8" }, - "__type_9": { "type": "object" }, - "Types.PackageLoaderFn": { "$ref": "#/definitions/__type_9" }, + "Types.DocumentTransformFileConfig": { "$ref": "#/definitions/__type_9" }, + "__type_10": { "type": "object" }, + "Types.PackageLoaderFn": { "$ref": "#/definitions/__type_10" }, "Array": { "type": "array", "items": { @@ -4143,16 +4235,16 @@ "type": "object", "properties": { "_value": {}, "value": { "type": "string" } } }, - "__type_10": { + "__type_11": { "type": "object", "properties": { "endpoint": { "type": "string" }, "fetchParams": { "anyOf": [{ "$ref": "#/definitions/Record_1" }, { "type": "string" }] } } }, - "__type_11": { "type": "object" }, - "Record_1": { "$ref": "#/definitions/__type_11" }, - "HardcodedFetch": { "$ref": "#/definitions/__type_10" }, + "__type_12": { "type": "object" }, + "Record_1": { "$ref": "#/definitions/__type_12" }, + "HardcodedFetch": { "$ref": "#/definitions/__type_11" }, "NamingConvention": { "anyOf": [ { "$ref": "#/definitions/NamingConventionFn" }, @@ -4160,8 +4252,8 @@ { "type": "string" } ] }, - "__type_12": { "type": "object" }, - "NamingConventionFn": { "$ref": "#/definitions/__type_12" }, + "__type_13": { "type": "object" }, + "NamingConventionFn": { "$ref": "#/definitions/__type_13" }, "NamingConventionMap": { "additionalProperties": false, "type": "object", @@ -4186,7 +4278,10 @@ "object": { "type": "boolean" }, "inputValue": { "type": "boolean" }, "defaultValue": { "type": "boolean" }, - "resolvers": { "type": "boolean" } + "resolvers": { "type": "boolean" }, + "query": { "type": "boolean" }, + "mutation": { "type": "boolean" }, + "subscription": { "type": "boolean" } } }, "EnumValuesMap": { @@ -4222,12 +4317,12 @@ } }, "DeclarationKind": { "enum": ["abstract class", "class", "interface", "type"], "type": "string" }, - "__type_13": { + "__type_14": { "description": "A map between the GraphQL directive name and the identifier that should be used", "type": "object", "additionalProperties": { "type": "string" } }, - "DirectiveArgumentAndInputFieldMappings": { "$ref": "#/definitions/__type_13" }, + "DirectiveArgumentAndInputFieldMappings": { "$ref": "#/definitions/__type_14" }, "ScalarsMap_1": { "description": "Scalars map or a string, a map between the GraphQL scalar name and the identifier that should be used", "anyOf": [ @@ -4250,8 +4345,8 @@ { "type": "string" } ] }, - "__type_14": { "type": "object" }, - "NamingConventionFn_1": { "$ref": "#/definitions/__type_14" }, + "__type_15": { "type": "object" }, + "NamingConventionFn_1": { "$ref": "#/definitions/__type_15" }, "NamingConventionMap_1": { "additionalProperties": false, "type": "object", @@ -4261,6 +4356,15 @@ "transformUnderscore": { "type": "boolean" } } }, + "CustomDirectivesConfig": { + "type": "object", + "properties": { + "apolloUnmask": { + "description": "Adds integration with Apollo Client's `@unmask` directive\nwhen using Apollo Client's data masking feature. `@unmask` ensures fields\nmarked by `@unmask` are available in the type definition.\nDefault value: \"false\"", + "type": "boolean" + } + } + }, "EnumValuesMap_1": { "description": "A raw configuration for enumValues map - can be represented with a single string value for a file path,\na map between enum name and a file path, or a map between enum name and an object with explicit enum values.", "anyOf": [ @@ -4294,8 +4398,8 @@ { "type": "string" } ] }, - "__type_15": { "type": "object" }, - "NamingConventionFn_2": { "$ref": "#/definitions/__type_15" }, + "__type_16": { "type": "object" }, + "NamingConventionFn_2": { "$ref": "#/definitions/__type_16" }, "NamingConventionMap_2": { "additionalProperties": false, "type": "object", @@ -4309,7 +4413,7 @@ "enum": ["documentNode", "documentNodeImportFragments", "external", "graphQLTag", "string"], "type": "string" }, - "__type_16": { + "__type_17": { "type": "object", "properties": { "type": { "type": "string" }, @@ -4319,7 +4423,7 @@ "arguments": { "type": "string" } } }, - "Partial_1": { "$ref": "#/definitions/__type_16" }, + "Partial_1": { "$ref": "#/definitions/__type_17" }, "AvoidOptionalsConfig_1": { "type": "object", "properties": { @@ -4342,12 +4446,12 @@ } }, "DeclarationKind_1": { "enum": ["abstract class", "class", "interface", "type"], "type": "string" }, - "__type_17": { + "__type_18": { "description": "A map between the GraphQL directive name and the identifier that should be used", "type": "object", "additionalProperties": { "type": "string" } }, - "DirectiveArgumentAndInputFieldMappings_1": { "$ref": "#/definitions/__type_17" }, + "DirectiveArgumentAndInputFieldMappings_1": { "$ref": "#/definitions/__type_18" }, "Array_1": { "type": "array", "items": { "type": "string" } }, "ResolversNonOptionalTypenameConfig": { "type": "object", @@ -4400,12 +4504,12 @@ } }, "DeclarationKind_2": { "enum": ["abstract class", "class", "interface", "type"], "type": "string" }, - "__type_18": { + "__type_19": { "description": "A map between the GraphQL directive name and the identifier that should be used", "type": "object", "additionalProperties": { "type": "string" } }, - "DirectiveArgumentAndInputFieldMappings_2": { "$ref": "#/definitions/__type_18" }, + "DirectiveArgumentAndInputFieldMappings_2": { "$ref": "#/definitions/__type_19" }, "AvoidOptionalsConfig_2": { "type": "object", "properties": { @@ -4416,8 +4520,8 @@ "resolvers": { "type": "boolean" } } }, - "__type_19": { "type": "object" }, - "FragmentImportFromFn": { "$ref": "#/definitions/__type_19" }, + "__type_20": { "type": "object" }, + "FragmentImportFromFn": { "$ref": "#/definitions/__type_20" }, "GeneratedPluginsMap": { "anyOf": [ { diff --git a/website/src/pages/docs/config-reference/codegen-config.mdx b/website/src/pages/docs/config-reference/codegen-config.mdx index d48b11824d8..68348da2b57 100644 --- a/website/src/pages/docs/config-reference/codegen-config.mdx +++ b/website/src/pages/docs/config-reference/codegen-config.mdx @@ -40,7 +40,7 @@ export default config ## Configuration options -Here are the supported options that you can define in the config file (see [source code](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/utils/plugins-helpers/src/types.ts#L92)): +Here are the supported options that you can define in the config file (see [source code](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/utils/plugins-helpers/src/types.ts#L388)): - [**`schema` (required)**](./schema-field#root-level) - A URL to your GraphQL endpoint, a local path to `.graphql` file, a glob pattern to your GraphQL schema files, or a JavaScript file that exports the schema to generate code from. This can also be an array that specifies multiple schemas to generate code from. You can read more about the supported formats [here](./schema-field#available-formats) @@ -66,7 +66,7 @@ Here are the supported options that you can define in the config file (see [sour - **`overwrite`** - A flag to overwrite files if they already exist when generating code (`true` by default) -- **`watch`** - A flag to trigger codegen when there are changes in the specified GraphQL schemas. You can either specify a boolean to turn it on/off or specify an array of glob patterns to add custom files to the watch +- **`watch`** - A flag to trigger codegen when there are changes in the specified GraphQL schemas. You can either specify a boolean to turn it on/off or specify an array of glob patterns to add custom files to the watch. When in watch mode, partial success still writes to files. - **`silent`** - A flag to suppress printing errors when they occur @@ -102,6 +102,8 @@ Here are the supported options that you can define in the config file (see [sour - **`skipDocumentsValidation.skipDuplicateValidation`** - A flag to disable the validation for duplicate documents +- **`allowPartialOutputs`** - Allows partial outputs to be written to files if one or more `generates` blocks have errors. Default is `false`. + ## Environment Variables You can use environment variables in your `codegen.ts` file: diff --git a/yarn.lock b/yarn.lock index bdf44c0d885..3d8d36f6c70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2921,28 +2921,6 @@ dependencies: tslib "^2.4.0" -"@graphql-tools/prisma-loader@^8.0.0": - version "8.0.4" - resolved "https://registry.yarnpkg.com/@graphql-tools/prisma-loader/-/prisma-loader-8.0.4.tgz#542be5567b93f1b6147ef85819eb5874969486b2" - integrity sha512-hqKPlw8bOu/GRqtYr0+dINAI13HinTVYBDqhwGAPIFmLr5s+qKskzgCiwbsckdrb5LWVFmVZc+UXn80OGiyBzg== - dependencies: - "@graphql-tools/url-loader" "^8.0.2" - "@graphql-tools/utils" "^10.0.13" - "@types/js-yaml" "^4.0.0" - "@whatwg-node/fetch" "^0.9.0" - chalk "^4.1.0" - debug "^4.3.1" - dotenv "^16.0.0" - graphql-request "^6.0.0" - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.0" - jose "^5.0.0" - js-yaml "^4.0.0" - lodash "^4.17.20" - scuid "^1.1.0" - tslib "^2.4.0" - yaml-ast-parser "^0.0.43" - "@graphql-tools/relay-operation-optimizer@^6.5.0": version "6.5.18" resolved "https://registry.yarnpkg.com/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz#a1b74a8e0a5d0c795b8a4d19629b654cf66aa5ab" @@ -3009,7 +2987,7 @@ value-or-promise "^1.0.11" ws "^8.12.0" -"@graphql-tools/url-loader@^8.0.0", "@graphql-tools/url-loader@^8.0.2": +"@graphql-tools/url-loader@^8.0.0": version "8.0.2" resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-8.0.2.tgz#ee8e10a85d82c72662f6bc6bbc7b408510a36ebd" integrity sha512-1dKp2K8UuFn7DFo1qX5c1cyazQv2h2ICwA9esHblEqCYrgf69Nk8N7SODmsfWg94OEaI74IqMoM12t7eIGwFzQ== @@ -4835,7 +4813,7 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/js-yaml@4.0.9", "@types/js-yaml@^4.0.0": +"@types/js-yaml@4.0.9": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== @@ -5006,16 +4984,16 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" - integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== - -"@types/unist@^3.0.0": +"@types/unist@*", "@types/unist@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a" integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w== +"@types/unist@^2.0.0", "@types/unist@^2.0.2": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" + integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== + "@types/ws@^8.0.0": version "8.5.4" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" @@ -5539,13 +5517,6 @@ acorn@8.14.0, acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.3, acorn@^8.12.0, acorn@^8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -agent-base@^7.0.2, agent-base@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" - integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== - dependencies: - debug "^4.3.4" - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -7274,7 +7245,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.6, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@~4.3.6: +debug@4.3.6, debug@~4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== @@ -7288,7 +7259,7 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.7: +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@^4.3.7: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -7481,11 +7452,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -dotenv@^16.0.0: - version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== - dotenv@^8.1.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -9120,14 +9086,6 @@ graphql-request@5.2.0: extract-files "^9.0.0" form-data "^3.0.0" -graphql-request@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.0.0.tgz#9c8b6a0c341f289e049936d03cc9205300faae1c" - integrity sha512-2BmHTuglonjZvmNVw6ZzCfFlW/qkIPds0f+Qdi/Lvjsl3whJg2uvHmSvHnLWhUTEw6zcxPYAHiZoPvSVKOZ7Jw== - dependencies: - "@graphql-typed-document-node/core" "^3.2.0" - cross-fetch "^3.1.5" - graphql-sock@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/graphql-sock/-/graphql-sock-1.0.0.tgz#efdfb991cbd8a37da91d7e2e81c2955945c6df37" @@ -9449,14 +9407,6 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -http-proxy-agent@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" - integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== - dependencies: - agent-base "^7.1.0" - debug "^4.3.4" - http-signature@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" @@ -9466,14 +9416,6 @@ http-signature@~1.4.0: jsprim "^2.0.2" sshpk "^1.18.0" -https-proxy-agent@^7.0.0: - version "7.0.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" - integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== - dependencies: - agent-base "^7.0.2" - debug "4" - human-id@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-id/-/human-id-1.0.2.tgz#e654d4b2b0d8b07e45da9f6020d8af17ec0a5df3" @@ -10630,11 +10572,6 @@ joi@^17.11.0: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" -jose@^5.0.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0" - integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg== - joycon@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" @@ -10645,7 +10582,7 @@ joycon@^3.1.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -11160,7 +11097,7 @@ lodash.startcase@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== -lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: +lodash@^4.17.21, lodash@~4.17.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -14133,11 +14070,6 @@ scslre@0.3.0: refa "^0.12.0" regexp-ast-analysis "^0.7.0" -scuid@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/scuid/-/scuid-1.1.0.tgz#d3f9f920956e737a60f72d0e4ad280bf324d5dab" - integrity sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg== - section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -16286,11 +16218,6 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml-ast-parser@^0.0.43: - version "0.0.43" - resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" - integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== - yaml-eslint-parser@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/yaml-eslint-parser/-/yaml-eslint-parser-1.2.2.tgz#1a9673ebe254328cfc2fa99f297f6d8c9364ccd8"