diff --git a/.changeset/fifty-dodos-marry.md b/.changeset/fifty-dodos-marry.md new file mode 100644 index 00000000000..aec4dc48134 --- /dev/null +++ b/.changeset/fifty-dodos-marry.md @@ -0,0 +1,11 @@ +--- +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-resolvers': minor +'@graphql-codegen/plugin-helpers': minor +--- + +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 08b7c572b0b..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 @@ -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 { @@ -33,6 +33,8 @@ import { ConvertOptions, DeclarationKind, EnumValuesMap, + type NormalizedGenerateInternalResolversIfNeededConfig, + type GenerateInternalResolversIfNeededConfig, NormalizedAvoidOptionalsConfig, NormalizedScalarsMap, ParsedEnumValuesMap, @@ -75,12 +77,20 @@ export interface ParsedResolversConfig extends ParsedConfig { resolverTypeSuffix: string; allResolversTypeName: string; internalResolversPrefix: string; + generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; } type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; +export interface RootResolver { + content: string; + generatedResolverTypes: { + resolversMap: { name: string }; + userDefined: Record; + }; +} export interface RawResolversConfig extends RawConfig { /** @@ -570,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 @@ -641,7 +661,12 @@ 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; + }; + } = {}; protected _collectedDirectiveResolvers: { [key: string]: string } = {}; protected _variablesTransformer: OperationVariablesToObject; protected _usedMappers: { [key: string]: boolean } = {}; @@ -656,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 = {}; @@ -696,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) ), @@ -1269,21 +1296,15 @@ export class BaseResolversVisitor< } public hasFederation(): boolean { - return this._hasFederation; + return Object.keys(this._federation.getMeta()).length > 0; } - public getRootResolver(): { - content: string; - generatedResolverTypes: { - resolversMap: { name: string }; - userDefined: Record; - }; - } { + public getRootResolver(): RootResolver { const name = this.convertName(this.config.allResolversTypeName); const declarationKind = 'type'; const contextType = ``; - const userDefinedTypes: Record = {}; + const userDefinedTypes: RootResolver['generatedResolverTypes']['userDefined'] = {}; const content = [ new DeclarationBlock(this._declarationBlockConfig) .export() @@ -1295,7 +1316,14 @@ export class BaseResolversVisitor< const resolverType = this._collectedResolvers[schemaTypeName]; if (resolverType.baseGeneratedTypename) { - userDefinedTypes[schemaTypeName] = { name: resolverType.baseGeneratedTypename }; + userDefinedTypes[schemaTypeName] = { + name: resolverType.baseGeneratedTypename, + }; + + const federationMeta = this._federation.getMeta()[schemaTypeName]; + if (federationMeta) { + userDefinedTypes[schemaTypeName].federation = federationMeta; + } } return indent(this.formatRootResolver(schemaTypeName, resolverType.typename, declarationKind)); @@ -1480,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); } 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/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 = []; 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..7aae74ee79c 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 () => { + describe('adds __resolveReference', () => { const federatedSchema = /* GraphQL */ ` type Query { allUsers: [User] @@ -39,23 +39,244 @@ 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({ - 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>; - `); - // Foo shouldn't because it doesn't have @key - expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, { __typename: 'Book' } & GraphQLRecursivePick, ContextType>; - `); + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type SingleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, ParentType, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick | GraphQLRecursivePick), ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + expect(content).toBeSimilarStringTo(` + export type MultipleNonResolvableResolvers = { + __resolveReference?: ReferenceResolver, ParentType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + + // Book does NOT have __resolveReference because it doesn't have @key + expect(content).toBeSimilarStringTo(` + export type BookResolvers = { + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + `); + }); + + 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 () => { @@ -584,4 +805,182 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(`id?: Resolver`); }); }); + + describe('meta - generates federation meta correctly', () => { + const federatedSchema = /* GraphQL */ ` + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE + + type Query { + user: UserPayload! + allUsers: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + + interface Node { + id: ID! + } + + type UserOk { + id: ID! + } + type UserError { + message: String! + } + union UserPayload = UserOk | UserError + + enum Country { + FR + US + } + + 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! + } + `; + + 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": 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", + }, + }, + } + `); + }); + + 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", + }, + "userDefined": Object { + "MultipleNonResolvable": Object { + "name": "MultipleNonResolvableResolvers", + }, + "MultipleResolvable": Object { + "federation": Object { + "hasResolveReference": true, + }, + "name": "MultipleResolvableResolvers", + }, + "Node": Object { + "name": "NodeResolvers", + }, + "NotResolvable": Object { + "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", + }, + }, + } + `); + }); + }); }); diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index de2a0c4a65a..d77277629e7 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -35,7 +35,7 @@ export const federationSpec = parse(/* GraphQL */ ` export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_TYPE]: type => { - if (isFederationObjectType(type, schema)) { + if (checkObjectTypeFederationDetails(type, schema)) { const typeConfig = type.toConfig(); typeConfig.fields = { [resolveReferenceFieldName]: { @@ -82,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; @@ -130,7 +135,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 +163,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 objectTypeFederationDetails = checkObjectTypeFederationDetails(parentType, this.schema); + if (!objectTypeFederationDetails) { + return parentTypeSignature; + } + + const { resolvableKeyDirectives } = objectTypeFederationDetails; - 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 +180,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); }); @@ -192,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); } @@ -275,7 +291,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 +): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { const { name: { value: name }, directives, @@ -284,9 +303,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 { resolvableKeyDirectives }; } /**