diff --git a/.changeset/small-fans-cross.md b/.changeset/small-fans-cross.md new file mode 100644 index 00000000000..729306c52a0 --- /dev/null +++ b/.changeset/small-fans-cross.md @@ -0,0 +1,7 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-resolvers': patch +'@graphql-codegen/plugin-helpers': patch +--- + +Update @requires type diff --git a/dev-test/test-schema/resolvers-federation.ts b/dev-test/test-schema/resolvers-federation.ts index 3583b2d7cae..1c52dab9c31 100644 --- a/dev-test/test-schema/resolvers-federation.ts +++ b/dev-test/test-schema/resolvers-federation.ts @@ -133,6 +133,15 @@ export type FederationTypes = { User: User; }; +/** Mapping of federation reference types */ +export type FederationReferenceTypes = { + User: { __typename: 'User' } & ( + | GraphQLRecursivePick + | GraphQLRecursivePick + ) & + ({} | GraphQLRecursivePick); +}; + /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Address: ResolverTypeWrapper
; @@ -154,13 +163,7 @@ export type ResolversParentTypes = { ID: Scalars['ID']['output']; Lines: Lines; Query: {}; - User: - | User - | ({ __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ) & - GraphQLRecursivePick); + User: User | FederationReferenceTypes['User']; Int: Scalars['Int']['output']; Boolean: Scalars['Boolean']['output']; }; @@ -199,15 +202,11 @@ export type QueryResolvers< export type UserResolvers< ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], - FederationType extends FederationTypes['User'] = FederationTypes['User'] + FederationReferenceType extends FederationReferenceTypes['User'] = FederationReferenceTypes['User'] > = { __resolveReference?: ReferenceResolver< - Maybe, - { __typename: 'User' } & ( - | GraphQLRecursivePick - | GraphQLRecursivePick - ) & - GraphQLRecursivePick, + Maybe | FederationReferenceType, + FederationReferenceType, ContextType >; email?: Resolver; diff --git a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts index c19beccaf9d..af047911afb 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts @@ -818,12 +818,8 @@ export class BaseResolversVisitor< shouldInclude: namedType => !isEnumType(namedType), onNotMappedObjectType: ({ typeName, initialType }) => { let result = initialType; - const federationReferenceTypes = this._federation.printReferenceSelectionSets({ - typeName, - baseFederationType: `${this.convertName('FederationTypes')}['${typeName}']`, - }); - if (federationReferenceTypes) { - result += ` | ${federationReferenceTypes}`; + if (this._federation.getMeta()[typeName]?.referenceSelectionSetsString) { + result += ` | ${this.convertName('FederationReferenceTypes')}['${typeName}']`; } return result; }, @@ -1306,6 +1302,33 @@ export class BaseResolversVisitor< ).string; } + public buildFederationReferenceTypes(): string { + const federationMeta = this._federation.getMeta(); + + if (Object.keys(federationMeta).length === 0) { + return ''; + } + + const declarationKind = 'type'; + return new DeclarationBlock(this._declarationBlockConfig) + .export() + .asKind(declarationKind) + .withName(this.convertName('FederationReferenceTypes')) + .withComment('Mapping of federation reference types') + .withBlock( + Object.entries(federationMeta) + .map(([typeName, { referenceSelectionSetsString }]) => { + if (!referenceSelectionSetsString) { + return undefined; + } + + return indent(`${typeName}: ${referenceSelectionSetsString}${this.getPunctuation(declarationKind)}`); + }) + .filter(v => v) + .join('\n') + ).string; + } + public get schema(): GraphQLSchema { return this._schema; } @@ -1586,13 +1609,6 @@ export class BaseResolversVisitor< } } - const parentTypeSignature = this._federation.transformFieldParentType({ - fieldNode: original, - parentType, - parentTypeSignature: this.getParentTypeForSignature(node), - federationTypeSignature: 'FederationType', - }); - const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => { const baseType = getBaseTypeNode(original.type); const realType = baseType.name.value; @@ -1637,15 +1653,18 @@ export class BaseResolversVisitor< name: typeName, modifier: avoidResolverOptionals ? '' : '?', type: resolverType, - genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f), + genericTypes: [mappedTypeKey, this.getParentTypeForSignature(node), contextType, argsType].filter(f => f), }; if (this._federation.isResolveReferenceField(node)) { if (!this._federation.getMeta()[parentType.name].hasResolveReference) { return { value: '', meta }; } + const resultType = `${mappedTypeKey} | FederationReferenceType`; + const referenceType = 'FederationReferenceType'; + signature.type = 'ReferenceResolver'; - signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType]; + signature.genericTypes = [resultType, referenceType, contextType]; meta.federation = { isResolveReference: true }; } @@ -1788,7 +1807,7 @@ export class BaseResolversVisitor< ]; this._federation.addFederationTypeGenericIfApplicable({ genericTypes, - federationTypesType: this.convertName('FederationTypes'), + federationTypesType: this.convertName('FederationReferenceTypes'), typeName, }); @@ -1975,7 +1994,7 @@ export class BaseResolversVisitor< ]; this._federation.addFederationTypeGenericIfApplicable({ genericTypes, - federationTypesType: this.convertName('FederationTypes'), + federationTypesType: this.convertName('FederationReferenceTypes'), typeName, }); diff --git a/packages/plugins/typescript/resolvers/src/index.ts b/packages/plugins/typescript/resolvers/src/index.ts index 02f6e801b29..0ccb1ce01aa 100644 --- a/packages/plugins/typescript/resolvers/src/index.ts +++ b/packages/plugins/typescript/resolvers/src/index.ts @@ -250,6 +250,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -433,6 +434,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: @@ -785,6 +787,7 @@ export type DirectiveResolverFn TResult | Promise; + /** Mapping of union types */ export type ResolversUnionTypes<_RefType extends Record> = ResolversObject<{ ChildUnion: diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts index d1e08bd54c1..6ac6bdb0cca 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.interface.spec.ts @@ -124,6 +124,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { Admin: Admin; }; + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + Person: + ( { __typename: 'Person' } + & GraphQLRecursivePick ); + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Admin: + ( { __typename: 'Admin' } + & GraphQLRecursivePick ); + }; + /** Mapping of interface types */ export type ResolversInterfaceTypes<_RefType extends Record> = { @@ -150,12 +163,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { Query: {}; Person: ResolversInterfaceTypes['Person']; ID: Scalars['ID']['output']; - User: User | - ( { __typename: 'User' } - & GraphQLRecursivePick ); - Admin: Admin | - ( { __typename: 'Admin' } - & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; + Admin: Admin | FederationReferenceTypes['Admin']; Boolean: Scalars['Boolean']['output']; PersonName: PersonName; String: Scalars['String']['output']; @@ -165,26 +174,20 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - Interface', () => { me?: Resolver, ParentType, ContextType>; }; - export type PersonResolvers = { + export type PersonResolvers = { __resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>; - __resolveReference?: ReferenceResolver, - ( { __typename: 'Person' } - & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; }; - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; - export type AdminResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'Admin' } - & GraphQLRecursivePick ), ContextType>; + export type AdminResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver; canImpersonate?: Resolver; diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts index 905ebc924fb..a6f58d073ac 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.mappers.spec.ts @@ -17,6 +17,12 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { id: ID! user: User! } + + type Account @key(fields: "id") { + id: ID! + name: String! @external + displayName: String! @requires(fields: "name") + } `; const content = await generate({ @@ -25,6 +31,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { federation: true, mappers: { User: './mappers#UserMapper', + Account: './mappers#AccountMapper', }, }, }); @@ -32,7 +39,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { // User should have it expect(content).toMatchInlineSnapshot(` "import { GraphQLResolveInfo } from 'graphql'; - import { UserMapper } from './mappers'; + import { UserMapper, AccountMapper } from './mappers'; export type Omit = Pick>; @@ -115,6 +122,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { /** Mapping of federation types */ export type FederationTypes = { User: User; + Account: Account; + }; + + /** Mapping of federation reference types */ + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick ) ); }; @@ -126,6 +146,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { ID: ResolverTypeWrapper; String: ResolverTypeWrapper; UserProfile: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + Account: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; }; @@ -136,6 +157,7 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { ID: Scalars['ID']['output']; String: Scalars['String']['output']; UserProfile: Omit & { user: ResolversParentTypes['User'] }; + Account: AccountMapper; Boolean: Scalars['Boolean']['output']; }; @@ -143,10 +165,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { me?: Resolver, ParentType, ContextType>; }; - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; }; @@ -156,10 +176,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => { user?: Resolver; }; + export type AccountResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + displayName?: Resolver; + }; + export type Resolvers = { Query?: QueryResolvers; User?: UserResolvers; UserProfile?: UserProfileResolvers; + Account?: AccountResolvers; }; " diff --git a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts index ba763bdc34c..54301d35578 100644 --- a/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts +++ b/packages/plugins/typescript/resolvers/tests/ts-resolvers.federation.spec.ts @@ -92,17 +92,35 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }; `); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + SingleResolvable: + ( { __typename: 'SingleResolvable' } + & GraphQLRecursivePick ); + AtLeastOneResolvable: + ( { __typename: 'AtLeastOneResolvable' } + & GraphQLRecursivePick ); + MixedResolvable: + ( { __typename: 'MixedResolvable' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; Book: Book; - SingleResolvable: SingleResolvable | ( { __typename: 'SingleResolvable' } & GraphQLRecursivePick ); + SingleResolvable: SingleResolvable | FederationReferenceTypes['SingleResolvable']; SingleNonResolvable: SingleNonResolvable; - AtLeastOneResolvable: AtLeastOneResolvable | ( { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick ); - MixedResolvable: MixedResolvable | ( { __typename: 'MixedResolvable' } & ( GraphQLRecursivePick | GraphQLRecursivePick ) ); + AtLeastOneResolvable: AtLeastOneResolvable | FederationReferenceTypes['AtLeastOneResolvable']; + MixedResolvable: MixedResolvable | FederationReferenceTypes['MixedResolvable']; MultipleNonResolvable: MultipleNonResolvable; Boolean: Scalars['Boolean']['output']; }; @@ -110,24 +128,21 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // User should have __resolveReference because it has resolvable @key (by default) expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } & GraphQLRecursivePick ), ContextType>; - id?: Resolver; - name?: Resolver, ParentType, ContextType>; - username?: Resolver, ParentType, ContextType>; - }; - `); + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + name?: Resolver, ParentType, ContextType>; + username?: Resolver, ParentType, ContextType>; + }; + `); // SingleResolvable has __resolveReference because it has resolvable: true expect(content).toBeSimilarStringTo(` - export type SingleResolvableResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'SingleResolvable' } - & GraphQLRecursivePick ), ContextType>; - id?: Resolver; - }; - `); + export type SingleResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + }; + `); // SingleNonResolvable does NOT have __resolveReference because it has resolvable: false expect(content).toBeSimilarStringTo(` @@ -138,26 +153,23 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { // AtLeastOneResolvable has __resolveReference because it at least one resolvable expect(content).toBeSimilarStringTo(` - export type AtLeastOneResolvableResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick ), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - }; - `); + export type AtLeastOneResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); // MixedResolvable has __resolveReference and references for resolvable keys expect(content).toBeSimilarStringTo(` - export type MixedResolvableResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'MixedResolvable' } - & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; - id?: Resolver; - id2?: Resolver; - id3?: Resolver; - }; - `); + export type MixedResolvableResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + id?: Resolver; + id2?: Resolver; + id3?: Resolver; + }; + `); // MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable expect(content).toBeSimilarStringTo(` @@ -200,13 +212,21 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + // User should have it expect(content).toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, ( { __typename: 'User' } & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; `); // Foo shouldn't because it doesn't have @key expect(content).not.toBeSimilarStringTo(` - __resolveReference?: ReferenceResolver, ( { __typename: 'Book' } & GraphQLRecursivePick ), ContextType>; + __resolveReference?: ReferenceResolver, FederationReferenceType, ContextType>; `); }); @@ -242,23 +262,30 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + Name: + ( { __typename: 'Name' } + & GraphQLRecursivePick ); + User: ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; - } + }; `); expect(content).toBeSimilarStringTo(` - export type NameResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'Name' } - & GraphQLRecursivePick ), ContextType>; + export type NameResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; first?: Resolver; last?: Resolver; - } + }; `); }); @@ -268,11 +295,25 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { users: [User] } + type Account @key(fields: "id") { + id: ID! + key: String! + } + type User @key(fields: "id") { id: ID! - name: String @external - age: Int! @external - username: String @requires(fields: "name age") + + a: String @external + aRequires: String @requires(fields: "a") + + b: String! @external + bRequires: String! @requires(fields: "b") + + c: String! @external + cRequires: String! @requires(fields: "c") + + d: String! @external + dRequires: String! @requires(fields: "d") } `; @@ -286,23 +327,49 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + Account: Account | FederationReferenceTypes['Account']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; - Int: Scalars['Int']['output']; + User: User | FederationReferenceTypes['User']; Boolean: Scalars['Boolean']['output']; }; `); - // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + Account: + ( { __typename: 'Account' } + & GraphQLRecursivePick ); + User: ( { __typename: 'User' } - & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + + // User should have it + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; - username?: Resolver, ParentType, ContextType>; + aRequires?: Resolver, ParentType, ContextType>; + bRequires?: Resolver; + cRequires?: Resolver; + dRequires?: Resolver; }; `); }); @@ -315,6 +382,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { extend type User @key(fields: "id") { id: ID! @external + + favouriteColor: String! @external + favouriteColorHex: String! @requires(fields: "favouriteColor") + name: String @external age: Int! @external address: Address! @external @@ -334,10 +405,22 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | ( { __typename: 'User' } & GraphQLRecursivePick & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; Int: Scalars['Int']['output']; @@ -347,11 +430,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; + favouriteColorHex?: Resolver; username?: Resolver, ParentType, ContextType>; }; `); @@ -382,11 +463,17 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type ResolversParentTypes = { - Query: {}; - User: User | + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type ResolversParentTypes = { + Query: {}; + User: User | FederationReferenceTypes['User']; String: Scalars['String']['output']; Name: Name; Boolean: Scalars['Boolean']['output']; @@ -394,10 +481,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; username?: Resolver, ParentType, ContextType>; }; `); @@ -431,14 +516,24 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & ( GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) + & ( {} + | GraphQLRecursivePick + | GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + expect(content).toBeSimilarStringTo(` export type ResolversParentTypes = { Query: {}; - User: User | - ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) - & GraphQLRecursivePick - & GraphQLRecursivePick ); + User: User | FederationReferenceTypes['User']; ID: Scalars['ID']['output']; String: Scalars['String']['output']; LegacyId: LegacyId; @@ -447,12 +542,8 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { `); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick | GraphQLRecursivePick ) - & GraphQLRecursivePick - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; uuid?: Resolver; username?: Resolver; @@ -486,10 +577,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }); expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + & GraphQLRecursivePick ); + }; + `); + + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; name?: Resolver; username?: Resolver, ParentType, ContextType>; }; @@ -546,13 +643,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); + expect(content).toBeSimilarStringTo(` + export type FederationReferenceTypes = { + User: + ( { __typename: 'User' } + & GraphQLRecursivePick ); + }; + `); + // `UserResolvers` should not have `username` resolver because it is marked with `@external` // `UserResolvers` should have `name` resolver because whilst it is marked with `@external`, it is provided by `Book.author` expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, - ( { __typename: 'User' } - & GraphQLRecursivePick ), ContextType>; + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; id?: Resolver; name?: Resolver, ParentType, ContextType>; address?: Resolver, ParentType, ContextType>; @@ -699,12 +802,19 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => { }, }); - // User should have it expect(content).toBeSimilarStringTo(` - export type UserResolvers = { - __resolveReference?: ReferenceResolver, + export type FederationReferenceTypes = { + User: ( { __typename: 'User' } - & ( GraphQLRecursivePick | GraphQLRecursivePick ) ), ContextType>; + & ( GraphQLRecursivePick + | GraphQLRecursivePick ) ); + }; + `); + + // User should have it + expect(content).toBeSimilarStringTo(` + export type UserResolvers = { + __resolveReference?: ReferenceResolver | FederationReferenceType, FederationReferenceType, ContextType>; name?: Resolver, ParentType, ContextType>; username?: Resolver, ParentType, ContextType>; }; diff --git a/packages/utils/plugins-helpers/src/federation.ts b/packages/utils/plugins-helpers/src/federation.ts index 2afd3aae3b6..a29866914a3 100644 --- a/packages/utils/plugins-helpers/src/federation.ts +++ b/packages/utils/plugins-helpers/src/federation.ts @@ -6,7 +6,6 @@ import { FieldDefinitionNode, GraphQLFieldConfigMap, GraphQLInterfaceType, - GraphQLNamedType, GraphQLObjectType, GraphQLSchema, InterfaceTypeDefinitionNode, @@ -38,12 +37,15 @@ export const federationSpec = parse(/* GraphQL */ ` * @example * - resolvable fields marked with `@key` * - fields declared in `@provides` + * - fields declared in `@requires` */ -interface ReferenceSelectionSet { +interface DirectiveSelectionSet { name: string; - selection: boolean | ReferenceSelectionSet[]; + selection: boolean | DirectiveSelectionSet[]; } +type ReferenceSelectionSet = Record; // TODO: handle nested + interface TypeMeta { hasResolveReference: boolean; resolvableKeyDirectives: readonly DirectiveNode[]; @@ -56,7 +58,8 @@ interface TypeMeta { * - [[A, B], [C], [D]] -> (A | B) & C & D * - [[A, B], [C, D], [E]] -> (A | B) & (C | D) & E */ - referenceSelectionSets: ReferenceSelectionSet[][]; + referenceSelectionSets: { directive: '@key' | '@requires'; selectionSets: ReferenceSelectionSet[] }[]; + referenceSelectionSetsString: string; } export type FederationMeta = { [typeName: string]: TypeMeta }; @@ -87,6 +90,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: false, resolvableKeyDirectives: [], referenceSelectionSets: [], + referenceSelectionSetsString: '', } satisfies TypeMeta)), ...update, }; @@ -99,25 +103,138 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { resolvableKeyDirectives: readonly DirectiveNode[]; fields: GraphQLFieldConfigMap; }): TypeMeta['referenceSelectionSets'] => { - const referenceSelectionSets: ReferenceSelectionSet[][] = []; + const referenceSelectionSets: TypeMeta['referenceSelectionSets'] = []; // @key() @key() - "primary keys" in Federation // A reference may receive one primary key combination at a time, so they will be combined with `|` const primaryKeys = resolvableKeyDirectives.map(extractReferenceSelectionSet); - referenceSelectionSets.push([...primaryKeys]); + referenceSelectionSets.push({ directive: '@key', selectionSets: [...primaryKeys] }); + const requiresPossibleTypes: ReferenceSelectionSet[] = []; for (const fieldNode of Object.values(fields)) { // Look for @requires and see what the service needs and gets const directives = getDirectivesByName('requires', fieldNode.astNode); for (const directive of directives) { const requires = extractReferenceSelectionSet(directive); - referenceSelectionSets.push([requires]); + requiresPossibleTypes.push(requires); } } + referenceSelectionSets.push({ directive: '@requires', selectionSets: requiresPossibleTypes }); return referenceSelectionSets; }; + /** + * Function to find all combinations of selection sets and push them into the `result` + * This is used for `@requires` directive because depending on the operation selection set, different + * combination of fields are sent from the router. + * + * @example + * Input: [ + * { a: true }, + * { b: true }, + * { c: true }, + * { d: true}, + * ] + * Output: [ + * { a: true }, + * { a: true, b: true }, + * { a: true, c: true }, + * { a: true, d: true }, + * { a: true, b: true, c: true }, + * { a: true, b: true, d: true }, + * { a: true, c: true, d: true }, + * { a: true, b: true, c: true, d: true } + * + * { b: true }, + * { b: true, c: true }, + * { b: true, d: true }, + * { b: true, c: true, d: true } + * + * { c: true }, + * { c: true, d: true }, + * + * { d: true }, + * ] + * ``` + */ + function findAllSelectionSetCombinations( + selectionSets: ReferenceSelectionSet[], + result: ReferenceSelectionSet[] + ): void { + if (selectionSets.length === 0) { + return; + } + + for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { + const base = selectionSets.slice(0, baseIndex + 1); + const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); + + const currentSelectionSet = base.reduce((acc, selectionSet) => { + acc = { ...acc, ...selectionSet }; + return acc; + }, {}); + + if (baseIndex === 0) { + result.push(currentSelectionSet); + } + + for (const selectionSet of rest) { + result.push({ ...currentSelectionSet, ...selectionSet }); + } + } + + const next = selectionSets.slice(1, selectionSets.length); + + if (next.length > 0) { + findAllSelectionSetCombinations(next, result); + } + } + + const printReferenceSelectionSets = ({ + typeName, + baseFederationType, + referenceSelectionSets, + }: { + typeName: string; + baseFederationType: string; + referenceSelectionSets: TypeMeta['referenceSelectionSets']; + }): string => { + const referenceSelectionSetStrings = referenceSelectionSets.reduce( + (acc, { directive, selectionSets: originalSelectionSets }) => { + const result: string[] = []; + + let selectionSets = originalSelectionSets; + if (directive === '@requires') { + selectionSets = []; + findAllSelectionSetCombinations(originalSelectionSets, selectionSets); + if (selectionSets.length > 0) { + result.push('{}'); + } + } + + for (const referenceSelectionSet of selectionSets) { + result.push(`GraphQLRecursivePick<${baseFederationType}, ${JSON.stringify(referenceSelectionSet)}>`); + } + + if (result.length === 0) { + return acc; + } + + if (result.length === 1) { + acc.push(result.join(' | ')); + return acc; + } + + acc.push(`( ${result.join('\n | ')} )`); + return acc; + }, + [] + ); + + return `\n ( { __typename: '${typeName}' }\n & ${referenceSelectionSetStrings.join('\n & ')} )`; + }; + const federationMeta: FederationMeta = {}; const transformedSchema = mapSchema(schema, { @@ -137,6 +254,12 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { fields: typeConfig.fields, }); + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + setFederationMeta({ meta: federationMeta, typeName: type.name, @@ -144,6 +267,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, referenceSelectionSets, + referenceSelectionSetsString, }, }); @@ -169,6 +293,12 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { ...typeConfig.fields, }; + const referenceSelectionSetsString = printReferenceSelectionSets({ + typeName: type.name, + baseFederationType: `FederationTypes['${type.name}']`, // FIXME: run convertName on FederationTypes + referenceSelectionSets, + }); + setFederationMeta({ meta: federationMeta, typeName: type.name, @@ -176,6 +306,7 @@ export function addFederationReferencesToSchema(schema: GraphQLSchema): { hasResolveReference: true, resolvableKeyDirectives: federationDetails.resolvableKeyDirectives, referenceSelectionSets, + referenceSelectionSetsString, }, }); @@ -339,44 +470,6 @@ export class ApolloFederation { return this.enabled && name === resolveReferenceFieldName; } - /** - * Transforms a field's ParentType signature in ObjectTypes or InterfaceTypes involved in Federation - */ - transformFieldParentType({ - fieldNode, - parentType, - parentTypeSignature, - federationTypeSignature, - }: { - fieldNode: FieldDefinitionNode; - parentType: GraphQLNamedType; - parentTypeSignature: string; - federationTypeSignature: string; - }): string { - if (!this.enabled) { - return parentTypeSignature; - } - - const result = this.printReferenceSelectionSets({ - typeName: parentType.name, - baseFederationType: federationTypeSignature, - }); - - // When `!result`, it means this is not a Federation entity, so we just return the parentTypeSignature - if (!result) { - return parentTypeSignature; - } - - const isEntityResolveReferenceField = - (isObjectType(parentType) || isInterfaceType(parentType)) && fieldNode.name.value === resolveReferenceFieldName; - - if (!isEntityResolveReferenceField) { - return parentTypeSignature; - } - - return result; - } - addFederationTypeGenericIfApplicable({ genericTypes, typeName, @@ -391,7 +484,7 @@ export class ApolloFederation { } const typeRef = `${federationTypesType}['${typeName}']`; - genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`); + genericTypes.push(`FederationReferenceType extends ${typeRef} = ${typeRef}`); } getMeta() { @@ -412,43 +505,6 @@ export class ApolloFederation { return false; } - printReferenceSelectionSet({ - typeName, - referenceSelectionSet, - }: { - typeName: string; - referenceSelectionSet: ReferenceSelectionSet; - }): string { - return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`; - } - - printReferenceSelectionSets({ - typeName, - baseFederationType, - }: { - typeName: string; - baseFederationType: string; - }): string | false { - const federationMeta = this.getMeta()[typeName]; - - if (!federationMeta?.hasResolveReference) { - return false; - } - - return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets - .map(referenceSelectionSetArray => { - const result = referenceSelectionSetArray.map(referenceSelectionSet => { - return this.printReferenceSelectionSet({ - referenceSelectionSet, - typeName: baseFederationType, - }); - }); - - return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | '); - }) - .join('\n & ')} )`; - } - private createMapOfProvides() { const providesMap: Record = {}; @@ -547,7 +603,7 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect return oldVisit(parse(`{${value}}`), { leave: { SelectionSet(node) { - return (node.selections as any as ReferenceSelectionSet[]).reduce((accum, field) => { + return (node.selections as any as DirectiveSelectionSet[]).reduce((accum, field) => { accum[field.name] = field.selection; return accum; }, {}); @@ -556,7 +612,7 @@ function extractReferenceSelectionSet(directive: DirectiveNode): ReferenceSelect return { name: node.name.value, selection: node.selectionSet || true, - } as ReferenceSelectionSet; + } as DirectiveSelectionSet; }, Document(node) { return node.definitions.find(