From c9bb00160e48b4f95b6e2696c57c4efdda747d88 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Dec 2024 19:35:53 +1100 Subject: [PATCH 01/11] [resolvers][federation] Fix mapper being incorrectly used as the base type for reference (#10216) --- .changeset/thick-pianos-smoke.md | 11 ++ .github/workflows/main.yml | 1 + dev-test/test-schema/resolvers-federation.ts | 18 +- .../src/base-resolvers-visitor.ts | 34 +++- .../plugins/typescript/resolvers/src/index.ts | 9 +- .../typescript/resolvers/src/visitor.ts | 16 +- .../__snapshots__/ts-resolvers.spec.ts.snap | 3 + .../ts-resolvers.federation.mappers.spec.ts | 168 ++++++++++++++++++ .../tests/ts-resolvers.federation.spec.ts | 143 ++++++--------- .../typescript/resolvers/tests/utils.ts | 15 ++ .../utils/plugins-helpers/src/federation.ts | 38 ++-- 11 files changed, 317 insertions(+), 139 deletions(-) create mode 100644 .changeset/thick-pianos-smoke.md create mode 100644 packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts create mode 100644 packages/plugins/typescript/resolvers/tests/utils.ts 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/.github/workflows/main.yml b/.github/workflows/main.yml index 0b337f5d2d4..8fd507f09a0 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' diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index d2c05ac9d13..ba1feb00f47 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -128,6 +128,11 @@ export type DirectiveResolverFn TResult | Promise; +/** Mapping of federation types */ +export type FederationTypes = { + User: User; +}; + /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Address: ResolverTypeWrapper
; @@ -190,13 +195,14 @@ export type QueryResolvers< export type UserResolvers< ContextType = any, - ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], + FederationType extends FederationTypes['User'] = FederationTypes['User'] > = { __resolveReference?: ReferenceResolver< Maybe, { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ), ContextType >; @@ -204,10 +210,10 @@ export type UserResolvers< email?: Resolver< ResolversTypes['String'], { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) & - GraphQLRecursivePick, + GraphQLRecursivePick, ContextType >; 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 3d7efa48c3b..e4facaf820e 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 @@ -1257,6 +1257,28 @@ 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 get schema(): GraphQLSchema { return this._schema; } @@ -1525,6 +1547,7 @@ export class BaseResolversVisitor< fieldNode: original, parentType, parentTypeSignature: this.getParentTypeForSignature(node), + federationTypeSignature: 'FederationType', }); const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => { @@ -1671,10 +1694,19 @@ export class BaseResolversVisitor< ); } + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + if (this._federation.getMeta()[typeName]) { + const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`; + genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); + } + 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] = { diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 0dbe840d1cd..3a8d8aeb59e 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -110,13 +110,6 @@ export type ResolverWithResolve = { 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 +241,7 @@ export type DirectiveResolverFn TResult | Promise; `; + const federationTypes = visitor.buildFederationTypes(); const resolversTypeMapping = visitor.buildResolversTypes(); const resolversParentTypeMapping = visitor.buildResolversParentTypes(); const resolversUnionTypesMapping = visitor.buildResolversUnionTypes(); @@ -291,6 +285,7 @@ export type DirectiveResolverFn`; } - 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)}>`; } 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 8783aac2cb1..0e3228d4016 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,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: ( Omit & { parent?: Maybe<_RefType['MyType']> } ) | ( MyOtherType ); @@ -425,6 +426,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: ( Omit & { parent?: Types.Maybe<_RefType['MyType']> } ) | ( Types.MyOtherType ); @@ -770,6 +772,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: ( Omit & { parent?: Maybe<_RefType['MyType']> } ) | ( MyOtherType ); 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..9db647c00cc --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -0,0 +1,168 @@ +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! + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + mappers: { + User: './mappers#UserMapper', + }, + }, + }); + + // User should have it + expect(content).toMatchInlineSnapshot(` + "import { GraphQLResolveInfo } from 'graphql'; + import { UserMapper } 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; + }; + + + + /** 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'] }>; + 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'] }; + Boolean: Scalars['Boolean']['output']; + }; + + export type QueryResolvers = { + me?: Resolver, ParentType, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type UserProfileResolvers = { + id?: Resolver; + user?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type Resolvers = { + Query?: QueryResolvers; + User?: UserResolvers; + UserProfile?: UserProfileResolvers; + }; + + " + `); + }); +}); 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..02a3d3ba0d7 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -85,8 +85,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; @@ -95,24 +95,24 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; `); expect(content).toBeSimilarStringTo(` - export type SingleNonResolvableResolvers = { - __resolveReference?: ReferenceResolver, ParentType, ContextType>; + export type SingleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, FederationType, ContextType>; id?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; `); expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -121,8 +121,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -131,8 +131,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type MultipleNonResolvableResolvers = { - __resolveReference?: ReferenceResolver, ParentType, ContextType>; + export type MultipleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, FederationType, ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -211,8 +211,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // User should have __resolveReference because it has resolvable @key (by default) expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; @@ -222,8 +222,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // SingleResolvable has __resolveReference because it has resolvable: true expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -239,8 +239,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // AtLeastOneResolvable has __resolveReference because it at least one resolvable expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -250,8 +250,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // MixedResolvable has __resolveReference and references for resolvable keys expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -305,11 +305,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // User should have it expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; `); // Foo shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; `); }); @@ -345,19 +345,19 @@ 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>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver, ContextType>; + name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; __isTypeOf?: IsTypeOfResolverFn; } `); expect(content).toBeSimilarStringTo(` - export type NameResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'Name' } & GraphQLRecursivePick, ContextType>; - first?: Resolver, ContextType>; - last?: Resolver, ContextType>; + export type NameResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'Name' } & GraphQLRecursivePick, ContextType>; + first?: Resolver, ContextType>; + last?: Resolver, ContextType>; __isTypeOf?: IsTypeOfResolverFn; } `); @@ -386,10 +386,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // 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>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver, ContextType>; + username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -423,9 +423,9 @@ 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>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -456,9 +456,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -489,8 +489,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; name?: Resolver; username?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -568,10 +568,10 @@ 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>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver, ContextType>; + name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -695,10 +695,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // 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>; + 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; }; `); @@ -763,49 +763,6 @@ 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 () => { - const federatedSchema = /* GraphQL */ ` - type User @key(fields: "id") { - id: ID! - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - wrapFieldDefinitions: true, - }, - }); - - expect(content).toBeSimilarStringTo(`type UnwrappedObject = {`); - }); - - it('should add UnwrappedObject around ParentType for __resloveReference', async () => { - const federatedSchema = /* GraphQL */ ` - type User @key(fields: "id") { - id: ID! - } - `; - - 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 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/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index d77277629e7..4da2e33e1c1 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -155,10 +155,12 @@ export class ApolloFederation { fieldNode, parentType, parentTypeSignature, + federationTypeSignature, }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType; parentTypeSignature: string; + federationTypeSignature: string; }) { if ( this.enabled && @@ -172,30 +174,32 @@ export class ApolloFederation { const { resolvableKeyDirectives } = objectTypeFederationDetails; - if (resolvableKeyDirectives.length) { - const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; + if (resolvableKeyDirectives.length === 0) { + return federationTypeSignature; + } - // 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); + const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; - // @key() @key() - "primary keys" in Federation - const primaryKeys = resolvableKeyDirectives.map(def => { - const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, parentTypeSignature); - }); + // 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), federationTypeSignature); - const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; + // @key() @key() - "primary keys" in Federation + const primaryKeys = resolvableKeyDirectives.map(def => { + const fields = this.extractFieldSet(def); + return this.translateFieldSet(fields, federationTypeSignature); + }); - outputs.push([open, primaryKeys.join(' | '), close].join('')); + const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; - // include required fields - if (requires.length) { - outputs.push(`& ${requiredFields}`); - } + outputs.push([open, primaryKeys.join(' | '), close].join('')); - return outputs.join(' '); + // include required fields + if (requires.length) { + outputs.push(`& ${requiredFields}`); } + + return outputs.join(' '); } return parentTypeSignature; From 4a8e5d489654749b3ea09251b7df5911503cd710 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 29 Jan 2025 20:24:27 +1100 Subject: [PATCH 02/11] [resolvers][federation] Add `__resolveReference` to applicable `Interface` entities, fix Interface types having non-meta resolver fields (#10221) * Add __resolveReference for applicable Interfaces - Deprecate generateInternalResolversIfNeeded.__resolveReference - Fix tests - Deprecate onlyResolveTypeForInterfaces - Add changeset - Cleanup - Handle __resolveReference generation in Interface - Let FieldDefinition decide whether to generate __resolveReference by checking whether parent has resolvable key * Fix test --- .changeset/loud-suits-admire.md | 10 + .../src/base-resolvers-visitor.ts | 129 +++--- .../other/visitor-plugin-common/src/types.ts | 4 +- .../plugins/typescript/resolvers/src/index.ts | 12 +- .../typescript/resolvers/src/visitor.ts | 8 +- .../__snapshots__/ts-resolvers.spec.ts.snap | 18 - ...ts-resolvers.config.avoidOptionals.spec.ts | 1 - .../ts-resolvers.federation.interface.spec.ts | 198 +++++++++ .../tests/ts-resolvers.federation.spec.ts | 377 ++++-------------- .../tests/ts-resolvers.interface.spec.ts | 1 - .../tests/ts-resolvers.mapping.spec.ts | 5 - .../resolvers/tests/ts-resolvers.spec.ts | 21 - .../utils/plugins-helpers/src/federation.ts | 189 ++++++--- 13 files changed, 506 insertions(+), 467 deletions(-) create mode 100644 .changeset/loud-suits-admire.md create mode 100644 packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts 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/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index e4facaf820e..6300e46454b 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 { @@ -78,13 +78,15 @@ export interface ParsedResolversConfig extends ParsedConfig { allResolversTypeName: string; internalResolversPrefix: string; generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; - onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; avoidCheckingAbstractTypesRecursively: boolean; } -type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; +type FieldDefinitionPrintFn = ( + parentName: string, + avoidResolverOptionals: boolean +) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } }; export interface RootResolver { content: string; generatedResolverTypes: { @@ -618,20 +620,13 @@ export interface RawResolversConfig extends RawConfig { internalResolversPrefix?: string; /** * @type object - * @default { __resolveReference: false } + * @default {} * @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 @@ -734,7 +729,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 +744,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 +758,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 +767,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, @@ -1392,7 +1389,9 @@ export class BaseResolversVisitor< const federationMeta = this._federation.getMeta()[schemaTypeName]; if (federationMeta) { - userDefinedTypes[schemaTypeName].federation = federationMeta; + userDefinedTypes[schemaTypeName].federation = { + hasResolveReference: federationMeta.hasResolveReference, + }; } } @@ -1506,9 +1505,10 @@ export class BaseResolversVisitor< return (parentName, avoidResolverOptionals) => { const original: FieldDefinitionNode = parent[key]; const parentType = this.schema.getType(parentName); + const meta: ReturnType['meta'] = {}; if (this._federation.skipField({ fieldNode: original, parentType })) { - return null; + return { value: null, meta }; } const contextType = this.getContextType(parentName, node); @@ -1543,7 +1543,7 @@ export class BaseResolversVisitor< } } - const parentTypeSignature = this._federation.transformParentType({ + const parentTypeSignature = this._federation.transformFieldParentType({ fieldNode: original, parentType, parentTypeSignature: this.getParentTypeForSignature(node), @@ -1598,29 +1598,22 @@ export class BaseResolversVisitor< }; 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.getMeta()[parentType.name].hasResolveReference) { + return { value: '', meta }; } - - this._federation.setMeta(parentType.name, { hasResolveReference: true }); signature.type = 'ReferenceResolver'; - if (signature.genericTypes.length >= 3) { - signature.genericTypes = signature.genericTypes.slice(0, 3); - } + signature.genericTypes = [mappedTypeKey, parentTypeSignature, 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, + }; }; } @@ -1681,7 +1674,7 @@ export class BaseResolversVisitor< (rootType === 'mutation' && this.config.avoidOptionals.mutation) || (rootType === 'subscription' && this.config.avoidOptionals.subscription) || (rootType === false && this.config.avoidOptionals.resolvers) - ); + ).value; }); if (!rootType) { @@ -1698,10 +1691,11 @@ export class BaseResolversVisitor< `ContextType = ${this.config.contextType.type}`, this.transformParentGenericType(parentType), ]; - if (this._federation.getMeta()[typeName]) { - const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`; - genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); - } + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationTypes'), + typeName, + }); const block = new DeclarationBlock(this._declarationBlockConfig) .export() @@ -1890,25 +1884,44 @@ export class BaseResolversVisitor< } const parentType = this.getParentTypeToUse(typeName); + + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationTypes'), + 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 FieldDefinitionPrintFn[]).map(f => + f(typeName, 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..e2e2004bfea 100644 --- a/packages/plugins/other/visitor-plugin-common/src/types.ts +++ b/packages/plugins/other/visitor-plugin-common/src/types.ts @@ -139,7 +139,5 @@ export interface CustomDirectivesConfig { apolloUnmask?: boolean; } -export interface GenerateInternalResolversIfNeededConfig { - __resolveReference?: boolean; -} +export interface GenerateInternalResolversIfNeededConfig {} export type NormalizedGenerateInternalResolversIfNeededConfig = Required; diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 3a8d8aeb59e..02f6e801b29 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 { - constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema) { + constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema, federationMeta: FederationMeta) { super( pluginConfig, { @@ -34,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( 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 0e3228d4016..eeda2eb6b4d 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 @@ -272,7 +272,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -282,19 +281,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<{ @@ -532,7 +526,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -542,19 +535,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<{ @@ -878,7 +866,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -888,19 +875,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.federation.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts new file mode 100644 index 00000000000..8ef9616ff40 --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -0,0 +1,198 @@ +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 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; + Admin: 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, { __typename: 'Person' } & GraphQLRecursivePick, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type AdminResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'Admin' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver; + canImpersonate?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type PersonNameResolvers = { + first?: Resolver; + last?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type Resolvers = { + Query?: QueryResolvers; + Person?: PersonResolvers; + User?: UserResolvers; + Admin?: AdminResolvers; + PersonName?: PersonNameResolvers; + }; + + " + `); + }); +}); 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 02a3d3ba0d7..71723b55187 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,141 +76,15 @@ 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, FederationType, 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, FederationType, 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! - } - - type MultipleNonResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: false) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - generateInternalResolversIfNeeded: { __resolveReference: true }, - }, - }); - - // User should have __resolveReference because it has resolvable @key (by default) - expect(content).toBeSimilarStringTo(` + // 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; @@ -220,8 +94,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // SingleResolvable has __resolveReference because it has resolvable: true - expect(content).toBeSimilarStringTo(` + // SingleResolvable has __resolveReference because it has resolvable: true + expect(content).toBeSimilarStringTo(` export type SingleResolvableResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; @@ -229,16 +103,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // 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(` + // AtLeastOneResolvable has __resolveReference because it at least one resolvable + expect(content).toBeSimilarStringTo(` export type AtLeastOneResolvableResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; @@ -248,8 +122,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // MixedResolvable has __resolveReference and references for resolvable keys - expect(content).toBeSimilarStringTo(` + // MixedResolvable has __resolveReference and references for resolvable keys + expect(content).toBeSimilarStringTo(` export type MixedResolvableResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; id?: Resolver; @@ -259,8 +133,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // 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; @@ -269,14 +143,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // 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 () => { @@ -498,50 +371,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); }); - it.skip('should handle interface 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") - } - - extend type User implements Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name { first last }") - 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 - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - }, - }); - - expect(content).toBeSimilarStringTo(` - export type PersonResolvers = { - __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - age?: Resolver, { __typename: 'User' | 'Admin' } & GraphQLRecursivePick, ContextType>; - }; - `); - }); - it('should skip to generate resolvers of fields with @external directive', async () => { const federatedSchema = /* GraphQL */ ` type Query { @@ -763,132 +592,68 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).not.toContain('GraphQLScalarType'); }); - 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 - } + describe('meta', () => { + it('generates federation meta correctly', async () => { + const federatedSchema = /* GraphQL */ ` + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - interface Node { - id: ID! - } + type Query { + user: UserPayload! + allUsers: [User] + } - type UserOk { - id: ID! - } - type UserError { - message: String! - } - union UserPayload = UserOk | UserError + type User @key(fields: "id") { + id: ID! + name: String + username: String + } - 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..64e997ac25d 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; }; `); }); 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/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 4da2e33e1c1..3ac608bf604 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,11 +1,13 @@ -import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { astFromInterfaceType, astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; import { DefinitionNode, DirectiveNode, FieldDefinitionNode, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLSchema, + isInterfaceType, isObjectType, ObjectTypeDefinitionNode, OperationDefinitionNode, @@ -28,14 +30,67 @@ export const federationSpec = parse(/* GraphQL */ ` directive @key(fields: _FieldSet!) on OBJECT | INTERFACE `); +interface TypeMeta { + hasResolveReference: boolean; + resolvableKeyDirectives: readonly DirectiveNode[]; +} + +export type FederationMeta = { [typeName: string]: TypeMeta }; + /** - * Adds `__resolveReference` in each ObjectType involved in Federation. + * 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: [] }), ...update }; + }; + + const federationMeta: FederationMeta = {}; + + const transformedSchema = mapSchema(schema, { + [MapperKind.INTERFACE_TYPE]: type => { + const federationDetails = checkTypeFederationDetails(type, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { + const typeConfig = type.toConfig(); + typeConfig.fields = { + [resolveReferenceFieldName]: { + type, + }, + ...typeConfig.fields, + }; + + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + }, + }); + + return new GraphQLInterfaceType(typeConfig); + } + + return type; + }, [MapperKind.OBJECT_TYPE]: type => { - if (checkObjectTypeFederationDetails(type, schema)) { + const federationDetails = checkTypeFederationDetails(type, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { const typeConfig = type.toConfig(); typeConfig.fields = { [resolveReferenceFieldName]: { @@ -44,11 +99,25 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLS ...typeConfig.fields, }; + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + }, + }); + return new GraphQLObjectType(typeConfig); } return type; }, }); + + return { + transformedSchema, + federationMeta, + }; } /** @@ -82,20 +151,17 @@ 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 } = {}; + 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.meta = meta; } /** @@ -135,7 +201,11 @@ export class ApolloFederation { * @param data */ skipField({ fieldNode, parentType }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType }): boolean { - if (!this.enabled || !isObjectType(parentType) || !checkObjectTypeFederationDetails(parentType, this.schema)) { + if ( + !this.enabled || + !(isObjectType(parentType) && !isInterfaceType(parentType)) || + !checkTypeFederationDetails(parentType, this.schema) + ) { return false; } @@ -148,10 +218,10 @@ export class ApolloFederation { } /** - * Transforms ParentType signature in ObjectTypes involved in Federation + * Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation * @param data */ - transformParentType({ + transformFieldParentType({ fieldNode, parentType, parentTypeSignature, @@ -162,52 +232,67 @@ export class ApolloFederation { parentTypeSignature: string; federationTypeSignature: 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 (!this.enabled) { + return parentTypeSignature; + } - const { resolvableKeyDirectives } = objectTypeFederationDetails; + const parentTypeMeta = this.getMeta()[parentType.name]; + if (!parentTypeMeta?.hasResolveReference) { + return parentTypeSignature; + } - if (resolvableKeyDirectives.length === 0) { - return federationTypeSignature; - } + const isObjectFieldWithFederationRef = + isObjectType(parentType) && + (nodeHasTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName); - const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; + const isInterfaceFieldWithFederationRef = + isInterfaceType(parentType) && fieldNode.name.value === resolveReferenceFieldName; - // 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), federationTypeSignature); + if (!isObjectFieldWithFederationRef && !isInterfaceFieldWithFederationRef) { + return parentTypeSignature; + } - // @key() @key() - "primary keys" in Federation - const primaryKeys = resolvableKeyDirectives.map(def => { - const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, federationTypeSignature); - }); + const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; - const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; + // 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), federationTypeSignature); - outputs.push([open, primaryKeys.join(' | '), close].join('')); + // @key() @key() - "primary keys" in Federation + const primaryKeys = parentTypeMeta.resolvableKeyDirectives.map(def => { + const fields = this.extractFieldSet(def); + return this.translateFieldSet(fields, federationTypeSignature); + }); - // include required fields - if (requires.length) { - outputs.push(`& ${requiredFields}`); - } + const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; - return outputs.join(' '); + outputs.push([open, primaryKeys.join(' | '), close].join('')); + + // include required fields + if (requires.length) { + outputs.push(`& ${requiredFields}`); } - return parentTypeSignature; + return outputs.join(' '); } - setMeta(typename: string, update: Partial): void { - this.meta[typename] = { ...(this.meta[typename] || { hasResolveReference: false }), ...update }; + addFederationTypeGenericIfApplicable({ + genericTypes, + typeName, + federationTypesType, + }: { + genericTypes: string[]; + typeName: string; + federationTypesType: string; + }): void { + if (!this.getMeta()[typeName]) { + return; + } + + const typeRef = `${federationTypesType}['${typeName}']`; + genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); } + getMeta() { return this.meta; } @@ -295,14 +380,18 @@ 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 | GraphQLObjectType | GraphQLInterfaceType, schema: GraphQLSchema ): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { const { name: { value: name }, directives, - } = isObjectType(node) ? astFromObjectType(node, schema) : node; + } = isObjectType(node) + ? astFromObjectType(node, schema) + : isInterfaceType(node) + ? astFromInterfaceType(node, schema) + : node; const rootTypeNames = getRootTypeNames(schema); const isNotRoot = !rootTypeNames.has(name); @@ -352,7 +441,7 @@ function getDirectivesByName( * Based on if any of its fields contain the `@external` directive * @param node Type */ -function isTypeExtension(node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema): boolean { +function nodeHasTypeExtension(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); } From 3e618b17009ea8c6610364a9a1d8e4ce5b855114 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jan 2025 09:24:59 +0000 Subject: [PATCH 03/11] chore(dependencies): updated changesets for modified dependencies --- .changeset/@graphql-codegen_cli-10218-dependencies.md | 6 ++++++ .../@graphql-codegen_client-preset-10218-dependencies.md | 9 +++++++++ ...phql-codegen_gql-tag-operations-10218-dependencies.md | 5 +++++ ...-codegen_graphql-modules-preset-10218-dependencies.md | 5 +++++ ...hql-codegen_typed-document-node-10218-dependencies.md | 5 +++++ .../@graphql-codegen_typescript-10218-dependencies.md | 5 +++++ ...degen_typescript-document-nodes-10218-dependencies.md | 5 +++++ ...l-codegen_typescript-operations-10218-dependencies.md | 6 ++++++ ...ql-codegen_typescript-resolvers-10218-dependencies.md | 6 ++++++ 9 files changed, 52 insertions(+) create mode 100644 .changeset/@graphql-codegen_cli-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_client-preset-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typed-document-node-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-operations-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md diff --git a/.changeset/@graphql-codegen_cli-10218-dependencies.md b/.changeset/@graphql-codegen_cli-10218-dependencies.md new file mode 100644 index 00000000000..51d8dedf635 --- /dev/null +++ b/.changeset/@graphql-codegen_cli-10218-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-codegen/cli": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/client-preset@^4.6.0` ↗︎](https://www.npmjs.com/package/@graphql-codegen/client-preset/v/4.6.0) (from `^4.4.0`, in `dependencies`) + - Updated dependency [`@whatwg-node/fetch@^0.10.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.10.0) (from `^0.9.20`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md new file mode 100644 index 00000000000..b1fe1cfaf73 --- /dev/null +++ b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md @@ -0,0 +1,9 @@ +--- +"@graphql-codegen/client-preset": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typed-document-node@^5.0.13` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typed-document-node/v/5.0.13) (from `^5.0.12`, in `dependencies`) + - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) + - Updated dependency [`@graphql-codegen/typescript-operations@^4.4.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript-operations/v/4.4.1) (from `^4.4.0`, in `dependencies`) + - Updated dependency [`@graphql-codegen/gql-tag-operations@4.0.13` ↗︎](https://www.npmjs.com/package/@graphql-codegen/gql-tag-operations/v/4.0.13) (from `4.0.12`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@^5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `^5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md new file mode 100644 index 00000000000..2521d242fbe --- /dev/null +++ b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/gql-tag-operations": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md new file mode 100644 index 00000000000..1012f14b647 --- /dev/null +++ b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/graphql-modules-preset": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md new file mode 100644 index 00000000000..968f5921f33 --- /dev/null +++ b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typed-document-node": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-10218-dependencies.md new file mode 100644 index 00000000000..fb52b9eee9c --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typescript": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md new file mode 100644 index 00000000000..51cb49e5535 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typescript-document-nodes": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md new file mode 100644 index 00000000000..619352f5678 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-codegen/typescript-operations": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md new file mode 100644 index 00000000000..bda76bce275 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-codegen/typescript-resolvers": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) From 46dae96c8e6814deb91d3017084370f3c140bacb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jan 2025 09:27:57 +0000 Subject: [PATCH 04/11] chore(dependencies): updated changesets for modified dependencies --- .changeset/@graphql-codegen_cli-10218-dependencies.md | 6 ------ .../@graphql-codegen_client-preset-10218-dependencies.md | 9 --------- ...phql-codegen_gql-tag-operations-10218-dependencies.md | 5 ----- ...-codegen_graphql-modules-preset-10218-dependencies.md | 5 ----- ...hql-codegen_typed-document-node-10218-dependencies.md | 5 ----- .../@graphql-codegen_typescript-10218-dependencies.md | 5 ----- ...degen_typescript-document-nodes-10218-dependencies.md | 5 ----- ...l-codegen_typescript-operations-10218-dependencies.md | 6 ------ ...ql-codegen_typescript-resolvers-10218-dependencies.md | 6 ------ 9 files changed, 52 deletions(-) delete mode 100644 .changeset/@graphql-codegen_cli-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_client-preset-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typed-document-node-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-operations-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md diff --git a/.changeset/@graphql-codegen_cli-10218-dependencies.md b/.changeset/@graphql-codegen_cli-10218-dependencies.md deleted file mode 100644 index 51d8dedf635..00000000000 --- a/.changeset/@graphql-codegen_cli-10218-dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@graphql-codegen/cli": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/client-preset@^4.6.0` ↗︎](https://www.npmjs.com/package/@graphql-codegen/client-preset/v/4.6.0) (from `^4.4.0`, in `dependencies`) - - Updated dependency [`@whatwg-node/fetch@^0.10.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.10.0) (from `^0.9.20`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md deleted file mode 100644 index b1fe1cfaf73..00000000000 --- a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@graphql-codegen/client-preset": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typed-document-node@^5.0.13` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typed-document-node/v/5.0.13) (from `^5.0.12`, in `dependencies`) - - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) - - Updated dependency [`@graphql-codegen/typescript-operations@^4.4.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript-operations/v/4.4.1) (from `^4.4.0`, in `dependencies`) - - Updated dependency [`@graphql-codegen/gql-tag-operations@4.0.13` ↗︎](https://www.npmjs.com/package/@graphql-codegen/gql-tag-operations/v/4.0.13) (from `4.0.12`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@^5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `^5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md deleted file mode 100644 index 2521d242fbe..00000000000 --- a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/gql-tag-operations": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md deleted file mode 100644 index 1012f14b647..00000000000 --- a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/graphql-modules-preset": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md deleted file mode 100644 index 968f5921f33..00000000000 --- a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typed-document-node": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-10218-dependencies.md deleted file mode 100644 index fb52b9eee9c..00000000000 --- a/.changeset/@graphql-codegen_typescript-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typescript": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md deleted file mode 100644 index 51cb49e5535..00000000000 --- a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typescript-document-nodes": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md deleted file mode 100644 index 619352f5678..00000000000 --- a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@graphql-codegen/typescript-operations": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md deleted file mode 100644 index bda76bce275..00000000000 --- a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@graphql-codegen/typescript-resolvers": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typescript@^4.1.3` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.3) (from `^4.1.2`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.6.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.6.1) (from `5.6.0`, in `dependencies`) From 3c7e4ac855be1361a4ab2430b05cc9e8ba5305be Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 19 Feb 2025 20:42:47 +1100 Subject: [PATCH 05/11] [resolvers] Ensure `__isTypeof` is only generated for implementing types (of Interfaces) or Union members (#10283) * Implement logic to only generate __isTypeOf for implementing types OR union members * Remove unused types * Add changeset * Remove generateInternalResolversIfNeeded * Fix dev tests * Refactor to use parsedSchemaMeta --- .changeset/angry-lamps-notice.md | 7 + dev-test/modules/types.ts | 3 - dev-test/subpath-import/result.d.ts | 2 - dev-test/test-schema/resolvers-federation.ts | 5 - dev-test/test-schema/resolvers-root.ts | 2 - dev-test/test-schema/resolvers-stitching.ts | 1 - dev-test/test-schema/resolvers-types.ts | 1 - dev-test/test-schema/typings.ts | 1 - .../src/base-resolvers-visitor.ts | 171 +++++++++++------- .../other/visitor-plugin-common/src/types.ts | 3 - .../ts-resolvers.federation.interface.spec.ts | 1 - .../ts-resolvers.federation.mappers.spec.ts | 2 - .../tests/ts-resolvers.federation.spec.ts | 15 -- .../tests/ts-resolvers.interface.spec.ts | 44 +++++ .../tests/ts-resolvers.union.spec.ts | 52 ++++++ 15 files changed, 204 insertions(+), 106 deletions(-) create mode 100644 .changeset/angry-lamps-notice.md 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/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 ba1feb00f47..b9ce39f9bb8 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -166,7 +166,6 @@ export type AddressResolvers< city?: Resolver, ParentType, ContextType>; lines?: Resolver; state?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type BookResolvers< @@ -174,7 +173,6 @@ export type BookResolvers< ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book'] > = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type LinesResolvers< @@ -183,7 +181,6 @@ export type LinesResolvers< > = { line1?: Resolver; line2?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type QueryResolvers< @@ -216,8 +213,6 @@ export type UserResolvers< GraphQLRecursivePick, ContextType >; - - __isTypeOf?: IsTypeOfResolverFn; }; 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/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index 6300e46454b..9e9144a70a2 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 @@ -6,9 +6,11 @@ import { DirectiveDefinitionNode, EnumTypeDefinitionNode, FieldDefinitionNode, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLSchema, + GraphQLUnionType, InputValueDefinitionNode, InterfaceTypeDefinitionNode, isEnumType, @@ -33,8 +35,6 @@ import { ConvertOptions, DeclarationKind, EnumValuesMap, - type NormalizedGenerateInternalResolversIfNeededConfig, - type GenerateInternalResolversIfNeededConfig, NormalizedAvoidOptionalsConfig, NormalizedScalarsMap, ParsedEnumValuesMap, @@ -77,7 +77,6 @@ export interface ParsedResolversConfig extends ParsedConfig { resolverTypeSuffix: string; allResolversTypeName: string; internalResolversPrefix: string; - generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; avoidCheckingAbstractTypesRecursively: boolean; @@ -618,15 +617,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 {} - * @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 - */ - generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig; /** * @description Makes `__typename` of resolver mappings non-optional without affecting the base types. * @default false @@ -705,6 +695,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 } = {}; @@ -713,7 +728,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(); @@ -779,6 +793,11 @@ 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), @@ -1030,22 +1049,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; } @@ -1055,37 +1072,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; } @@ -1637,6 +1639,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(' | ')}>`; @@ -1677,7 +1719,7 @@ export class BaseResolversVisitor< ).value; }); - if (!rootType) { + if (!rootType && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) { fieldsContent.push( indent( `${ @@ -1864,25 +1906,14 @@ 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 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[] = [ diff --git a/packages/plugins/other/visitor-plugin-common/src/types.ts b/packages/plugins/other/visitor-plugin-common/src/types.ts index e2e2004bfea..88f01fe14ed 100644 --- a/packages/plugins/other/visitor-plugin-common/src/types.ts +++ b/packages/plugins/other/visitor-plugin-common/src/types.ts @@ -138,6 +138,3 @@ export interface CustomDirectivesConfig { */ apolloUnmask?: boolean; } - -export interface GenerateInternalResolversIfNeededConfig {} -export type NormalizedGenerateInternalResolversIfNeededConfig = Required; 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 index 8ef9616ff40..b71c937b241 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -181,7 +181,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { export type PersonNameResolvers = { first?: Resolver; last?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { 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 index 9db647c00cc..93710cce5f7 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -147,13 +147,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; export type UserProfileResolvers = { id?: Resolver; user?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { 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 71723b55187..fb73dc0d613 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -90,7 +90,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { id?: Resolver; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -99,7 +98,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { export type SingleResolvableResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -107,7 +105,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type SingleNonResolvableResolvers = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -118,7 +115,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { id?: Resolver; id2?: Resolver; id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -129,7 +125,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { id?: Resolver; id2?: Resolver; id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -139,7 +134,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { id?: Resolver; id2?: Resolver; id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); @@ -147,7 +141,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type BookResolvers = { id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -222,7 +215,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver, ContextType>; name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; } `); @@ -231,7 +223,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'Name' } & GraphQLRecursivePick, ContextType>; first?: Resolver, ContextType>; last?: Resolver, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; } `); }); @@ -263,7 +254,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver, ContextType>; username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -299,7 +289,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { export type UserResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -332,7 +321,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { export type UserResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -366,7 +354,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; name?: Resolver; username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -401,7 +388,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; id?: Resolver, ContextType>; name?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); @@ -528,7 +514,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; name?: Resolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; username?: Resolver, { __typename: 'User' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - __isTypeOf?: IsTypeOfResolverFn; }; `); }); 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 64e997ac25d..72c4c41844c 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts @@ -460,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.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; + }; + `); + }); }); From 425eeabd5cb7e5418a0b9b2d39c40ebdcb54046f Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 5 Mar 2025 20:52:09 +1100 Subject: [PATCH 06/11] [resolvers][federation] Bring Federation reference selection set to ResolversParentTypes instead of each resolver (#10297) * Bring reference selection set to ResolversParentTypes * Put back old types to extractReferenceSelectionSet * Update tests for TDD * Handle parent type consistently for __resolveReference and subsequent resolvers * Update tests --- dev-test/test-schema/resolvers-federation.ts | 19 +- .../src/base-resolvers-visitor.ts | 19 ++ .../ts-resolvers.federation.interface.spec.ts | 20 +- .../ts-resolvers.federation.mappers.spec.ts | 4 +- .../tests/ts-resolvers.federation.spec.ts | 194 +++++++++++++--- .../utils/plugins-helpers/src/federation.ts | 216 ++++++++++++------ 6 files changed, 355 insertions(+), 117 deletions(-) diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index b9ce39f9bb8..063a7b496c5 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -154,7 +154,13 @@ export type ResolversParentTypes = { ID: Scalars['ID']['output']; Lines: Lines; Query: {}; - User: User; + User: + | User + | ({ __typename: 'User' } & ( + | GraphQLRecursivePick + | GraphQLRecursivePick + ) & + GraphQLRecursivePick); Int: Scalars['Int']['output']; Boolean: Scalars['Boolean']['output']; }; @@ -197,15 +203,6 @@ export type UserResolvers< > = { __resolveReference?: ReferenceResolver< Maybe, - { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ), - ContextType - >; - - email?: Resolver< - ResolversTypes['String'], { __typename: 'User' } & ( | GraphQLRecursivePick | GraphQLRecursivePick @@ -213,6 +210,8 @@ export type UserResolvers< GraphQLRecursivePick, ContextType >; + + email?: Resolver; }; export type Resolvers = { 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 9e9144a70a2..d44f422058c 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 @@ -803,6 +803,7 @@ export class BaseResolversVisitor< clearWrapper: type => this.clearResolverTypeWrapper(type), getTypeToUse: name => this.getTypeToUse(name), currentType: 'ResolversTypes', + onNotMappedObjectType: ({ initialType }) => initialType, }); this._resolversParentTypes = this.createResolversFields({ applyWrapper: type => type, @@ -810,6 +811,17 @@ export class BaseResolversVisitor< getTypeToUse: name => this.getParentTypeToUse(name), currentType: 'ResolversParentTypes', shouldInclude: namedType => !isEnumType(namedType), + onNotMappedObjectType: ({ typeName, initialType }) => { + let result = initialType; + const federationReferenceTypes = this._federation.printReferenceSelectionSets({ + typeName, + baseFederationType: `${this.convertName('FederationTypes')}['${typeName}']`, + }); + if (federationReferenceTypes) { + result += ` | ${federationReferenceTypes}`; + } + return result; + }, }); this._resolversUnionTypes = this.createResolversUnionTypes(); this._resolversInterfaceTypes = this.createResolversInterfaceTypes(); @@ -882,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)); @@ -976,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); 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 index b71c937b241..9af1da357ae 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -147,8 +147,12 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { Query: {}; Person: ResolversInterfaceTypes['Person']; ID: Scalars['ID']['output']; - User: User; - Admin: Admin; + User: User | + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Admin: Admin | + ( { __typename: 'Admin' } + & GraphQLRecursivePick ); Boolean: Scalars['Boolean']['output']; PersonName: PersonName; String: Scalars['String']['output']; @@ -160,18 +164,24 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { export type PersonResolvers = { __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - __resolveReference?: ReferenceResolver, { __typename: 'Person' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'Person' } + & GraphQLRecursivePick ), ContextType>; }; export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; id?: Resolver; name?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; export type AdminResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'Admin' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'Admin' } + & GraphQLRecursivePick ), ContextType>; id?: Resolver; name?: Resolver; canImpersonate?: Resolver; 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 index 93710cce5f7..905ebc924fb 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -144,7 +144,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { }; export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; }; 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 fb73dc0d613..57f1c665bec 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -83,10 +83,36 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationTypes = { + User: User; + SingleResolvable: SingleResolvable; + AtLeastOneResolvable: AtLeastOneResolvable; + MixedResolvable: MixedResolvable; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | ( { __typename: 'User' } & GraphQLRecursivePick ); + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + Book: Book; + SingleResolvable: SingleResolvable | ( { __typename: 'SingleResolvable' } & GraphQLRecursivePick ); + SingleNonResolvable: SingleNonResolvable; + AtLeastOneResolvable: AtLeastOneResolvable | ( { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick ); + MixedResolvable: MixedResolvable | ( { __typename: 'MixedResolvable' } & ( GraphQLRecursivePick | GraphQLRecursivePick ) ); + 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>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } & GraphQLRecursivePick ), ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; @@ -96,7 +122,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // SingleResolvable has __resolveReference because it has resolvable: true expect(content).toBeSimilarStringTo(` export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'SingleResolvable' } + & GraphQLRecursivePick ), ContextType>; id?: Resolver; }; `); @@ -111,7 +139,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // AtLeastOneResolvable has __resolveReference because it at least one resolvable expect(content).toBeSimilarStringTo(` export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick ), ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -121,7 +150,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // MixedResolvable has __resolveReference and references for resolvable keys expect(content).toBeSimilarStringTo(` export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'MixedResolvable' } + & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; id?: Resolver; id2?: Resolver; id3?: Resolver; @@ -171,11 +202,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // User should have it expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, ( { __typename: 'User' } & GraphQLRecursivePick ), ContextType>; `); // Foo shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, ( { __typename: 'Book' } & GraphQLRecursivePick ), ContextType>; `); }); @@ -212,17 +243,21 @@ 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>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; } `); expect(content).toBeSimilarStringTo(` export type NameResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'Name' } & GraphQLRecursivePick, ContextType>; - first?: Resolver, ContextType>; - last?: Resolver, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'Name' } + & GraphQLRecursivePick ), ContextType>; + first?: Resolver; + last?: Resolver; } `); }); @@ -248,12 +283,26 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + Int: Scalars['Int']['output']; + Boolean: Scalars['Boolean']['output']; + }; + `); + // 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>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick + & GraphQLRecursivePick ), ContextType>; + id?: Resolver; + username?: Resolver, ParentType, ContextType>; }; `); }); @@ -285,10 +334,25 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + 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, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick + & GraphQLRecursivePick ), ContextType>; + username?: Resolver, ParentType, ContextType>; }; `); }); @@ -317,10 +381,82 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | + ( { __typename: 'User' } + & GraphQLRecursivePick ); + String: Scalars['String']['output']; + Name: Name; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; + username?: Resolver, ParentType, ContextType>; + }; + `); + }); + + it('handles a mix of @key and @requires directives', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + users: [User] + } + + 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 LegacyId { + oldId1: ID! @external + oldId2: ID! @external + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, + }); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | + ( { __typename: 'User' } + & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) + & GraphQLRecursivePick + & GraphQLRecursivePick ); + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + LegacyId: LegacyId; + Boolean: Scalars['Boolean']['output']; + }; + `); + expect(content).toBeSimilarStringTo(` export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - username?: Resolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) + & GraphQLRecursivePick + & GraphQLRecursivePick ), ContextType>; + id?: Resolver; + uuid?: Resolver; + username?: Resolver; + usernameLegacy?: Resolver; }; `); }); @@ -351,7 +487,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; name?: Resolver; username?: Resolver, ParentType, ContextType>; }; @@ -385,9 +523,11 @@ 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>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & GraphQLRecursivePick ), ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; }; `); }); @@ -511,9 +651,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // 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>; + __resolveReference?: ReferenceResolver, + ( { __typename: 'User' } + & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; }; `); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 3ac608bf604..4c6ce6ee5ef 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -3,6 +3,7 @@ import { DefinitionNode, DirectiveNode, FieldDefinitionNode, + GraphQLFieldConfigMap, GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, @@ -14,7 +15,6 @@ import { parse, StringValueNode, } from 'graphql'; -import merge from 'lodash/merge.js'; import { oldVisit } from './index.js'; import { getBaseType } from './utils.js'; @@ -30,9 +30,31 @@ export const federationSpec = parse(/* GraphQL */ ` directive @key(fields: _FieldSet!) on OBJECT | INTERFACE `); +/** + * 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` + */ +interface ReferenceSelectionSet { + name: string; + selection: boolean | ReferenceSelectionSet[]; +} + 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: ReferenceSelectionSet[][]; } export type FederationMeta = { [typeName: string]: TypeMeta }; @@ -57,7 +79,41 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { typeName: string; update: TypeMeta; }): void => { - meta[typeName] = { ...(meta[typeName] || { hasResolveReference: false, resolvableKeyDirectives: [] }), ...update }; + meta[typeName] = { + ...(meta[typeName] || + ({ + hasResolveReference: false, + resolvableKeyDirectives: [], + referenceSelectionSets: [], + } satisfies TypeMeta)), + ...update, + }; + }; + + const getReferenceSelectionSets = ({ + resolvableKeyDirectives, + fields, + }: { + resolvableKeyDirectives: readonly DirectiveNode[]; + fields: GraphQLFieldConfigMap; + }): TypeMeta['referenceSelectionSets'] => { + const referenceSelectionSets: ReferenceSelectionSet[][] = []; + + // @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([...primaryKeys]); + + 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); + referenceSelectionSets.push([requires]); + } + } + + return referenceSelectionSets; }; const federationMeta: FederationMeta = {}; @@ -74,12 +130,18 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { ...typeConfig.fields, }; + const referenceSelectionSets = getReferenceSelectionSets({ + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + fields: typeConfig.fields, + }); + setFederationMeta({ meta: federationMeta, typeName: type.name, update: { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + referenceSelectionSets, }, }); @@ -92,6 +154,12 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { const federationDetails = checkTypeFederationDetails(type, schema); if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { const typeConfig = type.toConfig(); + + const referenceSelectionSets = getReferenceSelectionSets({ + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + fields: typeConfig.fields, + }); + typeConfig.fields = { [resolveReferenceFieldName]: { type, @@ -105,6 +173,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { update: { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + referenceSelectionSets, }, }); @@ -219,7 +288,6 @@ export class ApolloFederation { /** * Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation - * @param data */ transformFieldParentType({ fieldNode, @@ -231,49 +299,29 @@ export class ApolloFederation { parentType: GraphQLNamedType; parentTypeSignature: string; federationTypeSignature: string; - }) { + }): string { if (!this.enabled) { return parentTypeSignature; } - const parentTypeMeta = this.getMeta()[parentType.name]; - if (!parentTypeMeta?.hasResolveReference) { - return parentTypeSignature; - } - - const isObjectFieldWithFederationRef = - isObjectType(parentType) && - (nodeHasTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName); - - const isInterfaceFieldWithFederationRef = - isInterfaceType(parentType) && fieldNode.name.value === resolveReferenceFieldName; + const result = this.printReferenceSelectionSets({ + typeName: parentType.name, + baseFederationType: federationTypeSignature, + }); - if (!isObjectFieldWithFederationRef && !isInterfaceFieldWithFederationRef) { + // When `!result`, it means this is not a Federation entity, so we just return the parentTypeSignature + if (!result) { return parentTypeSignature; } - const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; - - // 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), federationTypeSignature); + const isEntityResolveReferenceField = + (isObjectType(parentType) || isInterfaceType(parentType)) && fieldNode.name.value === resolveReferenceFieldName; - // @key() @key() - "primary keys" in Federation - const primaryKeys = parentTypeMeta.resolvableKeyDirectives.map(def => { - const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, federationTypeSignature); - }); - - const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; - - outputs.push([open, primaryKeys.join(' | '), close].join('')); - - // include required fields - if (requires.length) { - outputs.push(`& ${requiredFields}`); + if (!isEntityResolveReferenceField) { + return parentTypeSignature; } - return outputs.join(' '); + return result; } addFederationTypeGenericIfApplicable({ @@ -315,41 +363,41 @@ export class ApolloFederation { return false; } - private translateFieldSet(fields: any, parentTypeRef: string): string { - return `GraphQLRecursivePick<${parentTypeRef}, ${JSON.stringify(fields)}>`; + printReferenceSelectionSet({ + typeName, + referenceSelectionSet, + }: { + typeName: string; + referenceSelectionSet: ReferenceSelectionSet; + }): string { + return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`; } - private extractFieldSet(directive: DirectiveNode): any { - const arg = directive.arguments.find(arg => arg.name.value === 'fields'); - const { value } = arg.value as StringValueNode; + printReferenceSelectionSets({ + typeName, + baseFederationType, + }: { + typeName: string; + baseFederationType: string; + }): string | false { + const federationMeta = this.getMeta()[typeName]; - type SelectionSetField = { - name: string; - selection: boolean | SelectionSetField[]; - }; + if (!federationMeta?.hasResolveReference) { + return false; + } - 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; - }, - }, - }); + return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets + .map(referenceSelectionSetArray => { + const result = referenceSelectionSetArray.map(referenceSelectionSet => { + return this.printReferenceSelectionSet({ + referenceSelectionSet, + typeName: baseFederationType, + }); + }); + + return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | '); + }) + .join('\n & ')} )`; } private createMapOfProvides() { @@ -361,7 +409,7 @@ export class ApolloFederation { if (isObjectType(objectType)) { for (const field of Object.values(objectType.getFields())) { const provides = getDirectivesByName('provides', field.astNode) - .map(this.extractFieldSet) + .map(extractReferenceSelectionSet) .reduce((prev, curr) => [...prev, ...Object.keys(curr)], []); const ofType = getBaseType(field.type); @@ -436,12 +484,30 @@ function getDirectivesByName( return astNode?.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 nodeHasTypeExtension(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 ReferenceSelectionSet[]).reduce((accum, field) => { + accum[field.name] = field.selection; + return accum; + }, {}); + }, + Field(node) { + return { + name: node.name.value, + selection: node.selectionSet || true, + } as ReferenceSelectionSet; + }, + Document(node) { + return node.definitions.find( + (def: DefinitionNode): def is OperationDefinitionNode => + def.kind === 'OperationDefinition' && def.operation === 'query' + ).selectionSet; + }, + }, + }); } From f202faca761c16e6a48f4cb0971b56ef84a16797 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Mar 2025 09:52:38 +0000 Subject: [PATCH 07/11] chore(dependencies): updated changesets for modified dependencies --- .../@graphql-codegen_client-preset-10218-dependencies.md | 9 +++++++++ ...phql-codegen_gql-tag-operations-10218-dependencies.md | 5 +++++ ...-codegen_graphql-modules-preset-10218-dependencies.md | 5 +++++ ...hql-codegen_typed-document-node-10218-dependencies.md | 5 +++++ .../@graphql-codegen_typescript-10218-dependencies.md | 5 +++++ ...degen_typescript-document-nodes-10218-dependencies.md | 5 +++++ ...l-codegen_typescript-operations-10218-dependencies.md | 6 ++++++ ...ql-codegen_typescript-resolvers-10218-dependencies.md | 6 ++++++ 8 files changed, 46 insertions(+) create mode 100644 .changeset/@graphql-codegen_client-preset-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typed-document-node-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-operations-10218-dependencies.md create mode 100644 .changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md diff --git a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md new file mode 100644 index 00000000000..b79994dc2c4 --- /dev/null +++ b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md @@ -0,0 +1,9 @@ +--- +"@graphql-codegen/client-preset": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typed-document-node@^5.0.15` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typed-document-node/v/5.0.15) (from `^5.0.13`, in `dependencies`) + - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) + - Updated dependency [`@graphql-codegen/typescript-operations@^4.5.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript-operations/v/4.5.1) (from `^4.4.1`, in `dependencies`) + - Updated dependency [`@graphql-codegen/gql-tag-operations@4.0.16` ↗︎](https://www.npmjs.com/package/@graphql-codegen/gql-tag-operations/v/4.0.16) (from `4.0.14`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@^5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `^5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md new file mode 100644 index 00000000000..685f15c5d0e --- /dev/null +++ b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/gql-tag-operations": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md new file mode 100644 index 00000000000..5321f1579a1 --- /dev/null +++ b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/graphql-modules-preset": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md new file mode 100644 index 00000000000..ecb175ca134 --- /dev/null +++ b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typed-document-node": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-10218-dependencies.md new file mode 100644 index 00000000000..2ca02ff15a9 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typescript": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md new file mode 100644 index 00000000000..2d3505a26dd --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typescript-document-nodes": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md new file mode 100644 index 00000000000..acb3f62f3a9 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-codegen/typescript-operations": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md new file mode 100644 index 00000000000..0e8793cf314 --- /dev/null +++ b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-codegen/typescript-resolvers": patch +--- +dependencies updates: + - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) + - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) From 3f5011d61a6d71af6b13cd72e9008c330c4a7ca5 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 5 Mar 2025 21:04:54 +1100 Subject: [PATCH 08/11] Add missing changeset --- .changeset/lovely-snails-travel.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/lovely-snails-travel.md 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. From 30ac8930f49f44ab10e6b7dcdd7f12a67bfa9fc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Mar 2025 21:36:59 +0000 Subject: [PATCH 09/11] chore(dependencies): updated changesets for modified dependencies --- .../@graphql-codegen_client-preset-10218-dependencies.md | 9 --------- ...phql-codegen_gql-tag-operations-10218-dependencies.md | 5 ----- ...-codegen_graphql-modules-preset-10218-dependencies.md | 5 ----- ...hql-codegen_typed-document-node-10218-dependencies.md | 5 ----- .../@graphql-codegen_typescript-10218-dependencies.md | 5 ----- ...degen_typescript-document-nodes-10218-dependencies.md | 5 ----- ...l-codegen_typescript-operations-10218-dependencies.md | 6 ------ ...ql-codegen_typescript-resolvers-10218-dependencies.md | 6 ------ 8 files changed, 46 deletions(-) delete mode 100644 .changeset/@graphql-codegen_client-preset-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typed-document-node-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-operations-10218-dependencies.md delete mode 100644 .changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md diff --git a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md b/.changeset/@graphql-codegen_client-preset-10218-dependencies.md deleted file mode 100644 index b79994dc2c4..00000000000 --- a/.changeset/@graphql-codegen_client-preset-10218-dependencies.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@graphql-codegen/client-preset": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typed-document-node@^5.0.15` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typed-document-node/v/5.0.15) (from `^5.0.13`, in `dependencies`) - - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) - - Updated dependency [`@graphql-codegen/typescript-operations@^4.5.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript-operations/v/4.5.1) (from `^4.4.1`, in `dependencies`) - - Updated dependency [`@graphql-codegen/gql-tag-operations@4.0.16` ↗︎](https://www.npmjs.com/package/@graphql-codegen/gql-tag-operations/v/4.0.16) (from `4.0.14`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@^5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `^5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md b/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md deleted file mode 100644 index 685f15c5d0e..00000000000 --- a/.changeset/@graphql-codegen_gql-tag-operations-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/gql-tag-operations": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md b/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md deleted file mode 100644 index 5321f1579a1..00000000000 --- a/.changeset/@graphql-codegen_graphql-modules-preset-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/graphql-modules-preset": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md b/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md deleted file mode 100644 index ecb175ca134..00000000000 --- a/.changeset/@graphql-codegen_typed-document-node-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typed-document-node": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-10218-dependencies.md deleted file mode 100644 index 2ca02ff15a9..00000000000 --- a/.changeset/@graphql-codegen_typescript-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typescript": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md deleted file mode 100644 index 2d3505a26dd..00000000000 --- a/.changeset/@graphql-codegen_typescript-document-nodes-10218-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-codegen/typescript-document-nodes": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md deleted file mode 100644 index acb3f62f3a9..00000000000 --- a/.changeset/@graphql-codegen_typescript-operations-10218-dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@graphql-codegen/typescript-operations": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) diff --git a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md b/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md deleted file mode 100644 index 0e8793cf314..00000000000 --- a/.changeset/@graphql-codegen_typescript-resolvers-10218-dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@graphql-codegen/typescript-resolvers": patch ---- -dependencies updates: - - Updated dependency [`@graphql-codegen/typescript@^4.1.5` ↗︎](https://www.npmjs.com/package/@graphql-codegen/typescript/v/4.1.5) (from `^4.1.3`, in `dependencies`) - - Updated dependency [`@graphql-codegen/visitor-plugin-common@5.7.1` ↗︎](https://www.npmjs.com/package/@graphql-codegen/visitor-plugin-common/v/5.7.1) (from `5.6.1`, in `dependencies`) From b446e84383ba57bc9f768b55db1e0ccf958a27c3 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 22 May 2025 21:09:10 +1000 Subject: [PATCH 10/11] [resolvers][federation] Fix fields or types being wrong generated when marked with @external (#10287) * Ensure @external does not generate resolver types - Handle external directive when part or whole type is marked - Add changeset - Add test cases for @provides and @external * Format and minor text updates * Add comments * Fix __resolveReference not getting generated * Fix test with __isTypeOf when not needed * Fix type cast that results in wrong type * Revert unncessary changes to FieldDefinitionPrintFn * Re-format * Convert to use AST Node instead of GraphQL Type * Update test template * Cache field nodes to generate for processed objects * Put FIXME on base-resolvers-visitor to remove Name method --- .changeset/twenty-planets-complain.md | 7 + dev-test/test-schema/resolvers-federation.ts | 1 - .../src/base-resolvers-visitor.ts | 260 ++++++++++-------- ...-resolvers.config.customDirectives.spec.ts | 1 - .../tests/ts-resolvers.federation.spec.ts | 55 +++- .../utils/plugins-helpers/src/federation.ts | 122 +++++--- 6 files changed, 294 insertions(+), 152 deletions(-) create mode 100644 .changeset/twenty-planets-complain.md 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/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index 063a7b496c5..3583b2d7cae 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -210,7 +210,6 @@ export type UserResolvers< GraphQLRecursivePick, ContextType >; - email?: Resolver; }; 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 d44f422058c..d447d4048ed 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 @@ -82,8 +82,13 @@ export interface ParsedResolversConfig extends ParsedConfig { avoidCheckingAbstractTypesRecursively: boolean; } +export interface FieldDefinitionResult { + node: FieldDefinitionNode; + printContent: FieldDefinitionPrintFn; +} + type FieldDefinitionPrintFn = ( - parentName: string, + parentNode: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, avoidResolverOptionals: boolean ) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } }; export interface RootResolver { @@ -1463,6 +1468,9 @@ export class BaseResolversVisitor< return ''; } + // FIXME: this Name method causes a lot of type inconsistencies + // because the type of nodes no longer matches the `graphql-js` types + // So, we should update this and remove any relevant `as any as string` or `as unknown as string` Name(node: NameNode): string { return node.value; } @@ -1519,122 +1527,133 @@ 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); - const meta: ReturnType['meta'] = {}; + const original: FieldDefinitionNode = parent[key]; - if (this._federation.skipField({ fieldNode: original, parentType })) { - return { value: null, meta }; - } + return { + node: original, + printContent: (parentNode, avoidResolverOptionals) => { + const parentName = parentNode.name as unknown as string; + const parentType = this.schema.getType(parentName); + const meta: ReturnType['meta'] = {}; + const typeName = node.name as unknown as string; + + 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 }; + } - 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 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); + } + } - const avoidInputsOptionals = this.config.avoidOptionals.inputValue; + const parentTypeSignature = this._federation.transformFieldParentType({ + fieldNode: original, + parentType, + parentTypeSignature: this.getParentTypeForSignature(node), + federationTypeSignature: 'FederationType', + }); - if (argsType !== null) { - const argsToForceRequire = original.arguments.filter( - arg => !!arg.defaultValue || arg.type.kind === 'NonNullType' - ); + 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', + }; + } - if (argsToForceRequire.length > 0) { - argsType = this.applyRequireFields(argsType, argsToForceRequire); - } else if (original.arguments.length > 0 && avoidInputsOptionals !== true) { - argsType = this.applyOptionalFields(argsType, original.arguments); - } - } + const directiveMappings = + node.directives + ?.map(directive => this._directiveResolverMappings[directive.name as any]) + .filter(Boolean) + .reverse() ?? []; - const parentTypeSignature = this._federation.transformFieldParentType({ - fieldNode: original, - parentType, - parentTypeSignature: this.getParentTypeForSignature(node), - federationTypeSignature: 'FederationType', - }); - - 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 signature: { + name: string; + modifier: string; + type: string; + genericTypes: string[]; + } = { + name: typeName, + modifier: avoidResolverOptionals ? '' : '?', + type: resolverType, + genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f), + }; - const directiveMappings = - node.directives - ?.map(directive => this._directiveResolverMappings[directive.name as any]) - .filter(Boolean) - .reverse() ?? []; + if (this._federation.isResolveReferenceField(node)) { + if (!this._federation.getMeta()[parentType.name].hasResolveReference) { + return { value: '', meta }; + } + signature.type = 'ReferenceResolver'; + signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType]; + meta.federation = { isResolveReference: true }; + } return { - mappedTypeKey: mappedType, - resolverType: directiveMappings[0] ?? 'Resolver', + value: indent( + `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( + ', ' + )}>${this.getPunctuation(declarationKind)}` + ), + meta, }; - })(); - - 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._federation.getMeta()[parentType.name].hasResolveReference) { - return { value: '', meta }; - } - signature.type = 'ReferenceResolver'; - signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType]; - meta.federation = { isResolveReference: true }; - } - - return { - value: indent( - `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( - ', ' - )}>${this.getPunctuation(declarationKind)}` - ), - meta, - }; + }, }; } @@ -1707,12 +1726,17 @@ export class BaseResolversVisitor< return `Partial<${argsType}>`; } - ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string { + ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | null { + const typeName = node.name as unknown as string; + 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' => { @@ -1728,15 +1752,17 @@ 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) - ).value; - }); + 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 && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) { fieldsContent.push( @@ -1748,6 +1774,10 @@ export class BaseResolversVisitor< ); } + if (fieldsContent.length === 0) { + return null; + } + const genericTypes: string[] = [ `ContextType = ${this.config.contextType.type}`, this.transformParentGenericType(parentType), @@ -1958,8 +1988,8 @@ export class BaseResolversVisitor< // 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 FieldDefinitionPrintFn[]).map(f => - f(typeName, this.config.avoidOptionals.resolvers) + 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) { 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.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts index 57f1c665bec..ba763bdc34c 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -496,7 +496,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); }); - 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] @@ -504,12 +504,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! } `; @@ -520,7 +546,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); - // UserResolver should not have a resolver function of name field + // `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, @@ -528,8 +555,32 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { & GraphQLRecursivePick ), 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 () => { diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 4c6ce6ee5ef..2afd3aae3b6 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,4 +1,5 @@ import { astFromInterfaceType, astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; +import type { FieldDefinitionResult } from '@graphql-codegen/visitor-plugin-common'; import { DefinitionNode, DirectiveNode, @@ -8,6 +9,7 @@ import { GraphQLNamedType, GraphQLObjectType, GraphQLSchema, + InterfaceTypeDefinitionNode, isInterfaceType, isObjectType, ObjectTypeDefinitionNode, @@ -224,12 +226,18 @@ export class ApolloFederation { private enabled = false; private schema: GraphQLSchema; private providesMap: Record; + /** + * `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, meta }: { enabled: boolean; schema: GraphQLSchema; meta: FederationMeta }) { this.enabled = enabled; this.schema = schema; this.providesMap = this.createMapOfProvides(); + this.fieldsToGenerate = {}; this.meta = meta; } @@ -266,19 +274,64 @@ 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) && !isInterfaceType(parentType)) || - !checkTypeFederationDetails(parentType, this.schema) - ) { - return false; + findFieldNodesToGenerate({ + node, + }: { + node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; + }): readonly FieldDefinitionNode[] { + const nodeName = node.name as unknown as string; + if (this.fieldsToGenerate[nodeName]) { + return this.fieldsToGenerate[nodeName]; + } + + const fieldNodes = ((node.fields || []) as unknown as FieldDefinitionResult[]).map(field => field.node); + + if (!this.enabled) { + return fieldNodes; + } + + // 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; + }, []); + + this.fieldsToGenerate[nodeName] = fieldNodesWithProvides; + + return fieldNodesWithProvides; } - return this.isExternalAndNotProvided(fieldNode, parentType); + // 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; + } + + if (this.isExternal(fieldNode) && this.hasProvides(node, fieldNode.name.value)) { + acc.push(fieldNode); + return acc; + } + + return acc; + }, []); + + this.fieldsToGenerate[nodeName] = fieldNodesWithoutExternalOrHasProvides; + + return fieldNodesWithoutExternalOrHasProvides; } isResolveReferenceField(fieldNode: FieldDefinitionNode): boolean { @@ -345,19 +398,15 @@ export class ApolloFederation { 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 as unknown as string]; if (fields?.length) { - return fields.includes(node.name.value); + return fields.includes(fieldName); } return false; @@ -410,7 +459,7 @@ export class ApolloFederation { for (const field of Object.values(objectType.getFields())) { const provides = getDirectivesByName('provides', field.astNode) .map(extractReferenceSelectionSet) - .reduce((prev, curr) => [...prev, ...Object.keys(curr)], []); + .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] ||= []; @@ -429,22 +478,22 @@ export class ApolloFederation { * @param node Type */ function checkTypeFederationDetails( - node: ObjectTypeDefinitionNode | GraphQLObjectType | GraphQLInterfaceType, + typeOrNode: ObjectTypeDefinitionNode | GraphQLObjectType | InterfaceTypeDefinitionNode | GraphQLInterfaceType, schema: GraphQLSchema ): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { - const { - name: { value: name }, - directives, - } = isObjectType(node) - ? astFromObjectType(node, schema) - : isInterfaceType(node) - ? astFromInterfaceType(node, schema) - : node; + const node = isObjectType(typeOrNode) + ? astFromObjectType(typeOrNode, schema) + : isInterfaceType(typeOrNode) + ? astFromInterfaceType(typeOrNode, schema) + : typeOrNode; + + const name = node.name.value || (typeOrNode.name as unknown as string); + const directives = node.directives; const rootTypeNames = getRootTypeNames(schema); const isNotRoot = !rootTypeNames.has(name); const isNotIntrospection = !name.startsWith('__'); - const keyDirectives = directives.filter(d => d.name.value === 'key'); + const keyDirectives = directives.filter(d => d.name.value === 'key' || (d.name as unknown as string) === 'key'); const check = isNotRoot && isNotIntrospection && keyDirectives.length > 0; @@ -471,17 +520,24 @@ function checkTypeFederationDetails( */ function getDirectivesByName( name: string, - node: ObjectTypeDefinitionNode | GraphQLObjectType | FieldDefinitionNode + node: ObjectTypeDefinitionNode | GraphQLObjectType | FieldDefinitionNode | InterfaceTypeDefinitionNode ): readonly DirectiveNode[] { - let astNode: ObjectTypeDefinitionNode | FieldDefinitionNode; + let astNode: ObjectTypeDefinitionNode | FieldDefinitionNode | InterfaceTypeDefinitionNode; - if (isObjectType(node)) { + if (isObjectType(node) || isInterfaceType(node)) { astNode = node.astNode; } else { astNode = node; } - return astNode?.directives?.filter(d => d.name.value === name) || []; + return ( + astNode?.directives?.filter(d => { + // A ObjectTypeDefinitionNode's directive looks like `{ kind: 'Directive', name: 'external', arguments: [] }` + // However, other directives looks like `{ kind: 'Directive', name: { kind: 'Name', value: 'external' }, arguments: [] }` + // Therefore, we need to check for both `d.name.value` and d.name + return d.name.value === name || (d.name as unknown as string) === name; + }) || [] + ); } function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelectionSet { From 5e08c758900167a8eaee6ff178599c2b990bf21d Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 29 May 2025 22:09:12 +1000 Subject: [PATCH 11/11] Fix unit test --- .../tests/ts-resolvers.federation.interface.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 9af1da357ae..d1e08bd54c1 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -127,7 +127,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { /** Mapping of interface types */ export type ResolversInterfaceTypes<_RefType extends Record> = { - Person: ( User ) | ( Admin ); + Person: + | ( User ) + | ( Admin ) + ; }; /** Mapping between all available schema types and the resolvers types */