From 80635c3d1c11e02ad8f64a12feb7adc17842f0db Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 4 Jun 2025 23:11:36 +1000 Subject: [PATCH 01/10] Update test setup --- .../tests/ts-resolvers.federation.spec.ts | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) 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 ba763bdc34c..bce3b770485 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -270,9 +270,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { type User @key(fields: "id") { id: ID! + name: String @external age: Int! @external username: String @requires(fields: "name age") + + publicName: String! @requires(fields: "name") + + birthDay: String! @external + birthMonth: String! @external + birthYear: String! @external + birthDate: String! @requires(fields: "birthDay birthMonth birthYear") } `; @@ -286,7 +294,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + User: User | + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); ID: Scalars['ID']['output']; String: Scalars['String']['output']; Int: Scalars['Int']['output']; @@ -300,9 +317,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { __resolveReference?: ReferenceResolver, ( { __typename: 'User' } & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ), ContextType>; id?: Resolver; username?: Resolver, ParentType, ContextType>; + publicName?: Resolver; + birthDate?: Resolver; }; `); }); @@ -315,6 +340,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { extend type User @key(fields: "id") { id: ID! @external + + favouriteColor: String! @external + favouriteColorHex: String! @requires(fields: "favouriteColor") + name: String @external age: Int! @external address: Address! @external @@ -337,7 +366,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + User: User | + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); ID: Scalars['ID']['output']; String: Scalars['String']['output']; Int: Scalars['Int']['output']; @@ -350,8 +385,12 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { export type UserResolvers = { __resolveReference?: ReferenceResolver, ( { __typename: 'User' } - & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ), ContextType>; + favouriteColorHex?: Resolver; username?: Resolver, ParentType, ContextType>; }; `); From 5c4ab1af97100dcbf3e0936f70fe711afff04be4 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 5 Jun 2025 00:20:35 +1000 Subject: [PATCH 02/10] Implement @requires combination --- .../utils/plugins-helpers/src/federation.ts | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 2afd3aae3b6..0065726afd3 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -38,12 +38,15 @@ export const federationSpec = parse(/* GraphQL */ ` * @example * - resolvable fields marked with `@key` * - fields declared in `@provides` + * - fields declared in `@requires` */ -interface ReferenceSelectionSet { +interface DirectiveSelectionSet { name: string; - selection: boolean | ReferenceSelectionSet[]; + selection: boolean | DirectiveSelectionSet[]; } +type ReferenceSelectionSet = Record; // TODO: handle nested + interface TypeMeta { hasResolveReference: boolean; resolvableKeyDirectives: readonly DirectiveNode[]; @@ -56,7 +59,7 @@ interface TypeMeta { * - [[A, B], [C], [D]] -> (A | B) & C & D * - [[A, B], [C, D], [E]] -> (A | B) & (C | D) & E */ - referenceSelectionSets: ReferenceSelectionSet[][]; + referenceSelectionSets: { directive: '@key' | '@requires'; selectionSets: ReferenceSelectionSet[] }[]; } export type FederationMeta = { [typeName: string]: TypeMeta }; @@ -99,21 +102,23 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { resolvableKeyDirectives: readonly DirectiveNode[]; fields: GraphQLFieldConfigMap; }): TypeMeta['referenceSelectionSets'] => { - const referenceSelectionSets: ReferenceSelectionSet[][] = []; + const referenceSelectionSets: TypeMeta['referenceSelectionSets'] = []; // @key() @key() - "primary keys" in Federation // A reference may receive one primary key combination at a time, so they will be combined with `|` const primaryKeys = resolvableKeyDirectives.map(extractReferenceSelectionSet); - referenceSelectionSets.push([...primaryKeys]); + referenceSelectionSets.push({ directive: '@key', selectionSets: [...primaryKeys] }); + const requiresPossibleTypes: ReferenceSelectionSet[] = []; for (const fieldNode of Object.values(fields)) { // Look for @requires and see what the service needs and gets const directives = getDirectivesByName('requires', fieldNode.astNode); for (const directive of directives) { const requires = extractReferenceSelectionSet(directive); - referenceSelectionSets.push([requires]); + requiresPossibleTypes.push(requires); } } + referenceSelectionSets.push({ directive: '@requires', selectionSets: requiresPossibleTypes }); return referenceSelectionSets; }; @@ -436,15 +441,26 @@ export class ApolloFederation { } return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets - .map(referenceSelectionSetArray => { - const result = referenceSelectionSetArray.map(referenceSelectionSet => { - return this.printReferenceSelectionSet({ - referenceSelectionSet, - typeName: baseFederationType, - }); - }); + .map(({ directive, selectionSets: originalSelectionSets }) => { + const result: string[] = []; + + let selectionSets = originalSelectionSets; + if (directive === '@requires') { + result.push('{}'); + selectionSets = []; + findAllCombinations(originalSelectionSets, selectionSets); + } - return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | '); + for (const referenceSelectionSet of selectionSets) { + result.push( + this.printReferenceSelectionSet({ + referenceSelectionSet, + typeName: baseFederationType, + }) + ); + } + + return result.length > 1 ? `( ${result.join('\n | ')} )` : result.join(' | '); }) .join('\n & ')} )`; } @@ -547,7 +563,7 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect return oldVisit(parse(`{${value}}`), { leave: { SelectionSet(node) { - return (node.selections as any as ReferenceSelectionSet[]).reduce((accum, field) => { + return (node.selections as any as DirectiveSelectionSet[]).reduce((accum, field) => { accum[field.name] = field.selection; return accum; }, {}); @@ -556,7 +572,7 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect return { name: node.name.value, selection: node.selectionSet || true, - } as ReferenceSelectionSet; + } as DirectiveSelectionSet; }, Document(node) { return node.definitions.find( @@ -567,3 +583,16 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect }, }); } + +function findAllCombinations(selectionSets: ReferenceSelectionSet[], result: ReferenceSelectionSet[]): void { + const [currentSelectionSet, ...rest] = selectionSets; + + result.push(currentSelectionSet); + for (const selectionSet of rest) { + result.push({ ...currentSelectionSet, ...selectionSet }); + } + + if (rest.length > 0) { + findAllCombinations(rest, result); + } +} From b6656865cbda1090b950cf9e94e7fbdbd671e297 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 5 Jun 2025 22:31:04 +1000 Subject: [PATCH 03/10] Add changeset --- .changeset/small-fans-cross.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/small-fans-cross.md diff --git a/.changeset/small-fans-cross.md b/.changeset/small-fans-cross.md new file mode 100644 index 00000000000..729306c52a0 --- /dev/null +++ b/.changeset/small-fans-cross.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/plugin-helpers': patch +--- + +Update @requires type From 1e7b946935178b3b974e69e9ee07aba437d33e95 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 5 Jun 2025 23:01:11 +1000 Subject: [PATCH 04/10] Force release alpha --- .github/workflows/pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bf47152690b..e19a2df1579 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - master + - federation-fixes jobs: # dependencies: From abd8276c8c3ea60547bf2b0dabfd58c20bc69065 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sat, 7 Jun 2025 23:08:14 +1000 Subject: [PATCH 05/10] Fix issue with empty array, set up tests --- .../tests/ts-resolvers.federation.spec.ts | 97 +++++++++++++------ .../utils/plugins-helpers/src/federation.ts | 97 ++++++++++++++++--- 2 files changed, 148 insertions(+), 46 deletions(-) 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 bce3b770485..df93cc3a939 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -268,19 +268,25 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { users: [User] } + type Account @key(fields: "id") { + id: ID! + key: String! + } + type User @key(fields: "id") { id: ID! - name: String @external - age: Int! @external - username: String @requires(fields: "name age") + a: String @external + aRequires: String @requires(fields: "a") + + b: String! @external + bRequires: String! @requires(fields: "b") - publicName: String! @requires(fields: "name") + c: String! @external + cRequires: String! @requires(fields: "c") - birthDay: String! @external - birthMonth: String! @external - birthYear: String! @external - birthDate: String! @requires(fields: "birthDay birthMonth birthYear") + d: String! @external + dRequires: String! @requires(fields: "d") } `; @@ -294,19 +300,29 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; + Account: Account | + ( { __typename: 'Account' } + & GraphQLRecursivePick ); + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; User: User | ( { __typename: 'User' } & GraphQLRecursivePick & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ); - ID: Scalars['ID']['output']; - String: Scalars['String']['output']; - Int: Scalars['Int']['output']; + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); Boolean: Scalars['Boolean']['output']; }; `); @@ -318,16 +334,25 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { ( { __typename: 'User' } & GraphQLRecursivePick & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ), ContextType>; + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ), ContextType>; id?: Resolver; - username?: Resolver, ParentType, ContextType>; - publicName?: Resolver; - birthDate?: Resolver; + aRequires?: Resolver, ParentType, ContextType>; + bRequires?: Resolver; + cRequires?: Resolver; + dRequires?: Resolver; }; `); }); @@ -475,9 +500,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { Query: {}; User: User | ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) - & GraphQLRecursivePick - & GraphQLRecursivePick ); + & ( GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); ID: Scalars['ID']['output']; String: Scalars['String']['output']; LegacyId: LegacyId; @@ -489,9 +518,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { export type UserResolvers = { __resolveReference?: ReferenceResolver, ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) - & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + & ( GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ), ContextType>; id?: Resolver; uuid?: Resolver; username?: Resolver; diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 0065726afd3..869e596bd37 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -440,15 +440,17 @@ export class ApolloFederation { return false; } - return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets - .map(({ directive, selectionSets: originalSelectionSets }) => { + const referenceSelectionSetStrings = federationMeta.referenceSelectionSets.reduce( + (acc, { directive, selectionSets: originalSelectionSets }) => { const result: string[] = []; let selectionSets = originalSelectionSets; if (directive === '@requires') { - result.push('{}'); selectionSets = []; - findAllCombinations(originalSelectionSets, selectionSets); + findAllSelectionSetCombinations(originalSelectionSets, selectionSets); + if (selectionSets.length > 0) { + result.push('{}'); + } } for (const referenceSelectionSet of selectionSets) { @@ -460,9 +462,22 @@ export class ApolloFederation { ); } - return result.length > 1 ? `( ${result.join('\n | ')} )` : result.join(' | '); - }) - .join('\n & ')} )`; + if (result.length === 0) { + return acc; + } + + if (result.length === 1) { + acc.push(result.join(' | ')); + return acc; + } + + acc.push(`( ${result.join('\n | ')} )`); + return acc; + }, + [] + ); + + return `\n ( { __typename: '${typeName}' }\n & ${referenceSelectionSetStrings.join('\n & ')} )`; } private createMapOfProvides() { @@ -584,15 +599,69 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect }); } -function findAllCombinations(selectionSets: ReferenceSelectionSet[], result: ReferenceSelectionSet[]): void { - const [currentSelectionSet, ...rest] = selectionSets; +/** + * Function to find all combinations of selection sets and push them into the `result` + * This is used for `@requires` directive because depending on the operation selection set, different + * combination of fields are sent from the router. + * + * @example + * Input: [ + * { a: true }, + * { b: true }, + * { c: true }, + * { d: true}, + * ] + * Output: [ + * { a: true }, + * { a: true, b: true }, + * { a: true, c: true }, + * { a: true, d: true }, + * { a: true, b: true, c: true }, + * { a: true, b: true, d: true }, + * { a: true, c: true, d: true }, + * { a: true, b: true, c: true, d: true } + * + * { b: true }, + * { b: true, c: true }, + * { b: true, d: true }, + * { b: true, c: true, d: true } + * + * { c: true }, + * { c: true, d: true }, + * + * { d: true }, + * ] + * ``` + */ +function findAllSelectionSetCombinations( + selectionSets: ReferenceSelectionSet[], + result: ReferenceSelectionSet[] +): void { + if (selectionSets.length === 0) { + return; + } + + for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { + const base = selectionSets.slice(0, baseIndex + 1); + const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); + + const currentSelectionSet = base.reduce((acc, selectionSet) => { + acc = { ...acc, ...selectionSet }; + return acc; + }, {}); + + if (baseIndex === 0) { + result.push(currentSelectionSet); + } - result.push(currentSelectionSet); - for (const selectionSet of rest) { - result.push({ ...currentSelectionSet, ...selectionSet }); + for (const selectionSet of rest) { + result.push({ ...currentSelectionSet, ...selectionSet }); + } } - if (rest.length > 0) { - findAllCombinations(rest, result); + const next = selectionSets.slice(1, selectionSets.length); + + if (next.length > 0) { + findAllSelectionSetCombinations(next, result); } } From a4ba6e27da3424a2429e5b6d18ef7ef3127597dd Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 9 Jun 2025 23:00:51 +1000 Subject: [PATCH 06/10] Generate FederationReferenceTypes once --- .../src/base-resolvers-visitor.ts | 53 +++- .../plugins/typescript/resolvers/src/index.ts | 2 + .../tests/ts-resolvers.federation.spec.ts | 38 +-- .../utils/plugins-helpers/src/federation.ts | 298 ++++++++---------- 4 files changed, 180 insertions(+), 211 deletions(-) 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 c19beccaf9d..af047911afb 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 @@ -818,12 +818,8 @@ export class BaseResolversVisitor< 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}`; + if (this._federation.getMeta()[typeName]?.referenceSelectionSetsString) { + result += ` | ${this.convertName('FederationReferenceTypes')}['${typeName}']`; } return result; }, @@ -1306,6 +1302,33 @@ export class BaseResolversVisitor< ).string; } + public buildFederationReferenceTypes(): string { + const federationMeta = this._federation.getMeta(); + + if (Object.keys(federationMeta).length === 0) { + return ''; + } + + const declarationKind = 'type'; + return new DeclarationBlock(this._declarationBlockConfig) + .export() + .asKind(declarationKind) + .withName(this.convertName('FederationReferenceTypes')) + .withComment('Mapping of federation reference types') + .withBlock( + Object.entries(federationMeta) + .map(([typeName, { referenceSelectionSetsString }]) => { + if (!referenceSelectionSetsString) { + return undefined; + } + + return indent(`${typeName}: ${referenceSelectionSetsString}${this.getPunctuation(declarationKind)}`); + }) + .filter(v => v) + .join('\n') + ).string; + } + public get schema(): GraphQLSchema { return this._schema; } @@ -1586,13 +1609,6 @@ export class BaseResolversVisitor< } } - 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; @@ -1637,15 +1653,18 @@ export class BaseResolversVisitor< name: typeName, modifier: avoidResolverOptionals ? '' : '?', type: resolverType, - genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f), + genericTypes: [mappedTypeKey, this.getParentTypeForSignature(node), contextType, argsType].filter(f => f), }; if (this._federation.isResolveReferenceField(node)) { if (!this._federation.getMeta()[parentType.name].hasResolveReference) { return { value: '', meta }; } + const resultType = `${mappedTypeKey} | FederationReferenceType`; + const referenceType = 'FederationReferenceType'; + signature.type = 'ReferenceResolver'; - signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType]; + signature.genericTypes = [resultType, referenceType, contextType]; meta.federation = { isResolveReference: true }; } @@ -1788,7 +1807,7 @@ export class BaseResolversVisitor< ]; this._federation.addFederationTypeGenericIfApplicable({ genericTypes, - federationTypesType: this.convertName('FederationTypes'), + federationTypesType: this.convertName('FederationReferenceTypes'), typeName, }); @@ -1975,7 +1994,7 @@ export class BaseResolversVisitor< ]; this._federation.addFederationTypeGenericIfApplicable({ genericTypes, - federationTypesType: this.convertName('FederationTypes'), + federationTypesType: this.convertName('FederationReferenceTypes'), typeName, }); diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 02f6e801b29..0ccb1ce01aa 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -250,6 +250,7 @@ export type DirectiveResolverFn { expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - Account: Account | - ( { __typename: 'Account' } - & GraphQLRecursivePick ); + Account: Account | FederationReferenceTypes['Account']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; - User: User | + User: User | FederationReferenceTypes['User']; + Boolean: Scalars['Boolean']['output']; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick ); + User: ( { __typename: 'User' } & GraphQLRecursivePick & ( {} @@ -323,31 +331,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { | GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) ); - Boolean: Scalars['Boolean']['output']; }; `); // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick - & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; aRequires?: Resolver, ParentType, ContextType>; bRequires?: Resolver; diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 869e596bd37..a29866914a3 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -6,7 +6,6 @@ import { FieldDefinitionNode, GraphQLFieldConfigMap, GraphQLInterfaceType, - GraphQLNamedType, GraphQLObjectType, GraphQLSchema, InterfaceTypeDefinitionNode, @@ -60,6 +59,7 @@ interface TypeMeta { * - [[A, B], [C, D], [E]] -> (A | B) & (C | D) & E */ referenceSelectionSets: { directive: '@key' | '@requires'; selectionSets: ReferenceSelectionSet[] }[]; + referenceSelectionSetsString: string; } export type FederationMeta = { [typeName: string]: TypeMeta }; @@ -90,6 +90,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: false, resolvableKeyDirectives: [], referenceSelectionSets: [], + referenceSelectionSetsString: '', } satisfies TypeMeta)), ...update, }; @@ -123,6 +124,117 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { return referenceSelectionSets; }; + /** + * Function to find all combinations of selection sets and push them into the `result` + * This is used for `@requires` directive because depending on the operation selection set, different + * combination of fields are sent from the router. + * + * @example + * Input: [ + * { a: true }, + * { b: true }, + * { c: true }, + * { d: true}, + * ] + * Output: [ + * { a: true }, + * { a: true, b: true }, + * { a: true, c: true }, + * { a: true, d: true }, + * { a: true, b: true, c: true }, + * { a: true, b: true, d: true }, + * { a: true, c: true, d: true }, + * { a: true, b: true, c: true, d: true } + * + * { b: true }, + * { b: true, c: true }, + * { b: true, d: true }, + * { b: true, c: true, d: true } + * + * { c: true }, + * { c: true, d: true }, + * + * { d: true }, + * ] + * ``` + */ + function findAllSelectionSetCombinations( + selectionSets: ReferenceSelectionSet[], + result: ReferenceSelectionSet[] + ): void { + if (selectionSets.length === 0) { + return; + } + + for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { + const base = selectionSets.slice(0, baseIndex + 1); + const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); + + const currentSelectionSet = base.reduce((acc, selectionSet) => { + acc = { ...acc, ...selectionSet }; + return acc; + }, {}); + + if (baseIndex === 0) { + result.push(currentSelectionSet); + } + + for (const selectionSet of rest) { + result.push({ ...currentSelectionSet, ...selectionSet }); + } + } + + const next = selectionSets.slice(1, selectionSets.length); + + if (next.length > 0) { + findAllSelectionSetCombinations(next, result); + } + } + + const printReferenceSelectionSets = ({ + typeName, + baseFederationType, + referenceSelectionSets, + }: { + typeName: string; + baseFederationType: string; + referenceSelectionSets: TypeMeta['referenceSelectionSets']; + }): string => { + const referenceSelectionSetStrings = referenceSelectionSets.reduce( + (acc, { directive, selectionSets: originalSelectionSets }) => { + const result: string[] = []; + + let selectionSets = originalSelectionSets; + if (directive === '@requires') { + selectionSets = []; + findAllSelectionSetCombinations(originalSelectionSets, selectionSets); + if (selectionSets.length > 0) { + result.push('{}'); + } + } + + for (const referenceSelectionSet of selectionSets) { + result.push(`GraphQLRecursivePick<${baseFederationType}, ${JSON.stringify(referenceSelectionSet)}>`); + } + + if (result.length === 0) { + return acc; + } + + if (result.length === 1) { + acc.push(result.join(' | ')); + return acc; + } + + acc.push(`( ${result.join('\n | ')} )`); + return acc; + }, + [] + ); + + return `\n ( { __typename: '${typeName}' }\n & ${referenceSelectionSetStrings.join('\n & ')} )`; + }; + const federationMeta: FederationMeta = {}; const transformedSchema = mapSchema(schema, { @@ -142,6 +254,12 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { fields: typeConfig.fields, }); + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + setFederationMeta({ meta: federationMeta, typeName: type.name, @@ -149,6 +267,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, referenceSelectionSets, + referenceSelectionSetsString, }, }); @@ -174,6 +293,12 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { ...typeConfig.fields, }; + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + setFederationMeta({ meta: federationMeta, typeName: type.name, @@ -181,6 +306,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, referenceSelectionSets, + referenceSelectionSetsString, }, }); @@ -344,44 +470,6 @@ export class ApolloFederation { return this.enabled && name === resolveReferenceFieldName; } - /** - * Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation - */ - transformFieldParentType({ - fieldNode, - parentType, - parentTypeSignature, - federationTypeSignature, - }: { - fieldNode: FieldDefinitionNode; - parentType: GraphQLNamedType; - parentTypeSignature: string; - federationTypeSignature: string; - }): string { - if (!this.enabled) { - return parentTypeSignature; - } - - const result = this.printReferenceSelectionSets({ - typeName: parentType.name, - baseFederationType: federationTypeSignature, - }); - - // When `!result`, it means this is not a Federation entity, so we just return the parentTypeSignature - if (!result) { - return parentTypeSignature; - } - - const isEntityResolveReferenceField = - (isObjectType(parentType) || isInterfaceType(parentType)) && fieldNode.name.value === resolveReferenceFieldName; - - if (!isEntityResolveReferenceField) { - return parentTypeSignature; - } - - return result; - } - addFederationTypeGenericIfApplicable({ genericTypes, typeName, @@ -396,7 +484,7 @@ export class ApolloFederation { } const typeRef = `${federationTypesType}['${typeName}']`; - genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); + genericTypes.push(`FederationReferenceType extends ${typeRef} = ${typeRef}`); } getMeta() { @@ -417,69 +505,6 @@ export class ApolloFederation { return false; } - printReferenceSelectionSet({ - typeName, - referenceSelectionSet, - }: { - typeName: string; - referenceSelectionSet: ReferenceSelectionSet; - }): string { - return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`; - } - - printReferenceSelectionSets({ - typeName, - baseFederationType, - }: { - typeName: string; - baseFederationType: string; - }): string | false { - const federationMeta = this.getMeta()[typeName]; - - if (!federationMeta?.hasResolveReference) { - return false; - } - - const referenceSelectionSetStrings = federationMeta.referenceSelectionSets.reduce( - (acc, { directive, selectionSets: originalSelectionSets }) => { - const result: string[] = []; - - let selectionSets = originalSelectionSets; - if (directive === '@requires') { - selectionSets = []; - findAllSelectionSetCombinations(originalSelectionSets, selectionSets); - if (selectionSets.length > 0) { - result.push('{}'); - } - } - - for (const referenceSelectionSet of selectionSets) { - result.push( - this.printReferenceSelectionSet({ - referenceSelectionSet, - typeName: baseFederationType, - }) - ); - } - - if (result.length === 0) { - return acc; - } - - if (result.length === 1) { - acc.push(result.join(' | ')); - return acc; - } - - acc.push(`( ${result.join('\n | ')} )`); - return acc; - }, - [] - ); - - return `\n ( { __typename: '${typeName}' }\n & ${referenceSelectionSetStrings.join('\n & ')} )`; - } - private createMapOfProvides() { const providesMap: Record = {}; @@ -598,70 +623,3 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect }, }); } - -/** - * Function to find all combinations of selection sets and push them into the `result` - * This is used for `@requires` directive because depending on the operation selection set, different - * combination of fields are sent from the router. - * - * @example - * Input: [ - * { a: true }, - * { b: true }, - * { c: true }, - * { d: true}, - * ] - * Output: [ - * { a: true }, - * { a: true, b: true }, - * { a: true, c: true }, - * { a: true, d: true }, - * { a: true, b: true, c: true }, - * { a: true, b: true, d: true }, - * { a: true, c: true, d: true }, - * { a: true, b: true, c: true, d: true } - * - * { b: true }, - * { b: true, c: true }, - * { b: true, d: true }, - * { b: true, c: true, d: true } - * - * { c: true }, - * { c: true, d: true }, - * - * { d: true }, - * ] - * ``` - */ -function findAllSelectionSetCombinations( - selectionSets: ReferenceSelectionSet[], - result: ReferenceSelectionSet[] -): void { - if (selectionSets.length === 0) { - return; - } - - for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { - const base = selectionSets.slice(0, baseIndex + 1); - const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); - - const currentSelectionSet = base.reduce((acc, selectionSet) => { - acc = { ...acc, ...selectionSet }; - return acc; - }, {}); - - if (baseIndex === 0) { - result.push(currentSelectionSet); - } - - for (const selectionSet of rest) { - result.push({ ...currentSelectionSet, ...selectionSet }); - } - } - - const next = selectionSets.slice(1, selectionSets.length); - - if (next.length > 0) { - findAllSelectionSetCombinations(next, result); - } -} From 44802c0840ae29ad1077e13de5b9a766f3802849 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 11 Jun 2025 01:24:13 +1000 Subject: [PATCH 07/10] Update tests related to FederationReferenceTypes --- .../__snapshots__/ts-resolvers.spec.ts.snap | 3 + .../ts-resolvers.federation.interface.spec.ts | 39 +-- .../ts-resolvers.federation.mappers.spec.ts | 13 +- .../tests/ts-resolvers.federation.spec.ts | 238 +++++++++++------- 4 files changed, 176 insertions(+), 117 deletions(-) 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 97ab3f2fcf5..148befbea90 100644 --- a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap +++ b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap @@ -167,6 +167,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -433,6 +434,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -785,6 +787,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: 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 d1e08bd54c1..6ac6bdb0cca 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 @@ -124,6 +124,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { Admin: Admin; }; + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + Person: + ( { __typename: 'Person' } + & GraphQLRecursivePick ); + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Admin: + ( { __typename: 'Admin' } + & GraphQLRecursivePick ); + }; + /** Mapping of interface types */ export type ResolversInterfaceTypes<_RefType extends Record> = { @@ -150,12 +163,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { Query: {}; Person: ResolversInterfaceTypes['Person']; ID: Scalars['ID']['output']; - User: User | - ( { __typename: 'User' } - & GraphQLRecursivePick ); - Admin: Admin | - ( { __typename: 'Admin' } - & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; + Admin: Admin | FederationReferenceTypes['Admin']; Boolean: Scalars['Boolean']['output']; PersonName: PersonName; String: Scalars['String']['output']; @@ -165,26 +174,20 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { me?: Resolver, ParentType, ContextType>; }; - export type PersonResolvers = { + export type PersonResolvers = { __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - __resolveReference?: ReferenceResolver, - ( { __typename: 'Person' } - & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; }; - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; - export type AdminResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'Admin' } - & GraphQLRecursivePick ), ContextType>; + export type AdminResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, 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 905ebc924fb..89f3c72a03c 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 @@ -117,6 +117,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { User: User; }; + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + /** Mapping between all available schema types and the resolvers types */ @@ -143,10 +150,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { me?: Resolver, ParentType, ContextType>; }; - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, 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 f4d7524a5da..54301d35578 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -92,17 +92,35 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + SingleResolvable: + ( { __typename: 'SingleResolvable' } + & GraphQLRecursivePick ); + AtLeastOneResolvable: + ( { __typename: 'AtLeastOneResolvable' } + & GraphQLRecursivePick ); + MixedResolvable: + ( { __typename: 'MixedResolvable' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; Book: Book; - SingleResolvable: SingleResolvable | ( { __typename: 'SingleResolvable' } & GraphQLRecursivePick ); + SingleResolvable: SingleResolvable | FederationReferenceTypes['SingleResolvable']; SingleNonResolvable: SingleNonResolvable; - AtLeastOneResolvable: AtLeastOneResolvable | ( { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick ); - MixedResolvable: MixedResolvable | ( { __typename: 'MixedResolvable' } & ( GraphQLRecursivePick | GraphQLRecursivePick ) ); + AtLeastOneResolvable: AtLeastOneResolvable | FederationReferenceTypes['AtLeastOneResolvable']; + MixedResolvable: MixedResolvable | FederationReferenceTypes['MixedResolvable']; MultipleNonResolvable: MultipleNonResolvable; Boolean: Scalars['Boolean']['output']; }; @@ -110,24 +128,21 @@ 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>; - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - }; - `); + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + }; + `); // SingleResolvable has __resolveReference because it has resolvable: true expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'SingleResolvable' } - & GraphQLRecursivePick ), ContextType>; - id?: Resolver; - }; - `); + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + }; + `); // SingleNonResolvable does NOT have __resolveReference because it has resolvable: false expect(content).toBeSimilarStringTo(` @@ -138,26 +153,23 @@ 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>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - }; - `); + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); // MixedResolvable has __resolveReference and references for resolvable keys expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'MixedResolvable' } - & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - }; - `); + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable expect(content).toBeSimilarStringTo(` @@ -200,13 +212,21 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + // User should have it expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, ( { __typename: 'User' } & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; `); // Foo shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, ( { __typename: 'Book' } & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver, FederationReferenceType, ContextType>; `); }); @@ -242,23 +262,30 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + Name: + ( { __typename: 'Name' } + & GraphQLRecursivePick ); + User: ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; - } + }; `); expect(content).toBeSimilarStringTo(` - export type NameResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'Name' } - & GraphQLRecursivePick ), ContextType>; + export type NameResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; first?: Resolver; last?: Resolver; - } + }; `); }); @@ -378,16 +405,22 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | - ( { __typename: 'User' } - & GraphQLRecursivePick - & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ); + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; Int: Scalars['Int']['output']; @@ -397,14 +430,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick - & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; favouriteColorHex?: Resolver; username?: Resolver, ParentType, ContextType>; }; @@ -436,11 +463,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type ResolversParentTypes = { - Query: {}; - User: User | + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; String: Scalars['String']['output']; Name: Name; Boolean: Scalars['Boolean']['output']; @@ -448,10 +481,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; username?: Resolver, ParentType, ContextType>; }; `); @@ -486,17 +517,23 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type ResolversParentTypes = { - Query: {}; - User: User | + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } - & ( GraphQLRecursivePick + & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) - & ( {} + & ( {} | GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; LegacyId: LegacyId; @@ -505,16 +542,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & ( GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) - & ( {} - | GraphQLRecursivePick - | GraphQLRecursivePick - | GraphQLRecursivePick ) ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; uuid?: Resolver; username?: Resolver; @@ -548,10 +577,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; name?: Resolver; username?: Resolver, ParentType, ContextType>; }; @@ -608,13 +643,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + // `UserResolvers` should not have `username` resolver because it is marked with `@external` // `UserResolvers` should have `name` resolver because whilst it is marked with `@external`, it is provided by `Book.author` expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; address?: Resolver, ParentType, ContextType>; @@ -761,12 +802,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); - // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + + // User should have it + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; }; From 3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 12 Jun 2025 23:44:45 +1000 Subject: [PATCH 08/10] Update dev-tests --- dev-test/test-schema/resolvers-federation.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index 3583b2d7cae..1c52dab9c31 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -133,6 +133,15 @@ export type FederationTypes = { User: User; }; +/** Mapping of federation reference types */ +export type FederationReferenceTypes = { + User: { __typename: 'User' } & ( + | GraphQLRecursivePick + | GraphQLRecursivePick + ) & + ({} | GraphQLRecursivePick); +}; + /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Address: ResolverTypeWrapper
; @@ -154,13 +163,7 @@ export type ResolversParentTypes = { ID: Scalars['ID']['output']; Lines: Lines; Query: {}; - User: - | User - | ({ __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ) & - GraphQLRecursivePick); + User: User | FederationReferenceTypes['User']; Int: Scalars['Int']['output']; Boolean: Scalars['Boolean']['output']; }; @@ -199,15 +202,11 @@ export type QueryResolvers< export type UserResolvers< ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], - FederationType extends FederationTypes['User'] = FederationTypes['User'] + FederationReferenceType extends FederationReferenceTypes['User'] = FederationReferenceTypes['User'] > = { __resolveReference?: ReferenceResolver< - Maybe, - { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ) & - GraphQLRecursivePick, + Maybe | FederationReferenceType, + FederationReferenceType, ContextType >; email?: Resolver; From 39faa7e9a66fe76425d6029a271ca94fe3f5abf0 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Jun 2025 00:19:20 +1000 Subject: [PATCH 09/10] Revert force release --- .github/workflows/pr.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e19a2df1579..bf47152690b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,7 +3,6 @@ on: pull_request: branches: - master - - federation-fixes jobs: # dependencies: From 61475debf3977c14f9d51e371515a68b6440994b Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Fri, 13 Jun 2025 00:37:00 +1000 Subject: [PATCH 10/10] Update test related to mapper --- .../ts-resolvers.federation.mappers.spec.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 89f3c72a03c..a6f58d073ac 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 @@ -17,6 +17,12 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { id: ID! user: User! } + + type Account @key(fields: "id") { + id: ID! + name: String! @external + displayName: String! @requires(fields: "name") + } `; const content = await generate({ @@ -25,6 +31,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { federation: true, mappers: { User: './mappers#UserMapper', + Account: './mappers#AccountMapper', }, }, }); @@ -32,7 +39,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { // User should have it expect(content).toMatchInlineSnapshot(` "import { GraphQLResolveInfo } from 'graphql'; - import { UserMapper } from './mappers'; + import { UserMapper, AccountMapper } from './mappers'; export type Omit = Pick>; @@ -115,6 +122,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { /** Mapping of federation types */ export type FederationTypes = { User: User; + Account: Account; }; /** Mapping of federation reference types */ @@ -122,6 +130,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { User: ( { __typename: 'User' } & GraphQLRecursivePick ); + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick ) ); }; @@ -133,6 +146,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { ID: ResolverTypeWrapper; String: ResolverTypeWrapper; UserProfile: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + Account: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; }; @@ -143,6 +157,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { ID: Scalars['ID']['output']; String: Scalars['String']['output']; UserProfile: Omit & { user: ResolversParentTypes['User'] }; + Account: AccountMapper; Boolean: Scalars['Boolean']['output']; }; @@ -161,10 +176,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { user?: Resolver; }; + export type AccountResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + displayName?: Resolver; + }; + export type Resolvers = { Query?: QueryResolvers; User?: UserResolvers; UserProfile?: UserProfileResolvers; + Account?: AccountResolvers; }; "