diff --git a/.changeset/loud-suits-admire.md b/.changeset/loud-suits-admire.md new file mode 100644 index 00000000000..3ecc3cbef0e --- /dev/null +++ b/.changeset/loud-suits-admire.md @@ -0,0 +1,10 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +Ensure Federation Interfaces have `__resolveReference` if they are resolvable entities + +BREAKING CHANGES: Deprecate `onlyResolveTypeForInterfaces` because majority of use cases cannot implement resolvers in Interfaces. +BREAKING CHANGES: Deprecate `generateInternalResolversIfNeeded.__resolveReference` because types do not have `__resolveReference` if they are not Federation entities or are not resolvable. Users should not have to manually set this option. This option was put in to wait for this major version. diff --git a/.changeset/thick-pianos-smoke.md b/.changeset/thick-pianos-smoke.md new file mode 100644 index 00000000000..569664420a9 --- /dev/null +++ b/.changeset/thick-pianos-smoke.md @@ -0,0 +1,11 @@ +--- +'@graphql-codegen/visitor-plugin-common': major +'@graphql-codegen/typescript-resolvers': major +'@graphql-codegen/plugin-helpers': major +--- + +Fix `mappers` usage with Federation + +`mappers` was previously used as `__resolveReference`'s first param (usually called "reference"). However, this is incorrect because `reference` interface comes directly from `@key` and `@requires` directives. This patch fixes the issue by creating a new `FederationTypes` type and use it as the base for federation entity types when being used to type entity references. + +BREAKING CHANGES: No longer generate `UnwrappedObject` utility type, as this was used to support the wrong previously generated type. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ff2632247d2..5ce63e4885b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - federation-fixes # FIXME: Remove this line after the PR is merged env: NODE_OPTIONS: '--max_old_space_size=4096' diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index d2c05ac9d13..ba1feb00f47 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -128,6 +128,11 @@ export type DirectiveResolverFn TResult | Promise; +/** Mapping of federation types */ +export type FederationTypes = { + User: User; +}; + /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Address: ResolverTypeWrapper
; @@ -190,13 +195,14 @@ export type QueryResolvers< export type UserResolvers< ContextType = any, - ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], + FederationType extends FederationTypes['User'] = FederationTypes['User'] > = { __resolveReference?: ReferenceResolver< Maybe, { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ), ContextType >; @@ -204,10 +210,10 @@ export type UserResolvers< email?: Resolver< ResolversTypes['String'], { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) & - GraphQLRecursivePick, + GraphQLRecursivePick, ContextType >; diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index 6adb71df597..bcdb478d8b0 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -1,4 +1,4 @@ -import { ApolloFederation, checkObjectTypeFederationDetails, getBaseType } from '@graphql-codegen/plugin-helpers'; +import { ApolloFederation, type FederationMeta, getBaseType } from '@graphql-codegen/plugin-helpers'; import { getRootTypeNames } from '@graphql-tools/utils'; import autoBind from 'auto-bind'; import { @@ -78,13 +78,15 @@ export interface ParsedResolversConfig extends ParsedConfig { allResolversTypeName: string; internalResolversPrefix: string; generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig; - onlyResolveTypeForInterfaces: boolean; directiveResolverMappings: Record; resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig; avoidCheckingAbstractTypesRecursively: boolean; } -type FieldDefinitionPrintFn = (parentName: string, avoidResolverOptionals: boolean) => string | null; +type FieldDefinitionPrintFn = ( + parentName: string, + avoidResolverOptionals: boolean +) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } }; export interface RootResolver { content: string; generatedResolverTypes: { @@ -584,20 +586,13 @@ export interface RawResolversConfig extends RawConfig { internalResolversPrefix?: string; /** * @type object - * @default { __resolveReference: false } + * @default {} * @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met. * Enabling this allows a more correct type generation for the resolvers. * For example: * - `__isTypeOf` is generated for implementing types and union members - * - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive */ generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig; - /** - * @type boolean - * @default false - * @description Turning this flag to `true` will generate resolver signature that has only `resolveType` for interfaces, forcing developers to write inherited type resolvers in the type itself. - */ - onlyResolveTypeForInterfaces?: boolean; /** * @description Makes `__typename` of resolver mappings non-optional without affecting the base types. * @default false @@ -700,7 +695,8 @@ export class BaseResolversVisitor< rawConfig: TRawConfig, additionalConfig: TPluginConfig, private _schema: GraphQLSchema, - defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS + defaultScalars: NormalizedScalarsMap = DEFAULT_SCALARS, + federationMeta: FederationMeta = {} ) { super(rawConfig, { immutableTypes: getConfigValue(rawConfig.immutableTypes, false), @@ -714,7 +710,6 @@ export class BaseResolversVisitor< mapOrStr: rawConfig.enumValues, }), addUnderscoreToArgsType: getConfigValue(rawConfig.addUnderscoreToArgsType, false), - onlyResolveTypeForInterfaces: getConfigValue(rawConfig.onlyResolveTypeForInterfaces, false), contextType: parseMapper(rawConfig.contextType || 'any', 'ContextType'), fieldContextTypes: getConfigValue(rawConfig.fieldContextTypes, []), directiveContextTypes: getConfigValue(rawConfig.directiveContextTypes, []), @@ -729,9 +724,7 @@ export class BaseResolversVisitor< mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix), scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars), internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'), - generateInternalResolversIfNeeded: { - __resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false, - }, + generateInternalResolversIfNeeded: {}, resolversNonOptionalTypename: normalizeResolversNonOptionalTypename( getConfigValue(rawConfig.resolversNonOptionalTypename, false) ), @@ -740,7 +733,11 @@ export class BaseResolversVisitor< } as TPluginConfig); autoBind(this); - this._federation = new ApolloFederation({ enabled: this.config.federation, schema: this.schema }); + this._federation = new ApolloFederation({ + enabled: this.config.federation, + schema: this.schema, + meta: federationMeta, + }); this._rootTypeNames = getRootTypeNames(_schema); this._variablesTransformer = new OperationVariablesToObject( this.scalars, @@ -1223,6 +1220,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; } @@ -1336,7 +1355,9 @@ export class BaseResolversVisitor< const federationMeta = this._federation.getMeta()[schemaTypeName]; if (federationMeta) { - userDefinedTypes[schemaTypeName].federation = federationMeta; + userDefinedTypes[schemaTypeName].federation = { + hasResolveReference: federationMeta.hasResolveReference, + }; } } @@ -1452,9 +1473,10 @@ export class BaseResolversVisitor< const baseType = getBaseTypeNode(original.type); const realType = baseType.name.value; const parentType = this.schema.getType(parentName); + const meta: ReturnType['meta'] = {}; if (this._federation.skipField({ fieldNode: original, parentType })) { - return null; + return { value: null, meta }; } const contextType = this.getContextType(parentName, node); @@ -1494,10 +1516,11 @@ export class BaseResolversVisitor< } } - const parentTypeSignature = this._federation.transformParentType({ + const parentTypeSignature = this._federation.transformFieldParentType({ fieldNode: original, parentType, parentTypeSignature: this.getParentTypeForSignature(node), + federationTypeSignature: 'FederationType', }); const mappedTypeKey = isSubscriptionType ? `${mappedType}, "${node.name}"` : mappedType; @@ -1522,29 +1545,22 @@ export class BaseResolversVisitor< }; if (this._federation.isResolveReferenceField(node)) { - if (this.config.generateInternalResolversIfNeeded.__resolveReference) { - const federationDetails = checkObjectTypeFederationDetails( - parentType.astNode as ObjectTypeDefinitionNode, - this._schema - ); - - if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) { - return ''; - } + if (!this._federation.getMeta()[parentType.name].hasResolveReference) { + return { value: '', meta }; } - - this._federation.setMeta(parentType.name, { hasResolveReference: true }); signature.type = 'ReferenceResolver'; - if (signature.genericTypes.length >= 3) { - signature.genericTypes = signature.genericTypes.slice(0, 3); - } + signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType]; + meta.federation = { isResolveReference: true }; } - return indent( - `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( - ', ' - )}>${this.getPunctuation(declarationKind)}` - ); + return { + value: indent( + `${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join( + ', ' + )}>${this.getPunctuation(declarationKind)}` + ), + meta, + }; }; } @@ -1605,7 +1621,7 @@ export class BaseResolversVisitor< (rootType === 'mutation' && this.config.avoidOptionals.mutation) || (rootType === 'subscription' && this.config.avoidOptionals.subscription) || (rootType === false && this.config.avoidOptionals.resolvers) - ); + ).value; }); if (!rootType) { @@ -1618,10 +1634,20 @@ export class BaseResolversVisitor< ); } + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationTypes'), + typeName, + }); + 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] = { @@ -1805,25 +1831,44 @@ export class BaseResolversVisitor< } const parentType = this.getParentTypeToUse(typeName); + + const genericTypes: string[] = [ + `ContextType = ${this.config.contextType.type}`, + this.transformParentGenericType(parentType), + ]; + this._federation.addFederationTypeGenericIfApplicable({ + genericTypes, + federationTypesType: this.convertName('FederationTypes'), + typeName, + }); + const possibleTypes = implementingTypes.map(name => `'${name}'`).join(' | ') || 'null'; - const fields = this.config.onlyResolveTypeForInterfaces ? [] : node.fields || []; + + // An Interface has __resolveType resolver, and no other fields. + const blockFields: string[] = [ + indent( + `${this.config.internalResolversPrefix}resolveType${ + this.config.optionalResolveType ? '?' : '' + }: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}` + ), + ]; + + // An Interface in Federation may have the additional __resolveReference resolver, if resolvable. + // So, we filter out the normal fields declared on the Interface and add the __resolveReference resolver. + const fields = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f => + f(typeName, this.config.avoidOptionals.resolvers) + ); + for (const field of fields) { + if (field.meta.federation?.isResolveReference) { + blockFields.push(field.value); + } + } return new DeclarationBlock(this._declarationBlockConfig) .export() .asKind(declarationKind) - .withName(name, ``) - .withBlock( - [ - indent( - `${this.config.internalResolversPrefix}resolveType${ - this.config.optionalResolveType ? '?' : '' - }: TypeResolveFn<${possibleTypes}, ParentType, ContextType>${this.getPunctuation(declarationKind)}` - ), - ...(fields as unknown as FieldDefinitionPrintFn[]).map(f => - f(typeName, this.config.avoidOptionals.resolvers) - ), - ].join('\n') - ).string; + .withName(name, `<${genericTypes.join(', ')}>`) + .withBlock(blockFields.join('\n')).string; } SchemaDefinition() { diff --git a/packages/plugins/other/visitor-plugin-common/src/types.ts b/packages/plugins/other/visitor-plugin-common/src/types.ts index 16f64e0f029..e2e2004bfea 100644 --- a/packages/plugins/other/visitor-plugin-common/src/types.ts +++ b/packages/plugins/other/visitor-plugin-common/src/types.ts @@ -139,7 +139,5 @@ export interface CustomDirectivesConfig { apolloUnmask?: boolean; } -export interface GenerateInternalResolversIfNeededConfig { - __resolveReference?: boolean; -} +export interface GenerateInternalResolversIfNeededConfig {} export type NormalizedGenerateInternalResolversIfNeededConfig = Required; diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index e0d3ff293a3..3518dbb0004 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -75,8 +75,14 @@ export type Resolver${capitalizedDirectiveName}WithResolve = { 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, @@ -244,6 +243,7 @@ export type DirectiveResolverFn TResult | Promise; `; + const federationTypes = visitor.buildFederationTypes(); const resolversTypeMapping = visitor.buildResolversTypes(); const resolversParentTypeMapping = visitor.buildResolversParentTypes(); const resolversUnionTypesMapping = visitor.buildResolversUnionTypes(); @@ -287,6 +287,7 @@ export type DirectiveResolverFn { - constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema) { + constructor(pluginConfig: TypeScriptResolversPluginConfig, schema: GraphQLSchema, federationMeta: FederationMeta) { super( pluginConfig, { @@ -41,7 +36,9 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor< allowParentTypeOverride: getConfigValue(pluginConfig.allowParentTypeOverride, false), optionalInfoArgument: getConfigValue(pluginConfig.optionalInfoArgument, false), } as ParsedTypeScriptResolversConfig, - schema + schema, + DEFAULT_SCALARS, + federationMeta ); autoBind(this); this.setVariablesTransformer( @@ -96,13 +93,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..eeda2eb6b4d 100644 --- a/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap +++ b/packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap @@ -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 ); @@ -271,7 +272,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -281,19 +281,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ @@ -425,6 +420,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 ); @@ -530,7 +526,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -540,19 +535,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ @@ -770,6 +760,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: ( Omit & { parent?: Maybe<_RefType['MyType']> } ) | ( MyOtherType ); @@ -875,7 +866,6 @@ export type SubscriptionResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }>; export type SomeNodeResolvers = ResolversObject<{ @@ -885,19 +875,14 @@ export type SomeNodeResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - id?: Resolver; }>; export type WithChildResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithChild' | 'AnotherNodeWithAll', ParentType, ContextType>; - unionChild?: Resolver, ParentType, ContextType>; - node?: Resolver, ParentType, ContextType>; }>; export type WithChildrenResolvers = ResolversObject<{ __resolveType: TypeResolveFn<'AnotherNodeWithAll', ParentType, ContextType>; - unionChildren?: Resolver, ParentType, ContextType>; - nodes?: Resolver, ParentType, ContextType>; }>; export type AnotherNodeWithChildResolvers = ResolversObject<{ diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts index ff7b7730a36..42918d7b710 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.config.avoidOptionals.spec.ts @@ -53,7 +53,6 @@ describe('TypeScript Resolvers Plugin - config.avoidOptionals', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts new file mode 100644 index 00000000000..8ef9616ff40 --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -0,0 +1,198 @@ +import '@graphql-codegen/testing'; +import { generate } from './utils'; + +describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { + it('generates __resolveReference for Interfaces with @key', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + me: Person + } + + interface Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type User implements Person @key(fields: "id") { + id: ID! + name: PersonName! + } + + type Admin implements Person @key(fields: "id") { + id: ID! + name: PersonName! + canImpersonate: Boolean! + } + + type PersonName { + first: String! + last: String! + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, + }); + + expect(content).toMatchInlineSnapshot(` + "import { GraphQLResolveInfo } from 'graphql'; + + + export type ResolverTypeWrapper = Promise | T; + + export type ReferenceResolver = ( + reference: TReference, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + type ScalarCheck = S extends true ? T : NullableCheck; + type NullableCheck = Maybe extends T ? Maybe, S>> : ListCheck; + type ListCheck = T extends (infer U)[] ? NullableCheck[] : GraphQLRecursivePick; + export type GraphQLRecursivePick = { [K in keyof T & keyof S]: ScalarCheck }; + + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = ResolverFn | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => AsyncIterable | Promise>; + + export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; + } + + export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; + } + + export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + + export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + + export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo + ) => Maybe | Promise>; + + export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + + export type NextResolverFn = () => Promise; + + export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + /** Mapping of federation types */ + export type FederationTypes = { + Person: Person; + User: User; + Admin: Admin; + }; + + + /** Mapping of interface types */ + export type ResolversInterfaceTypes<_RefType extends Record> = { + Person: ( User ) | ( Admin ); + }; + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>; + Person: ResolverTypeWrapper['Person']>; + ID: ResolverTypeWrapper; + User: ResolverTypeWrapper; + Admin: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + PersonName: ResolverTypeWrapper; + String: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + Query: {}; + Person: ResolversInterfaceTypes['Person']; + ID: Scalars['ID']['output']; + User: User; + Admin: Admin; + Boolean: Scalars['Boolean']['output']; + PersonName: PersonName; + String: Scalars['String']['output']; + }; + + export type QueryResolvers = { + me?: Resolver, ParentType, ContextType>; + }; + + export type PersonResolvers = { + __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; + __resolveReference?: ReferenceResolver, { __typename: 'Person' } & GraphQLRecursivePick, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type AdminResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'Admin' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver; + canImpersonate?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type PersonNameResolvers = { + first?: Resolver; + last?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type Resolvers = { + Query?: QueryResolvers; + Person?: PersonResolvers; + User?: UserResolvers; + Admin?: AdminResolvers; + PersonName?: PersonNameResolvers; + }; + + " + `); + }); +}); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts new file mode 100644 index 00000000000..9db647c00cc --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -0,0 +1,168 @@ +import '@graphql-codegen/testing'; +import { generate } from './utils'; + +describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { + it('generates FederationTypes and use it for reference type', async () => { + const federatedSchema = /* GraphQL */ ` + type Query { + me: User + } + + type User @key(fields: "id") { + id: ID! + name: String + } + + type UserProfile { + id: ID! + user: User! + } + `; + + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + mappers: { + User: './mappers#UserMapper', + }, + }, + }); + + // User should have it + expect(content).toMatchInlineSnapshot(` + "import { GraphQLResolveInfo } from 'graphql'; + import { UserMapper } from './mappers'; + export type Omit = Pick>; + + + export type ResolverTypeWrapper = Promise | T; + + export type ReferenceResolver = ( + reference: TReference, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + type ScalarCheck = S extends true ? T : NullableCheck; + type NullableCheck = Maybe extends T ? Maybe, S>> : ListCheck; + type ListCheck = T extends (infer U)[] ? NullableCheck[] : GraphQLRecursivePick; + export type GraphQLRecursivePick = { [K in keyof T & keyof S]: ScalarCheck }; + + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = ResolverFn | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => Promise | TResult; + + export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => AsyncIterable | Promise>; + + export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; + } + + export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; + } + + export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + + export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + + export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo + ) => Maybe | Promise>; + + export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + + export type NextResolverFn = () => Promise; + + export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo + ) => TResult | Promise; + + /** Mapping of federation types */ + export type FederationTypes = { + User: User; + }; + + + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + String: ResolverTypeWrapper; + UserProfile: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + Boolean: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + Query: {}; + User: UserMapper; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + UserProfile: Omit & { user: ResolversParentTypes['User'] }; + Boolean: Scalars['Boolean']['output']; + }; + + export type QueryResolvers = { + me?: Resolver, ParentType, ContextType>; + }; + + export type UserResolvers = { + __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type UserProfileResolvers = { + id?: Resolver; + user?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + + export type Resolvers = { + Query?: QueryResolvers; + User?: UserResolvers; + UserProfile?: UserProfileResolvers; + }; + + " + `); + }); +}); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts index 1cae62b4c31..71723b55187 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -24,7 +24,7 @@ function generate({ schema, config }: { schema: string; config: TypeScriptResolv } describe('TypeScript Resolvers Plugin + Apollo Federation', () => { - describe('adds __resolveReference', () => { + it('generates __resolveReference for object types with resolvable @key', async () => { const federatedSchema = /* GraphQL */ ` type Query { allUsers: [User] @@ -76,143 +76,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { } `; - it('when generateInternalResolversIfNeeded.__resolveReference = false, generates optional __resolveReference for object types with @key', async () => { - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - }, - }); - - expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'SingleResolvable' } & GraphQLRecursivePick, ContextType>; - id?: Resolver; - __isTypeOf?: IsTypeOfResolverFn; - }; - `); - - expect(content).toBeSimilarStringTo(` - export type SingleNonResolvableResolvers = { - __resolveReference?: ReferenceResolver, 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; - }; - `); + const content = await generate({ + schema: federatedSchema, + config: { + federation: true, + }, }); - it('when generateInternalResolversIfNeeded.__resolveReference = true, generates required __resolveReference for object types with resolvable @key', async () => { - const federatedSchema = /* GraphQL */ ` - type Query { - allUsers: [User] - } - - type User @key(fields: "id") { - id: ID! - name: String - username: String - } - - type Book { - id: ID! - } - - type SingleResolvable @key(fields: "id", resolvable: true) { - id: ID! - } - - type SingleNonResolvable @key(fields: "id", resolvable: false) { - id: ID! - } - - type AtLeastOneResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - - type MixedResolvable - @key(fields: "id") - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - - type MultipleNonResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: false) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - generateInternalResolversIfNeeded: { __resolveReference: true }, - }, - }); - - // User should have __resolveReference because it has resolvable @key (by default) - expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, { __typename: 'User' } & GraphQLRecursivePick, ContextType>; + // 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>; @@ -220,27 +94,27 @@ 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>; + // 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(` + // 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>; + // 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; @@ -248,10 +122,10 @@ 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>; + // 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; @@ -259,8 +133,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable - expect(content).toBeSimilarStringTo(` + // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable + expect(content).toBeSimilarStringTo(` export type MultipleNonResolvableResolvers = { id?: Resolver; id2?: Resolver; @@ -269,14 +143,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); - // Book does NOT have __resolveReference because it doesn't have @key - expect(content).toBeSimilarStringTo(` + // Book does NOT have __resolveReference because it doesn't have @key + expect(content).toBeSimilarStringTo(` export type BookResolvers = { id?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; `); - }); }); it('should support extend keyword', async () => { @@ -305,11 +178,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 +218,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 +259,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 +296,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 +329,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 +362,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; @@ -498,50 +371,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); }); - it.skip('should handle interface types', async () => { - const federatedSchema = /* GraphQL */ ` - type Query { - people: [Person] - } - - extend interface Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name") - } - - extend type User implements Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name { first last }") - username: String - } - - type Admin implements Person @key(fields: "name { first last }") { - name: Name! @external - age: Int @requires(fields: "name { first last }") - permissions: [String!]! - } - - extend type Name { - first: String! @external - last: String! @external - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - }, - }); - - expect(content).toBeSimilarStringTo(` - export type PersonResolvers = { - __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - age?: Resolver, { __typename: 'User' | 'Admin' } & GraphQLRecursivePick, ContextType>; - }; - `); - }); - it('should skip to generate resolvers of fields with @external directive', async () => { const federatedSchema = /* GraphQL */ ` type Query { @@ -568,10 +397,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 +524,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,175 +592,68 @@ 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 () => { + describe('meta', () => { + it('generates federation meta correctly', async () => { const federatedSchema = /* GraphQL */ ` - type User @key(fields: "id") { - id: ID! - } - `; - - const content = await generate({ - schema: federatedSchema, - config: { - federation: true, - wrapFieldDefinitions: true, - }, - }); + scalar _FieldSet + directive @key(fields: _FieldSet!, resolvable: Boolean) repeatable on OBJECT | INTERFACE - expect(content).toBeSimilarStringTo(`type UnwrappedObject = {`); - }); + type Query { + user: UserPayload! + allUsers: [User] + } - it('should add UnwrappedObject around ParentType for __resloveReference', async () => { - const federatedSchema = /* GraphQL */ ` type User @key(fields: "id") { id: ID! + name: String + username: String } - `; - - 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 - 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 - } + interface Node { + id: ID! + } - type NotResolvable @key(fields: "id", resolvable: false) { - id: ID! - } + type UserOk { + id: ID! + } + type UserError { + message: String! + } + union UserPayload = UserOk | UserError - type Resolvable @key(fields: "id", resolvable: true) { - id: ID! - } + enum Country { + FR + US + } - type MultipleResolvable - @key(fields: "id") - @key(fields: "id2", resolvable: true) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } + type NotResolvable @key(fields: "id", resolvable: false) { + id: ID! + } - type MultipleNonResolvable - @key(fields: "id", resolvable: false) - @key(fields: "id2", resolvable: false) - @key(fields: "id3", resolvable: false) { - id: ID! - id2: ID! - id3: ID! - } - `; + type Resolvable @key(fields: "id", resolvable: true) { + id: ID! + } - it('when generateInternalResolversIfNeeded.__resolveReference = false', async () => { - const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); + type MultipleResolvable + @key(fields: "id") + @key(fields: "id2", resolvable: true) + @key(fields: "id3", resolvable: false) { + id: ID! + id2: ID! + id3: ID! + } - expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` - Object { - "resolversMap": Object { - "name": "Resolvers", - }, - "userDefined": Object { - "MultipleNonResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "MultipleNonResolvableResolvers", - }, - "MultipleResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "MultipleResolvableResolvers", - }, - "Node": Object { - "name": "NodeResolvers", - }, - "NotResolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "NotResolvableResolvers", - }, - "Query": Object { - "name": "QueryResolvers", - }, - "Resolvable": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "ResolvableResolvers", - }, - "User": Object { - "federation": Object { - "hasResolveReference": true, - }, - "name": "UserResolvers", - }, - "UserError": Object { - "name": "UserErrorResolvers", - }, - "UserOk": Object { - "name": "UserOkResolvers", - }, - "UserPayload": Object { - "name": "UserPayloadResolvers", - }, - }, + type MultipleNonResolvable + @key(fields: "id", resolvable: false) + @key(fields: "id2", resolvable: false) + @key(fields: "id3", resolvable: false) { + id: ID! + id2: ID! + id3: ID! } - `); - }); + `; - it('when generateInternalResolversIfNeeded.__resolveReference = true', async () => { - const result = await plugin( - buildSchema(federatedSchema), - [], - { federation: true, generateInternalResolversIfNeeded: { __resolveReference: true } }, - { outputFile: '' } - ); + const result = await plugin(buildSchema(federatedSchema), [], { federation: true }, { outputFile: '' }); expect(result.meta?.generatedResolverTypes).toMatchInlineSnapshot(` Object { diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts index 332216111f6..64e997ac25d 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.interface.spec.ts @@ -162,7 +162,6 @@ describe('TypeScript Resolvers Plugin - Interfaces', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn; - id?: Resolver; }; `); }); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts index c595ebe7041..17e67670276 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.mapping.spec.ts @@ -1265,7 +1265,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1348,7 +1347,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1429,7 +1427,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1499,7 +1496,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1569,7 +1565,6 @@ describe('TypeScript Resolvers Plugin - Mapping', () => { expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts index a1d13fe829e..8028e1522af 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.spec.ts @@ -85,19 +85,6 @@ describe('TypeScript Resolvers Plugin', () => { }); describe('Config', () => { - it('onlyResolveTypeForInterfaces - should allow to have only resolveType for interfaces', async () => { - const config = { - onlyResolveTypeForInterfaces: true, - }; - const result = await plugin(resolversTestingSchema, [], config, { outputFile: '' }); - const content = await resolversTestingValidate(result, config, resolversTestingSchema); - - expect(content).toBeSimilarStringTo(` - export type NodeResolvers = { - __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - };`); - }); - it('optionalInfoArgument - should allow to have optional info argument', async () => { const config = { noSchemaStitching: true, @@ -655,7 +642,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType?: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); }); @@ -705,7 +691,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -778,7 +763,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -869,7 +853,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -944,7 +927,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1018,7 +1000,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -1093,7 +1074,6 @@ __isTypeOf?: IsTypeOfResolverFn; expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { __resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); @@ -2360,7 +2340,6 @@ export type ResolverFn = ( expect(result.content).toBeSimilarStringTo(` export type NodeResolvers = { resolveType: TypeResolveFn<'SomeNode', ParentType, ContextType>; - id?: Resolver; }; `); diff --git a/packages/plugins/typescript/resolvers/tests/utils.ts b/packages/plugins/typescript/resolvers/tests/utils.ts new file mode 100644 index 00000000000..20f77f1ac05 --- /dev/null +++ b/packages/plugins/typescript/resolvers/tests/utils.ts @@ -0,0 +1,15 @@ +import { codegen } from '@graphql-codegen/core'; +import { parse } from 'graphql'; +import { TypeScriptResolversPluginConfig } from '../src/config.js'; +import { plugin } from '../src/index.js'; + +export function generate({ schema, config }: { schema: string; config: TypeScriptResolversPluginConfig }) { + return codegen({ + filename: 'graphql.ts', + schema: parse(schema), + documents: [], + plugins: [{ 'typescript-resolvers': {} }], + config, + pluginMap: { 'typescript-resolvers': { plugin } }, + }); +} diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index d77277629e7..3ac608bf604 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -1,11 +1,13 @@ -import { astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { astFromInterfaceType, astFromObjectType, getRootTypeNames, MapperKind, mapSchema } from '@graphql-tools/utils'; import { DefinitionNode, DirectiveNode, FieldDefinitionNode, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLSchema, + isInterfaceType, isObjectType, ObjectTypeDefinitionNode, OperationDefinitionNode, @@ -28,14 +30,67 @@ export const federationSpec = parse(/* GraphQL */ ` directive @key(fields: _FieldSet!) on OBJECT | INTERFACE `); +interface TypeMeta { + hasResolveReference: boolean; + resolvableKeyDirectives: readonly DirectiveNode[]; +} + +export type FederationMeta = { [typeName: string]: TypeMeta }; + /** - * Adds `__resolveReference` in each ObjectType involved in Federation. + * Adds `__resolveReference` in each ObjectType and InterfaceType involved in Federation. + * We do this to utilise the existing FieldDefinition logic of the plugin, which includes many logic: + * - mapper + * - return type * @param schema */ -export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema, { +export function addFederationReferencesToSchema(schema: GraphQLSchema): { + transformedSchema: GraphQLSchema; + federationMeta: FederationMeta; +} { + const setFederationMeta = ({ + meta, + typeName, + update, + }: { + meta: FederationMeta; + typeName: string; + update: TypeMeta; + }): void => { + meta[typeName] = { ...(meta[typeName] || { hasResolveReference: false, resolvableKeyDirectives: [] }), ...update }; + }; + + const federationMeta: FederationMeta = {}; + + const transformedSchema = mapSchema(schema, { + [MapperKind.INTERFACE_TYPE]: type => { + const federationDetails = checkTypeFederationDetails(type, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { + const typeConfig = type.toConfig(); + typeConfig.fields = { + [resolveReferenceFieldName]: { + type, + }, + ...typeConfig.fields, + }; + + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + }, + }); + + return new GraphQLInterfaceType(typeConfig); + } + + return type; + }, [MapperKind.OBJECT_TYPE]: type => { - if (checkObjectTypeFederationDetails(type, schema)) { + const federationDetails = checkTypeFederationDetails(type, schema); + if (federationDetails && federationDetails.resolvableKeyDirectives.length > 0) { const typeConfig = type.toConfig(); typeConfig.fields = { [resolveReferenceFieldName]: { @@ -44,11 +99,25 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): GraphQLS ...typeConfig.fields, }; + setFederationMeta({ + meta: federationMeta, + typeName: type.name, + update: { + hasResolveReference: true, + resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, + }, + }); + return new GraphQLObjectType(typeConfig); } return type; }, }); + + return { + transformedSchema, + federationMeta, + }; } /** @@ -82,20 +151,17 @@ export function removeFederation(schema: GraphQLSchema): GraphQLSchema { const resolveReferenceFieldName = '__resolveReference'; -interface TypeMeta { - hasResolveReference: boolean; -} - export class ApolloFederation { private enabled = false; private schema: GraphQLSchema; private providesMap: Record; - protected meta: { [typename: string]: TypeMeta } = {}; + protected meta: FederationMeta = {}; - constructor({ enabled, schema }: { enabled: boolean; schema: GraphQLSchema }) { + constructor({ enabled, schema, meta }: { enabled: boolean; schema: GraphQLSchema; meta: FederationMeta }) { this.enabled = enabled; this.schema = schema; this.providesMap = this.createMapOfProvides(); + this.meta = meta; } /** @@ -135,7 +201,11 @@ export class ApolloFederation { * @param data */ skipField({ fieldNode, parentType }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType }): boolean { - if (!this.enabled || !isObjectType(parentType) || !checkObjectTypeFederationDetails(parentType, this.schema)) { + if ( + !this.enabled || + !(isObjectType(parentType) && !isInterfaceType(parentType)) || + !checkTypeFederationDetails(parentType, this.schema) + ) { return false; } @@ -148,62 +218,81 @@ export class ApolloFederation { } /** - * Transforms ParentType signature in ObjectTypes involved in Federation + * Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation * @param data */ - transformParentType({ + transformFieldParentType({ fieldNode, parentType, parentTypeSignature, + federationTypeSignature, }: { fieldNode: FieldDefinitionNode; parentType: GraphQLNamedType; parentTypeSignature: string; + federationTypeSignature: string; }) { - if ( - this.enabled && + if (!this.enabled) { + return parentTypeSignature; + } + + const parentTypeMeta = this.getMeta()[parentType.name]; + if (!parentTypeMeta?.hasResolveReference) { + return parentTypeSignature; + } + + const isObjectFieldWithFederationRef = isObjectType(parentType) && - (isTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName) - ) { - const objectTypeFederationDetails = checkObjectTypeFederationDetails(parentType, this.schema); - if (!objectTypeFederationDetails) { - return parentTypeSignature; - } + (nodeHasTypeExtension(parentType, this.schema) || fieldNode.name.value === resolveReferenceFieldName); - const { resolvableKeyDirectives } = objectTypeFederationDetails; + const isInterfaceFieldWithFederationRef = + isInterfaceType(parentType) && fieldNode.name.value === resolveReferenceFieldName; - if (resolvableKeyDirectives.length) { - const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; + if (!isObjectFieldWithFederationRef && !isInterfaceFieldWithFederationRef) { + return parentTypeSignature; + } - // Look for @requires and see what the service needs and gets - const requires = getDirectivesByName('requires', fieldNode).map(this.extractFieldSet); - const requiredFields = this.translateFieldSet(merge({}, ...requires), parentTypeSignature); + const outputs: string[] = [`{ __typename: '${parentType.name}' } &`]; - // @key() @key() - "primary keys" in Federation - const primaryKeys = resolvableKeyDirectives.map(def => { - const fields = this.extractFieldSet(def); - return this.translateFieldSet(fields, parentTypeSignature); - }); + // Look for @requires and see what the service needs and gets + const requires = getDirectivesByName('requires', fieldNode).map(this.extractFieldSet); + const requiredFields = this.translateFieldSet(merge({}, ...requires), federationTypeSignature); - const [open, close] = primaryKeys.length > 1 ? ['(', ')'] : ['', '']; + // @key() @key() - "primary keys" in Federation + const primaryKeys = parentTypeMeta.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 parentTypeSignature; + return outputs.join(' '); } - setMeta(typename: string, update: Partial): void { - this.meta[typename] = { ...(this.meta[typename] || { hasResolveReference: false }), ...update }; + addFederationTypeGenericIfApplicable({ + genericTypes, + typeName, + federationTypesType, + }: { + genericTypes: string[]; + typeName: string; + federationTypesType: string; + }): void { + if (!this.getMeta()[typeName]) { + return; + } + + const typeRef = `${federationTypesType}['${typeName}']`; + genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); } + getMeta() { return this.meta; } @@ -291,14 +380,18 @@ export class ApolloFederation { * Checks if Object Type is involved in Federation. Based on `@key` directive * @param node Type */ -export function checkObjectTypeFederationDetails( - node: ObjectTypeDefinitionNode | GraphQLObjectType, +function checkTypeFederationDetails( + node: ObjectTypeDefinitionNode | GraphQLObjectType | GraphQLInterfaceType, schema: GraphQLSchema ): { resolvableKeyDirectives: readonly DirectiveNode[] } | false { const { name: { value: name }, directives, - } = isObjectType(node) ? astFromObjectType(node, schema) : node; + } = isObjectType(node) + ? astFromObjectType(node, schema) + : isInterfaceType(node) + ? astFromInterfaceType(node, schema) + : node; const rootTypeNames = getRootTypeNames(schema); const isNotRoot = !rootTypeNames.has(name); @@ -348,7 +441,7 @@ function getDirectivesByName( * Based on if any of its fields contain the `@external` directive * @param node Type */ -function isTypeExtension(node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema): boolean { +function nodeHasTypeExtension(node: ObjectTypeDefinitionNode | GraphQLObjectType, schema: GraphQLSchema): boolean { const definition = isObjectType(node) ? node.astNode || astFromObjectType(node, schema) : node; return definition.fields?.some(field => getDirectivesByName('external', field).length); }