From 661e0395cb927976283c5b95255034a020e778d7 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 10 Dec 2024 23:40:09 +1100 Subject: [PATCH 1/8] Fix reference being assigned mappers incorrectly --- .../src/base-resolvers-visitor.ts | 34 ++++++++++++++++++- .../plugins/typescript/resolvers/src/index.ts | 2 ++ .../utils/plugins-helpers/src/federation.ts | 6 ++-- 3 files changed, 39 insertions(+), 3 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 19351915e15..b82049905ef 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 @@ -1223,6 +1223,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; } @@ -1498,6 +1520,7 @@ export class BaseResolversVisitor< fieldNode: original, parentType, parentTypeSignature: this.getParentTypeForSignature(node), + federationTypeSignature: 'FederationType', }); const mappedTypeKey = isSubscriptionType ? `${mappedType}, "${node.name}"` : mappedType; @@ -1619,10 +1642,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 e0d3ff293a3..e949ae972a2 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -244,6 +244,7 @@ export type DirectiveResolverFn TResult | Promise; `; + const federationTypes = visitor.buildFederationTypes(); const resolversTypeMapping = visitor.buildResolversTypes(); const resolversParentTypeMapping = visitor.buildResolversParentTypes(); const resolversUnionTypesMapping = visitor.buildResolversUnionTypes(); @@ -287,6 +288,7 @@ export type DirectiveResolverFn { const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, parentTypeSignature); + return this.translateFieldSet(fields, federationTypeSignature); }); const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; From 64ab400f5134314d3037478f5fbd34cfdff4b6b6 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 10 Dec 2024 23:41:12 +1100 Subject: [PATCH 2/8] Add test for federation mappers usage in reference --- .../ts-resolvers.federation.mappers.spec.ts | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts 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..928f659410e --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -0,0 +1,182 @@ +import '@graphql-codegen/testing'; +import { codegen } from '@graphql-codegen/core'; +import { parse } from 'graphql'; +import { TypeScriptResolversPluginConfig } from '../src/config.js'; +import { plugin } from '../src/index.js'; + +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 } }, + }); +} + +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; + }; + + " + `); + }); +}); From 9273310b8d7fb125bfb0ddb1fbacd138fb2150f0 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Wed, 11 Dec 2024 00:05:19 +1100 Subject: [PATCH 3/8] Add changeset --- .changeset/thick-pianos-smoke.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/thick-pianos-smoke.md diff --git a/.changeset/thick-pianos-smoke.md b/.changeset/thick-pianos-smoke.md new file mode 100644 index 00000000000..53df81ad368 --- /dev/null +++ b/.changeset/thick-pianos-smoke.md @@ -0,0 +1,9 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/plugin-helpers': patch +--- + +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 From c9320446817e4c17168a351242e95be8f48c0c6d Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Dec 2024 19:33:31 +1100 Subject: [PATCH 4/8] Remove extraneous UnwrappedObject type --- .../plugins/typescript/resolvers/src/index.ts | 7 - .../typescript/resolvers/src/visitor.ts | 16 +- .../__snapshots__/ts-resolvers.spec.ts.snap | 3 + .../tests/ts-resolvers.federation.spec.ts | 143 ++++++------------ .../utils/plugins-helpers/src/federation.ts | 36 ++--- 5 files changed, 73 insertions(+), 132 deletions(-) diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index e949ae972a2..692622a72bb 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -106,13 +106,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, diff --git a/packages/plugins/typescript/resolvers/src/visitor.ts b/packages/plugins/typescript/resolvers/src/visitor.ts index a587e67b550..ea4434dfa7e 100644 --- a/packages/plugins/typescript/resolvers/src/visitor.ts +++ b/packages/plugins/typescript/resolvers/src/visitor.ts @@ -7,14 +7,7 @@ import { ParsedResolversConfig, } from '@graphql-codegen/visitor-plugin-common'; import autoBind from 'auto-bind'; -import { - EnumTypeDefinitionNode, - FieldDefinitionNode, - GraphQLSchema, - ListTypeNode, - NamedTypeNode, - NonNullTypeNode, -} from 'graphql'; +import { EnumTypeDefinitionNode, GraphQLSchema, ListTypeNode, NamedTypeNode, NonNullTypeNode } from 'graphql'; import { TypeScriptResolversPluginConfig } from './config.js'; export const ENUM_RESOLVERS_SIGNATURE = @@ -96,13 +89,6 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< return `${this.config.immutableTypes ? 'ReadonlyArray' : 'Array'}<${str}>`; } - protected getParentTypeForSignature(node: FieldDefinitionNode) { - if (this._federation.isResolveReferenceField(node) && this.config.wrapFieldDefinitions) { - return 'UnwrappedObject'; - } - return 'ParentType'; - } - NamedType(node: NamedTypeNode): string { return `Maybe<${super.NamedType(node)}>`; } 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.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts index 7aae74ee79c..d6f97f1e515 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/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 685150e6c10..4da2e33e1c1 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -174,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), federationTypeSignature); + 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, 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), 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 bab88db218ee0796e8858c2d2f0fff815e2e7584 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Dec 2024 21:22:07 +1100 Subject: [PATCH 5/8] Change to major because it may break existing use cases --- .changeset/thick-pianos-smoke.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/thick-pianos-smoke.md b/.changeset/thick-pianos-smoke.md index 53df81ad368..569664420a9 100644 --- a/.changeset/thick-pianos-smoke.md +++ b/.changeset/thick-pianos-smoke.md @@ -1,9 +1,11 @@ --- -'@graphql-codegen/visitor-plugin-common': patch -'@graphql-codegen/typescript-resolvers': patch -'@graphql-codegen/plugin-helpers': patch +'@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 +`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. From c3a941062a81535e1427fa62d17113bd9e5fe3fa Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Dec 2024 23:04:06 +1100 Subject: [PATCH 6/8] Run CI on federation-fixes feature branch --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b57dd5d69a6..cc9d7369b93 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' From 9d1df529741dc20a0821a7ba83dce5409a5b1b5a Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 16 Dec 2024 23:15:21 +1100 Subject: [PATCH 7/8] Update dev tests --- dev-test/test-schema/resolvers-federation.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 >; From a784d3bf06225f47066ff6e99a270fabb201f32f Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 22 Dec 2024 23:18:11 +1100 Subject: [PATCH 8/8] Clean up tests --- .../ts-resolvers.federation.mappers.spec.ts | 16 +--------------- .../plugins/typescript/resolvers/tests/utils.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 packages/plugins/typescript/resolvers/tests/utils.ts 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 928f659410e..9db647c00cc 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 @@ -1,19 +1,5 @@ import '@graphql-codegen/testing'; -import { codegen } from '@graphql-codegen/core'; -import { parse } from 'graphql'; -import { TypeScriptResolversPluginConfig } from '../src/config.js'; -import { plugin } from '../src/index.js'; - -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 } }, - }); -} +import { generate } from './utils'; describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { it('generates FederationTypes and use it for reference type', async () => { 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 } }, + }); +}