From 4831bfbecfa7f351f1e80b98cb1d6aa235b0120e Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 6 Jun 2024 20:00:42 +1000 Subject: [PATCH 1/9] Handle resolvable:false in federation entities --- .../src/base-resolvers-visitor.ts | 27 +- .../tests/ts-resolvers.federation.spec.ts | 247 +++++++++++++++++- .../utils/plugins-helpers/src/federation.ts | 68 +++-- 3 files changed, 315 insertions(+), 27 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 08b7c572b0b..849059e3570 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, getBaseType } from '@graphql-codegen/plugin-helpers'; +import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers'; import { getRootTypeNames } from '@graphql-tools/utils'; import autoBind from 'auto-bind'; import { @@ -641,7 +641,13 @@ export class BaseResolversVisitor< > extends BaseVisitor { protected _parsedConfig: TPluginConfig; protected _declarationBlockConfig: DeclarationBlockConfig = {}; - protected _collectedResolvers: { [key: string]: { typename: string; baseGeneratedTypename?: string } } = {}; + protected _collectedResolvers: { + [key: string]: { + typename: string; + baseGeneratedTypename?: string; + federation?: { hasResolveReference: boolean }; + }; + } = {}; protected _collectedDirectiveResolvers: { [key: string]: string } = {}; protected _variablesTransformer: OperationVariablesToObject; protected _usedMappers: { [key: string]: boolean } = {}; @@ -1283,7 +1289,7 @@ export class BaseResolversVisitor< const declarationKind = 'type'; const contextType = ``; - const userDefinedTypes: Record = {}; + const userDefinedTypes: Record = {}; const content = [ new DeclarationBlock(this._declarationBlockConfig) .export() @@ -1295,7 +1301,10 @@ export class BaseResolversVisitor< const resolverType = this._collectedResolvers[schemaTypeName]; if (resolverType.baseGeneratedTypename) { - userDefinedTypes[schemaTypeName] = { name: resolverType.baseGeneratedTypename }; + userDefinedTypes[schemaTypeName] = { + name: resolverType.baseGeneratedTypename, + federation: resolverType.federation, + }; } return indent(this.formatRootResolver(schemaTypeName, resolverType.typename, declarationKind)); @@ -1525,7 +1534,7 @@ export class BaseResolversVisitor< return `Partial<${argsType}>`; } - ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string { + ObjectTypeDefinition(node: ObjectTypeDefinitionNode, key: number, parent: any): string { const declarationKind = 'type'; const name = this.convertName(node, { suffix: this.config.resolverTypeSuffix, @@ -1577,6 +1586,14 @@ export class BaseResolversVisitor< baseGeneratedTypename: name, }; + if (this.config.federation) { + const originalNode = parent[key] as ObjectTypeDefinitionNode; + const federationDetails = checkObjectTypeFederationDetails(originalNode, this._schema); + this._collectedResolvers[node.name as any].federation = { + hasResolveReference: federationDetails ? federationDetails.resolvableKeyDirectives.length > 0 : false, + }; + } + return block.string; } 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 47119241af1..8b916cb21f6 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -1,6 +1,6 @@ import '@graphql-codegen/testing'; import { codegen } from '@graphql-codegen/core'; -import { parse } from 'graphql'; +import { buildSchema, parse } from 'graphql'; import { TypeScriptResolversPluginConfig } from '../src/config.js'; import { plugin } from '../src/index.js'; @@ -24,7 +24,7 @@ function generate({ schema, config }: { schema: string; config: TypeScriptResolv } describe('TypeScript Resolvers Plugin + Apollo Federation', () => { - it('should add __resolveReference to objects that have @key', async () => { + it('should add __resolveReference to objects that have @key and is resolvable', async () => { const federatedSchema = /* GraphQL */ ` type Query { allUsers: [User] @@ -39,6 +39,41 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { 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({ @@ -52,7 +87,57 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; `); - // Foo shouldn't because it doesn't have @key + + // SingleResolvable should have __resolveReference because it has resolvable: true + expect(content).toBeSimilarStringTo(` + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // SingleNonResolvable shouldn't have __resolveReference because it has resolvable: false + expect(content).toBeSimilarStringTo(` + export type SingleNonResolvableResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // AtLeastOneResolvable should have __resolveReference because it at least one resolvable + expect(content).toBeSimilarStringTo(` + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // MixedResolvable should have __resolveReference and references for resolvable keys + expect(content).toBeSimilarStringTo(` + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // MultipleNonResolvableResolvers does not have __resolveReference because all keys are non-resolvable + expect(content).toBeSimilarStringTo(` + export type MultipleNonResolvableResolvers = { + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // Book shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; `); @@ -584,4 +669,160 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(`id?: Resolver`); }); }); + + it('meta - generates federation meta correctly', async () => { + const federatedSchema = /* GraphQL */ ` + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE + + type Query { + user: UserPayload! + allUsers: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + + interface Node { + id: ID! + } + + type UserOk { + id: ID! + } + type UserError { + message: String! + } + union UserPayload = UserOk | UserError + + enum Country { + FR + US + } + + type NotResolvable @key(fields: "id", resolvable: false) { + id: ID! + } + + type Resolvable @key(fields: "id", resolvable: true) { + id: ID! + } + + type MultipleResolvable + @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 result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); + + expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` + Object { + "MultipleNonResolvable": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "MultipleNonResolvableResolvers", + }, + "MultipleResolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "MultipleResolvableResolvers", + }, + "Node": Object { + "federation": undefined, + "name": "NodeResolvers", + }, + "NotResolvable": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "NotResolvableResolvers", + }, + "Query": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "QueryResolvers", + }, + "Resolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "ResolvableResolvers", + }, + "User": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "UserResolvers", + }, + "UserError": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "UserErrorResolvers", + }, + "UserOk": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "UserOkResolvers", + }, + "UserPayload": Object { + "federation": undefined, + "name": "UserPayloadResolvers", + }, + } + `); + }); + + it('meta - does not generate federation meta if federation config is false', async () => { + const federatedSchema = /* GraphQL */ ` + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE + + type Query { + allUsers: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + `; + + const result = await plugin(buildSchema(federatedSchema), [], {}, { outputFile: '' }); + + expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` + Object { + "Query": Object { + "federation": undefined, + "name": "QueryResolvers", + }, + "User": Object { + "federation": undefined, + "name": "UserResolvers", + }, + } + `); + }); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index de2a0c4a65a..2fbd510f3a4 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,5 +1,6 @@ import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; import { + type ConstDirectiveNode, DefinitionNode, DirectiveNode, FieldDefinitionNode, @@ -35,18 +36,25 @@ export const federationSpec = parse(/* GraphQL */ ` export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_TYPE]: type => { - if (isFederationObjectType(type, schema)) { - const typeConfig = type.toConfig(); - typeConfig.fields = { - [resolveReferenceFieldName]: { - type, - }, - ...typeConfig.fields, - }; - - return new GraphQLObjectType(typeConfig); + const federationObjectTypeDetails = checkObjectTypeFederationDetails(type, schema); + + if (!federationObjectTypeDetails) { + return type; + } + + if (federationObjectTypeDetails.resolvableKeyDirectives.length === 0) { + return type; } - return type; + + const typeConfig = type.toConfig(); + typeConfig.fields = { + [resolveReferenceFieldName]: { + type, + }, + ...typeConfig.fields, + }; + + return new GraphQLObjectType(typeConfig); }, }); } @@ -130,7 +138,7 @@ export class ApolloFederation { * @param data */ skipField({ fieldNode, parentType }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType }): boolean { - if (!this.enabled || !isObjectType(parentType) || !isFederationObjectType(parentType, this.schema)) { + if (!this.enabled || !isObjectType(parentType) || !checkObjectTypeFederationDetails(parentType, this.schema)) { return false; } @@ -158,12 +166,16 @@ export class ApolloFederation { if ( this.enabled && isObjectType(parentType) && - isFederationObjectType(parentType, this.schema) && (isTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName) ) { - const keys = getDirectivesByName('key', parentType); + const federationObjectTypeDetails = checkObjectTypeFederationDetails(parentType, this.schema); + if (!federationObjectTypeDetails) { + return parentTypeSignature; + } + + const { resolvableKeyDirectives } = federationObjectTypeDetails; - if (keys.length) { + if (resolvableKeyDirectives.length) { const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; // Look for @requires and see what the service needs and gets @@ -171,7 +183,7 @@ export class ApolloFederation { const requiredFields = this.translateFieldSet(merge({}, ...requires), parentTypeSignature); // @key() @key() - "primary keys" in Federation - const primaryKeys = keys.map(def => { + const primaryKeys = resolvableKeyDirectives.map(def => { const fields = this.extractFieldSet(def); return this.translateFieldSet(fields, parentTypeSignature); }); @@ -275,7 +287,10 @@ export class ApolloFederation { * Checks if Object Type is involved in Federation. Based on `@key` directive * @param node Type */ -function isFederationObjectType(node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema): boolean { +export function checkObjectTypeFederationDetails( + node: ObjectTypeDefinitionNode | GraphQLObjectType, + schema: GraphQLSchema +): { keyDirectives: readonly ConstDirectiveNode[]; resolvableKeyDirectives: readonly ConstDirectiveNode[] } | false { const { name: { value: name }, directives, @@ -284,9 +299,24 @@ function isFederationObjectType(node: ObjectTypeDefinitionNode | GraphQLObjectTy const rootTypeNames = getRootTypeNames(schema); const isNotRoot = !rootTypeNames.has(name); const isNotIntrospection = !name.startsWith('__'); - const hasKeyDirective = directives.some(d => d.name.value === 'key'); + const keyDirectives = directives.filter(d => d.name.value === 'key'); + + const check = isNotRoot && isNotIntrospection && keyDirectives.length > 0; + + if (!check) { + return false; + } + + const resolvableKeyDirectives = keyDirectives.filter(d => { + for (const arg of d.arguments) { + if (arg.name.value === 'resolvable' && arg.value.kind === 'BooleanValue' && arg.value.value === false) { + return false; + } + } + return true; + }); - return isNotRoot && isNotIntrospection && hasKeyDirective; + return { keyDirectives, resolvableKeyDirectives }; } /** From f8965dea37c77baa412fae39335ac28b6809d119 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 6 Jun 2024 21:48:50 +1000 Subject: [PATCH 2/9] Fix naming --- packages/utils/plugins-helpers/src/federation.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 2fbd510f3a4..eeccd16c1c6 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -36,13 +36,13 @@ export const federationSpec = parse(/* GraphQL */ ` export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_TYPE]: type => { - const federationObjectTypeDetails = checkObjectTypeFederationDetails(type, schema); + const objectTypeFederationDetails = checkObjectTypeFederationDetails(type, schema); - if (!federationObjectTypeDetails) { + if (!objectTypeFederationDetails) { return type; } - if (federationObjectTypeDetails.resolvableKeyDirectives.length === 0) { + if (objectTypeFederationDetails.resolvableKeyDirectives.length === 0) { return type; } @@ -168,12 +168,12 @@ export class ApolloFederation { isObjectType(parentType) && (isTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName) ) { - const federationObjectTypeDetails = checkObjectTypeFederationDetails(parentType, this.schema); - if (!federationObjectTypeDetails) { + const objectTypeFederationDetails = checkObjectTypeFederationDetails(parentType, this.schema); + if (!objectTypeFederationDetails) { return parentTypeSignature; } - const { resolvableKeyDirectives } = federationObjectTypeDetails; + const { resolvableKeyDirectives } = objectTypeFederationDetails; if (resolvableKeyDirectives.length) { const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; From 1332e8b0003336ff6e40e14b10f295e2f5861b78 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 6 Jun 2024 21:52:25 +1000 Subject: [PATCH 3/9] Refactor and remove extraneous test --- .../tests/ts-resolvers.federation.spec.ts | 32 ------------------- .../utils/plugins-helpers/src/federation.ts | 10 ++---- 2 files changed, 3 insertions(+), 39 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 8b916cb21f6..0941a774261 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -793,36 +793,4 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { } `); }); - - it('meta - does not generate federation meta if federation config is false', async () => { - const federatedSchema = /* GraphQL */ ` - scalar _FieldSet - directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - - type Query { - allUsers: [User] - } - - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - `; - - const result = await plugin(buildSchema(federatedSchema), [], {}, { outputFile: '' }); - - expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` - Object { - "Query": Object { - "federation": undefined, - "name": "QueryResolvers", - }, - "User": Object { - "federation": undefined, - "name": "UserResolvers", - }, - } - `); - }); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index eeccd16c1c6..9aa2abd441b 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -38,11 +38,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLS [MapperKind.OBJECT_TYPE]: type => { const objectTypeFederationDetails = checkObjectTypeFederationDetails(type, schema); - if (!objectTypeFederationDetails) { - return type; - } - - if (objectTypeFederationDetails.resolvableKeyDirectives.length === 0) { + if (!objectTypeFederationDetails || objectTypeFederationDetails.resolvableKeyDirectives.length === 0) { return type; } @@ -290,7 +286,7 @@ export class ApolloFederation { export function checkObjectTypeFederationDetails( node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema -): { keyDirectives: readonly ConstDirectiveNode[]; resolvableKeyDirectives: readonly ConstDirectiveNode[] } | false { +): { resolvableKeyDirectives: readonly ConstDirectiveNode[] } | false { const { name: { value: name }, directives, @@ -316,7 +312,7 @@ export function checkObjectTypeFederationDetails( return true; }); - return { keyDirectives, resolvableKeyDirectives }; + return { resolvableKeyDirectives }; } /** From 3b21974112cad1bbb874196f7632682351e6af15 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 6 Jun 2024 22:08:52 +1000 Subject: [PATCH 4/9] Add changeset --- .changeset/fifty-dodos-marry.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fifty-dodos-marry.md diff --git a/.changeset/fifty-dodos-marry.md b/.changeset/fifty-dodos-marry.md new file mode 100644 index 00000000000..c92959bae84 --- /dev/null +++ b/.changeset/fifty-dodos-marry.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-resolvers': minor +'@graphql-codegen/plugin-helpers': minor +--- + +Avoid generating reference resolvers if federation object is not resolvable From 9e8c92d5e13909b46741ab99679b875011487128 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 20 Aug 2024 21:35:24 +1000 Subject: [PATCH 5/9] Clean up typing --- .../src/base-resolvers-visitor.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 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 849059e3570..814b5bc9eb0 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 @@ -81,6 +81,13 @@ export interface ParsedResolversConfig extends ParsedConfig { } type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; +interface RootResolverResult { + content: string; + generatedResolverTypes: { + resolversMap: { name: string }; + userDefined: Record; + }; +} export interface RawResolversConfig extends RawConfig { /** @@ -1278,18 +1285,12 @@ export class BaseResolversVisitor< return this._hasFederation; } - public getRootResolver(): { - content: string; - generatedResolverTypes: { - resolversMap: { name: string }; - userDefined: Record; - }; - } { + public getRootResolver(): RootResolverResult { const name = this.convertName(this.config.allResolversTypeName); const declarationKind = 'type'; const contextType = ``; - const userDefinedTypes: Record = {}; + const userDefinedTypes: RootResolverResult['generatedResolverTypes']['userDefined'] = {}; const content = [ new DeclarationBlock(this._declarationBlockConfig) .export() From 06035ca5e5744851bdd46e252fe4026be0902c75 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 20 Aug 2024 21:50:51 +1000 Subject: [PATCH 6/9] Fix typing issue for GraphQL 15 --- packages/utils/plugins-helpers/src/federation.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 9aa2abd441b..c64ad3b16df 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,8 +1,7 @@ import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; import { - type ConstDirectiveNode, + type DirectiveNode, DefinitionNode, - DirectiveNode, FieldDefinitionNode, GraphQLNamedType, GraphQLObjectType, @@ -286,7 +285,7 @@ export class ApolloFederation { export function checkObjectTypeFederationDetails( node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema -): { resolvableKeyDirectives: readonly ConstDirectiveNode[] } | false { +): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { const { name: { value: name }, directives, From 3034c9a805e0ef5d80c80d66fb637e327e0b402c Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 22 Aug 2024 20:50:11 +1000 Subject: [PATCH 7/9] Ensure no federation: undefined case --- .../src/base-resolvers-visitor.ts | 4 +- .../tests/ts-resolvers.federation.spec.ts | 97 ++++++++++--------- 2 files changed, 53 insertions(+), 48 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 814b5bc9eb0..64ca9b25e00 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 @@ -1304,8 +1304,10 @@ export class BaseResolversVisitor< if (resolverType.baseGeneratedTypename) { userDefinedTypes[schemaTypeName] = { name: resolverType.baseGeneratedTypename, - federation: resolverType.federation, }; + if (resolverType.federation) { + userDefinedTypes[schemaTypeName].federation = resolverType.federation; + } } return indent(this.formatRootResolver(schemaTypeName, resolverType.typename, declarationKind)); 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 0941a774261..a70081fd926 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -734,61 +734,64 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` Object { - "MultipleNonResolvable": Object { - "federation": Object { - "hasResolveReference": false, - }, - "name": "MultipleNonResolvableResolvers", + "resolversMap": Object { + "name": "Resolvers", }, - "MultipleResolvable": Object { - "federation": Object { - "hasResolveReference": true, + "userDefined": Object { + "MultipleNonResolvable": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "MultipleNonResolvableResolvers", }, - "name": "MultipleResolvableResolvers", - }, - "Node": Object { - "federation": undefined, - "name": "NodeResolvers", - }, - "NotResolvable": Object { - "federation": Object { - "hasResolveReference": false, + "MultipleResolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "MultipleResolvableResolvers", }, - "name": "NotResolvableResolvers", - }, - "Query": Object { - "federation": Object { - "hasResolveReference": false, + "Node": Object { + "name": "NodeResolvers", }, - "name": "QueryResolvers", - }, - "Resolvable": Object { - "federation": Object { - "hasResolveReference": true, + "NotResolvable": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "NotResolvableResolvers", }, - "name": "ResolvableResolvers", - }, - "User": Object { - "federation": Object { - "hasResolveReference": true, + "Query": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "QueryResolvers", }, - "name": "UserResolvers", - }, - "UserError": Object { - "federation": Object { - "hasResolveReference": false, + "Resolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "ResolvableResolvers", }, - "name": "UserErrorResolvers", - }, - "UserOk": Object { - "federation": Object { - "hasResolveReference": false, + "User": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "UserResolvers", + }, + "UserError": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "UserErrorResolvers", + }, + "UserOk": Object { + "federation": Object { + "hasResolveReference": false, + }, + "name": "UserOkResolvers", + }, + "UserPayload": Object { + "name": "UserPayloadResolvers", }, - "name": "UserOkResolvers", - }, - "UserPayload": Object { - "federation": undefined, - "name": "UserPayloadResolvers", }, } `); From 89be355f9a56a0305c13888fb5d971ade13675c7 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Thu, 22 Aug 2024 21:35:34 +1000 Subject: [PATCH 8/9] Use consistent typing for meta --- .../visitor-plugin-common/src/base-resolvers-visitor.ts | 6 +++--- packages/plugins/typescript/resolvers/src/index.ts | 7 ++----- 2 files changed, 5 insertions(+), 8 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 64ca9b25e00..c7118cb3c21 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 @@ -81,7 +81,7 @@ export interface ParsedResolversConfig extends ParsedConfig { } type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; -interface RootResolverResult { +export interface RootResolver { content: string; generatedResolverTypes: { resolversMap: { name: string }; @@ -1285,12 +1285,12 @@ export class BaseResolversVisitor< return this._hasFederation; } - public getRootResolver(): RootResolverResult { + public getRootResolver(): RootResolver { const name = this.convertName(this.config.allResolversTypeName); const declarationKind = 'type'; const contextType = ``; - const userDefinedTypes: RootResolverResult['generatedResolverTypes']['userDefined'] = {}; + const userDefinedTypes: RootResolver['generatedResolverTypes']['userDefined'] = {}; const content = [ new DeclarationBlock(this._declarationBlockConfig) .export() diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 2c546bb75a2..e0d3ff293a3 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -5,7 +5,7 @@ import { PluginFunction, Types, } from '@graphql-codegen/plugin-helpers'; -import { parseMapper } from '@graphql-codegen/visitor-plugin-common'; +import { parseMapper, type RootResolver } from '@graphql-codegen/visitor-plugin-common'; import { GraphQLSchema } from 'graphql'; import { TypeScriptResolversPluginConfig } from './config.js'; import { TypeScriptResolversVisitor } from './visitor.js'; @@ -15,10 +15,7 @@ const capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1) export const plugin: PluginFunction< TypeScriptResolversPluginConfig, Types.ComplexPluginOutput<{ - generatedResolverTypes: { - resolversMap: { name: string }; - userDefined: Record; - }; + generatedResolverTypes: RootResolver['generatedResolverTypes']; }> > = (schema: GraphQLSchema, documents: Types.DocumentFile[], config: TypeScriptResolversPluginConfig) => { const imports = []; From 587899fa7a3e2a93691aa808cffc232c189c850c Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 25 Aug 2024 23:12:20 +1000 Subject: [PATCH 9/9] Use _federation to keep track of its meta --- .changeset/fifty-dodos-marry.md | 6 +- .../src/base-resolvers-visitor.ts | 51 ++- .../other/visitor-plugin-common/src/types.ts | 5 + .../tests/ts-resolvers.federation.spec.ts | 411 +++++++++++++----- .../utils/plugins-helpers/src/federation.ts | 39 +- 5 files changed, 368 insertions(+), 144 deletions(-) diff --git a/.changeset/fifty-dodos-marry.md b/.changeset/fifty-dodos-marry.md index c92959bae84..aec4dc48134 100644 --- a/.changeset/fifty-dodos-marry.md +++ b/.changeset/fifty-dodos-marry.md @@ -4,4 +4,8 @@ '@graphql-codegen/plugin-helpers': minor --- -Avoid generating reference resolvers if federation object is not resolvable +Add `generateInternalResolversIfNeeded` option + +This option can be used to generate more correct types for internal resolvers. For example, only generate `__resolveReference` if the federation object has a resolvable `@key`. + +In the future, this option can be extended to support other internal resolvers e.g. `__isTypeOf` is only generated for implementing types and union members. 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 c7118cb3c21..cf3f6386c2d 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 @@ -33,6 +33,8 @@ import { ConvertOptions, DeclarationKind, EnumValuesMap, + type NormalizedGenerateInternalResolversIfNeededConfig, + type GenerateInternalResolversIfNeededConfig, NormalizedAvoidOptionalsConfig, NormalizedScalarsMap, ParsedEnumValuesMap, @@ -75,6 +77,7 @@ export interface ParsedResolversConfig extends ParsedConfig { resolverTypeSuffix: string; allResolversTypeName: string; internalResolversPrefix: string; + generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; @@ -577,6 +580,16 @@ export interface RawResolversConfig extends RawConfig { * If you are using `mercurius-js`, please set this field to empty string for better compatibility. */ internalResolversPrefix?: string; + /** + * @type object + * @default { __resolveReference: false } + * @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met. + * Enabling this allows a more correct type generation for the resolvers. + * For example: + * - `__isTypeOf` is generated for implementing types and union members + * - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive + */ + generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig; /** * @type boolean * @default false @@ -652,7 +665,6 @@ export class BaseResolversVisitor< [key: string]: { typename: string; baseGeneratedTypename?: string; - federation?: { hasResolveReference: boolean }; }; } = {}; protected _collectedDirectiveResolvers: { [key: string]: string } = {}; @@ -669,7 +681,6 @@ export class BaseResolversVisitor< protected _globalDeclarations = new Set(); protected _federation: ApolloFederation; protected _hasScalars = false; - protected _hasFederation = false; protected _fieldContextTypeMap: FieldContextTypeMap; protected _directiveContextTypesMap: FieldContextTypeMap; protected _checkedTypesWithNestedAbstractTypes: Record = {}; @@ -709,6 +720,9 @@ export class BaseResolversVisitor< mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix), scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars), internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'), + generateInternalResolversIfNeeded: { + __resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false, + }, resolversNonOptionalTypename: normalizeResolversNonOptionalTypename( getConfigValue(rawConfig.resolversNonOptionalTypename, false) ), @@ -1282,7 +1296,7 @@ export class BaseResolversVisitor< } public hasFederation(): boolean { - return this._hasFederation; + return Object.keys(this._federation.getMeta()).length > 0; } public getRootResolver(): RootResolver { @@ -1305,8 +1319,10 @@ export class BaseResolversVisitor< userDefinedTypes[schemaTypeName] = { name: resolverType.baseGeneratedTypename, }; - if (resolverType.federation) { - userDefinedTypes[schemaTypeName].federation = resolverType.federation; + + const federationMeta = this._federation.getMeta()[schemaTypeName]; + if (federationMeta) { + userDefinedTypes[schemaTypeName].federation = federationMeta; } } @@ -1492,9 +1508,20 @@ export class BaseResolversVisitor< }; if (this._federation.isResolveReferenceField(node)) { - this._hasFederation = true; - signature.type = 'ReferenceResolver'; + if (this.config.generateInternalResolversIfNeeded.__resolveReference) { + const federationDetails = checkObjectTypeFederationDetails( + parentType.astNode as ObjectTypeDefinitionNode, + this._schema + ); + + if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) { + return ''; + } + signature.modifier = ''; // if a federation type has resolvable @key, then it should be required + } + this._federation.setMeta(parentType.name, { hasResolveReference: true }); + signature.type = 'ReferenceResolver'; if (signature.genericTypes.length >= 3) { signature.genericTypes = signature.genericTypes.slice(0, 3); } @@ -1537,7 +1564,7 @@ export class BaseResolversVisitor< return `Partial<${argsType}>`; } - ObjectTypeDefinition(node: ObjectTypeDefinitionNode, key: number, parent: any): string { + ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string { const declarationKind = 'type'; const name = this.convertName(node, { suffix: this.config.resolverTypeSuffix, @@ -1589,14 +1616,6 @@ export class BaseResolversVisitor< baseGeneratedTypename: name, }; - if (this.config.federation) { - const originalNode = parent[key] as ObjectTypeDefinitionNode; - const federationDetails = checkObjectTypeFederationDetails(originalNode, this._schema); - this._collectedResolvers[node.name as any].federation = { - hasResolveReference: federationDetails ? federationDetails.resolvableKeyDirectives.length > 0 : false, - }; - } - return block.string; } diff --git a/packages/plugins/other/visitor-plugin-common/src/types.ts b/packages/plugins/other/visitor-plugin-common/src/types.ts index f275fa47af7..a9aa76ce36d 100644 --- a/packages/plugins/other/visitor-plugin-common/src/types.ts +++ b/packages/plugins/other/visitor-plugin-common/src/types.ts @@ -127,3 +127,8 @@ export interface ResolversNonOptionalTypenameConfig { interfaceImplementingType?: boolean; excludeTypes?: string[]; } + +export interface GenerateInternalResolversIfNeededConfig { + __resolveReference?: boolean; +} +export type NormalizedGenerateInternalResolversIfNeededConfig = Required; 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 a70081fd926..7aae74ee79c 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', () => { - it('should add __resolveReference to objects that have @key and is resolvable', async () => { + describe('adds __resolveReference', () => { const federatedSchema = /* GraphQL */ ` type Query { allUsers: [User] @@ -76,71 +76,207 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { } `; - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - }, - }); + it('when generateInternalResolversIfNeeded.__resolveReference = false, generates optional __resolveReference for object types with @key', async () => { + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, + }); - // User should have it - expect(content).toBeSimilarStringTo(` + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - `); + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // SingleResolvable should have __resolveReference because it has resolvable: true - expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + expect(content).toBeSimilarStringTo(` + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // SingleNonResolvable shouldn't have __resolveReference because it has resolvable: false - expect(content).toBeSimilarStringTo(` - export type SingleNonResolvableResolvers = { - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + expect(content).toBeSimilarStringTo(` + export type SingleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, ParentType, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // AtLeastOneResolvable should have __resolveReference because it at least one resolvable - expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + expect(content).toBeSimilarStringTo(` + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // MixedResolvable should have __resolveReference and references for resolvable keys - expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + expect(content).toBeSimilarStringTo(` + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // MultipleNonResolvableResolvers does not have __resolveReference because all keys are non-resolvable - expect(content).toBeSimilarStringTo(` - export type MultipleNonResolvableResolvers = { - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); + expect(content).toBeSimilarStringTo(` + export type MultipleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, ParentType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); - // Book shouldn't because it doesn't have @key - expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; - `); + // Book does NOT have __resolveReference because it doesn't have @key + expect(content).toBeSimilarStringTo(` + export type BookResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + }); + + 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(` + export type UserResolvers = { + __resolveReference: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // SingleResolvable has __resolveReference because it has resolvable: true + expect(content).toBeSimilarStringTo(` + export type SingleResolvableResolvers = { + __resolveReference: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // SingleNonResolvable does NOT have __resolveReference because it has resolvable: false + expect(content).toBeSimilarStringTo(` + export type SingleNonResolvableResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // AtLeastOneResolvable has __resolveReference because it at least one resolvable + expect(content).toBeSimilarStringTo(` + export type AtLeastOneResolvableResolvers = { + __resolveReference: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // MixedResolvable has __resolveReference and references for resolvable keys + expect(content).toBeSimilarStringTo(` + export type MixedResolvableResolvers = { + __resolveReference: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable + expect(content).toBeSimilarStringTo(` + export type MultipleNonResolvableResolvers = { + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // Book does NOT have __resolveReference because it doesn't have @key + expect(content).toBeSimilarStringTo(` + export type BookResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + }); }); it('should support extend keyword', async () => { @@ -670,7 +806,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); }); - it('meta - generates federation meta correctly', async () => { + describe('meta - generates federation meta correctly', () => { const federatedSchema = /* GraphQL */ ` scalar _FieldSet directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE @@ -730,70 +866,121 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { } `; - const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); + it('when generateInternalResolversIfNeeded.__resolveReference = false', async () => { + const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); - expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` - Object { - "resolversMap": Object { - "name": "Resolvers", - }, - "userDefined": Object { - "MultipleNonResolvable": Object { - "federation": Object { - "hasResolveReference": false, - }, - "name": "MultipleNonResolvableResolvers", + expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` + Object { + "resolversMap": Object { + "name": "Resolvers", }, - "MultipleResolvable": Object { - "federation": Object { - "hasResolveReference": true, + "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", }, - "name": "MultipleResolvableResolvers", }, - "Node": Object { - "name": "NodeResolvers", + } + `); + }); + + it('when generateInternalResolversIfNeeded.__resolveReference = true', async () => { + const result = await plugin( + buildSchema(federatedSchema), + [], + { federation: true, generateInternalResolversIfNeeded: { __resolveReference: true } }, + { outputFile: '' } + ); + + expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` + Object { + "resolversMap": Object { + "name": "Resolvers", }, - "NotResolvable": Object { - "federation": Object { - "hasResolveReference": false, + "userDefined": Object { + "MultipleNonResolvable": Object { + "name": "MultipleNonResolvableResolvers", }, - "name": "NotResolvableResolvers", - }, - "Query": Object { - "federation": Object { - "hasResolveReference": false, + "MultipleResolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "MultipleResolvableResolvers", }, - "name": "QueryResolvers", - }, - "Resolvable": Object { - "federation": Object { - "hasResolveReference": true, + "Node": Object { + "name": "NodeResolvers", }, - "name": "ResolvableResolvers", - }, - "User": Object { - "federation": Object { - "hasResolveReference": true, + "NotResolvable": Object { + "name": "NotResolvableResolvers", }, - "name": "UserResolvers", - }, - "UserError": Object { - "federation": Object { - "hasResolveReference": false, + "Query": Object { + "name": "QueryResolvers", }, - "name": "UserErrorResolvers", - }, - "UserOk": Object { - "federation": Object { - "hasResolveReference": false, + "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", }, - "name": "UserOkResolvers", - }, - "UserPayload": Object { - "name": "UserPayloadResolvers", }, - }, - } - `); + } + `); + }); }); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index c64ad3b16df..d77277629e7 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,7 +1,7 @@ import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; import { - type DirectiveNode, DefinitionNode, + DirectiveNode, FieldDefinitionNode, GraphQLNamedType, GraphQLObjectType, @@ -35,21 +35,18 @@ export const federationSpec = parse(/* GraphQL */ ` export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_TYPE]: type => { - const objectTypeFederationDetails = checkObjectTypeFederationDetails(type, schema); - - if (!objectTypeFederationDetails || objectTypeFederationDetails.resolvableKeyDirectives.length === 0) { - return type; + if (checkObjectTypeFederationDetails(type, schema)) { + const typeConfig = type.toConfig(); + typeConfig.fields = { + [resolveReferenceFieldName]: { + type, + }, + ...typeConfig.fields, + }; + + return new GraphQLObjectType(typeConfig); } - - const typeConfig = type.toConfig(); - typeConfig.fields = { - [resolveReferenceFieldName]: { - type, - }, - ...typeConfig.fields, - }; - - return new GraphQLObjectType(typeConfig); + return type; }, }); } @@ -85,10 +82,15 @@ 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 } = {}; constructor({ enabled, schema }: { enabled: boolean; schema: GraphQLSchema }) { this.enabled = enabled; @@ -199,6 +201,13 @@ export class ApolloFederation { return parentTypeSignature; } + setMeta(typename: string, update: Partial): void { + this.meta[typename] = { ...(this.meta[typename] || { hasResolveReference: false }), ...update }; + } + getMeta() { + return this.meta; + } + private isExternalAndNotProvided(fieldNode: FieldDefinitionNode, objectType: GraphQLObjectType): boolean { return this.isExternal(fieldNode) && !this.hasProvides(objectType, fieldNode); }