Skip to content

Commit aa191e7

Browse files
authored
[resolvers][federation] Fix mapper being incorrectly used as the base type for reference (#10216)
* Fix reference being assigned mappers incorrectly * Add test for federation mappers usage in reference * Add changeset * Remove extraneous UnwrappedObject type * Change to major because it may break existing use cases * Run CI on federation-fixes feature branch * Update dev tests * Clean up tests
1 parent f5116a7 commit aa191e7

File tree

11 files changed

+317
-139
lines changed

11 files changed

+317
-139
lines changed

.changeset/thick-pianos-smoke.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': major
3+
'@graphql-codegen/typescript-resolvers': major
4+
'@graphql-codegen/plugin-helpers': major
5+
---
6+
7+
Fix `mappers` usage with Federation
8+
9+
`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.
10+
11+
BREAKING CHANGES: No longer generate `UnwrappedObject` utility type, as this was used to support the wrong previously generated type.

.github/workflows/main.yml

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
pull_request:
88
branches:
99
- master
10+
- federation-fixes # FIXME: Remove this line after the PR is merged
1011

1112
env:
1213
NODE_OPTIONS: '--max_old_space_size=4096'

dev-test/test-schema/resolvers-federation.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
128128
info: GraphQLResolveInfo
129129
) => TResult | Promise<TResult>;
130130

131+
/** Mapping of federation types */
132+
export type FederationTypes = {
133+
User: User;
134+
};
135+
131136
/** Mapping between all available schema types and the resolvers types */
132137
export type ResolversTypes = {
133138
Address: ResolverTypeWrapper<Address>;
@@ -190,24 +195,25 @@ export type QueryResolvers<
190195

191196
export type UserResolvers<
192197
ContextType = any,
193-
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']
198+
ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'],
199+
FederationType extends FederationTypes['User'] = FederationTypes['User']
194200
> = {
195201
__resolveReference?: ReferenceResolver<
196202
Maybe<ResolversTypes['User']>,
197203
{ __typename: 'User' } & (
198-
| GraphQLRecursivePick<ParentType, { id: true }>
199-
| GraphQLRecursivePick<ParentType, { name: true }>
204+
| GraphQLRecursivePick<FederationType, { id: true }>
205+
| GraphQLRecursivePick<FederationType, { name: true }>
200206
),
201207
ContextType
202208
>;
203209

204210
email?: Resolver<
205211
ResolversTypes['String'],
206212
{ __typename: 'User' } & (
207-
| GraphQLRecursivePick<ParentType, { id: true }>
208-
| GraphQLRecursivePick<ParentType, { name: true }>
213+
| GraphQLRecursivePick<FederationType, { id: true }>
214+
| GraphQLRecursivePick<FederationType, { name: true }>
209215
) &
210-
GraphQLRecursivePick<ParentType, { address: { city: true; lines: { line2: true } } }>,
216+
GraphQLRecursivePick<FederationType, { address: { city: true; lines: { line2: true } } }>,
211217
ContextType
212218
>;
213219

packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,28 @@ export class BaseResolversVisitor<
12231223
).string;
12241224
}
12251225

1226+
public buildFederationTypes(): string {
1227+
const federationMeta = this._federation.getMeta();
1228+
1229+
if (Object.keys(federationMeta).length === 0) {
1230+
return '';
1231+
}
1232+
1233+
const declarationKind = 'type';
1234+
return new DeclarationBlock(this._declarationBlockConfig)
1235+
.export()
1236+
.asKind(declarationKind)
1237+
.withName(this.convertName('FederationTypes'))
1238+
.withComment('Mapping of federation types')
1239+
.withBlock(
1240+
Object.keys(federationMeta)
1241+
.map(typeName => {
1242+
return indent(`${typeName}: ${this.convertName(typeName)}${this.getPunctuation(declarationKind)}`);
1243+
})
1244+
.join('\n')
1245+
).string;
1246+
}
1247+
12261248
public get schema(): GraphQLSchema {
12271249
return this._schema;
12281250
}
@@ -1498,6 +1520,7 @@ export class BaseResolversVisitor<
14981520
fieldNode: original,
14991521
parentType,
15001522
parentTypeSignature: this.getParentTypeForSignature(node),
1523+
federationTypeSignature: 'FederationType',
15011524
});
15021525
const mappedTypeKey = isSubscriptionType ? `${mappedType}, "${node.name}"` : mappedType;
15031526

@@ -1619,10 +1642,19 @@ export class BaseResolversVisitor<
16191642
);
16201643
}
16211644

1645+
const genericTypes: string[] = [
1646+
`ContextType = ${this.config.contextType.type}`,
1647+
this.transformParentGenericType(parentType),
1648+
];
1649+
if (this._federation.getMeta()[typeName]) {
1650+
const typeRef = `${this.convertName('FederationTypes')}['${typeName}']`;
1651+
genericTypes.push(`FederationType extends ${typeRef} = ${typeRef}`);
1652+
}
1653+
16221654
const block = new DeclarationBlock(this._declarationBlockConfig)
16231655
.export()
16241656
.asKind(declarationKind)
1625-
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
1657+
.withName(name, `<${genericTypes.join(', ')}>`)
16261658
.withBlock(fieldsContent.join('\n'));
16271659

16281660
this._collectedResolvers[node.name as any] = {

packages/plugins/typescript/resolvers/src/index.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,6 @@ export type ResolverWithResolve<TResult, TParent, TContext, TArgs> = {
106106
const stitchingResolverUsage = `StitchingResolver<TResult, TParent, TContext, TArgs>`;
107107

108108
if (visitor.hasFederation()) {
109-
if (visitor.config.wrapFieldDefinitions) {
110-
defsToInclude.push(`export type UnwrappedObject<T> = {
111-
[P in keyof T]: T[P] extends infer R | Promise<infer R> | (() => infer R2 | Promise<infer R2>)
112-
? R & R2 : T[P]
113-
};`);
114-
}
115-
116109
defsToInclude.push(
117110
`export type ReferenceResolver<TResult, TReference, TContext> = (
118111
reference: TReference,
@@ -244,6 +237,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
244237
) => TResult | Promise<TResult>;
245238
`;
246239

240+
const federationTypes = visitor.buildFederationTypes();
247241
const resolversTypeMapping = visitor.buildResolversTypes();
248242
const resolversParentTypeMapping = visitor.buildResolversParentTypes();
249243
const resolversUnionTypesMapping = visitor.buildResolversUnionTypes();
@@ -287,6 +281,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
287281
prepend,
288282
content: [
289283
header,
284+
federationTypes,
290285
resolversUnionTypesMapping,
291286
resolversInterfaceTypesMapping,
292287
resolversTypeMapping,

packages/plugins/typescript/resolvers/src/visitor.ts

+1-15
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,7 @@ import {
77
ParsedResolversConfig,
88
} from '@graphql-codegen/visitor-plugin-common';
99
import autoBind from 'auto-bind';
10-
import {
11-
EnumTypeDefinitionNode,
12-
FieldDefinitionNode,
13-
GraphQLSchema,
14-
ListTypeNode,
15-
NamedTypeNode,
16-
NonNullTypeNode,
17-
} from 'graphql';
10+
import { EnumTypeDefinitionNode, GraphQLSchema, ListTypeNode, NamedTypeNode, NonNullTypeNode } from 'graphql';
1811
import { TypeScriptResolversPluginConfig } from './config.js';
1912

2013
export const ENUM_RESOLVERS_SIGNATURE =
@@ -96,13 +89,6 @@ export class TypeScriptResolversVisitor extends BaseResolversVisitor<
9689
return `${this.config.immutableTypes ? 'ReadonlyArray' : 'Array'}<${str}>`;
9790
}
9891

99-
protected getParentTypeForSignature(node: FieldDefinitionNode) {
100-
if (this._federation.isResolveReferenceField(node) && this.config.wrapFieldDefinitions) {
101-
return 'UnwrappedObject<ParentType>';
102-
}
103-
return 'ParentType';
104-
}
105-
10692
NamedType(node: NamedTypeNode): string {
10793
return `Maybe<${super.NamedType(node)}>`;
10894
}

packages/plugins/typescript/resolvers/tests/__snapshots__/ts-resolvers.spec.ts.snap

+3
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
166166
info: GraphQLResolveInfo
167167
) => TResult | Promise<TResult>;
168168

169+
169170
/** Mapping of union types */
170171
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
171172
ChildUnion: ( Omit<Child, 'parent'> & { parent?: Maybe<_RefType['MyType']> } ) | ( MyOtherType );
@@ -425,6 +426,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
425426
info: GraphQLResolveInfo
426427
) => TResult | Promise<TResult>;
427428

429+
428430
/** Mapping of union types */
429431
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
430432
ChildUnion: ( Omit<Types.Child, 'parent'> & { parent?: Types.Maybe<_RefType['MyType']> } ) | ( Types.MyOtherType );
@@ -770,6 +772,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
770772
info?: GraphQLResolveInfo
771773
) => TResult | Promise<TResult>;
772774

775+
773776
/** Mapping of union types */
774777
export type ResolversUnionTypes<_RefType extends Record<string, unknown>> = ResolversObject<{
775778
ChildUnion: ( Omit<Child, 'parent'> & { parent?: Maybe<_RefType['MyType']> } ) | ( MyOtherType );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import '@graphql-codegen/testing';
2+
import { generate } from './utils';
3+
4+
describe('TypeScript Resolvers Plugin + Apollo Federation - mappers', () => {
5+
it('generates FederationTypes and use it for reference type', async () => {
6+
const federatedSchema = /* GraphQL */ `
7+
type Query {
8+
me: User
9+
}
10+
11+
type User @key(fields: "id") {
12+
id: ID!
13+
name: String
14+
}
15+
16+
type UserProfile {
17+
id: ID!
18+
user: User!
19+
}
20+
`;
21+
22+
const content = await generate({
23+
schema: federatedSchema,
24+
config: {
25+
federation: true,
26+
mappers: {
27+
User: './mappers#UserMapper',
28+
},
29+
},
30+
});
31+
32+
// User should have it
33+
expect(content).toMatchInlineSnapshot(`
34+
"import { GraphQLResolveInfo } from 'graphql';
35+
import { UserMapper } from './mappers';
36+
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
37+
38+
39+
export type ResolverTypeWrapper<T> = Promise<T> | T;
40+
41+
export type ReferenceResolver<TResult, TReference, TContext> = (
42+
reference: TReference,
43+
context: TContext,
44+
info: GraphQLResolveInfo
45+
) => Promise<TResult> | TResult;
46+
47+
type ScalarCheck<T, S> = S extends true ? T : NullableCheck<T, S>;
48+
type NullableCheck<T, S> = Maybe<T> extends T ? Maybe<ListCheck<NonNullable<T>, S>> : ListCheck<T, S>;
49+
type ListCheck<T, S> = T extends (infer U)[] ? NullableCheck<U, S>[] : GraphQLRecursivePick<T, S>;
50+
export type GraphQLRecursivePick<T, S> = { [K in keyof T & keyof S]: ScalarCheck<T[K], S[K]> };
51+
52+
53+
export type ResolverWithResolve<TResult, TParent, TContext, TArgs> = {
54+
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
55+
};
56+
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> = ResolverFn<TResult, TParent, TContext, TArgs> | ResolverWithResolve<TResult, TParent, TContext, TArgs>;
57+
58+
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
59+
parent: TParent,
60+
args: TArgs,
61+
context: TContext,
62+
info: GraphQLResolveInfo
63+
) => Promise<TResult> | TResult;
64+
65+
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
66+
parent: TParent,
67+
args: TArgs,
68+
context: TContext,
69+
info: GraphQLResolveInfo
70+
) => AsyncIterable<TResult> | Promise<AsyncIterable<TResult>>;
71+
72+
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
73+
parent: TParent,
74+
args: TArgs,
75+
context: TContext,
76+
info: GraphQLResolveInfo
77+
) => TResult | Promise<TResult>;
78+
79+
export interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {
80+
subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;
81+
resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>;
82+
}
83+
84+
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
85+
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
86+
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
87+
}
88+
89+
export type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =
90+
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
91+
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
92+
93+
export type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =
94+
| ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
95+
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
96+
97+
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
98+
parent: TParent,
99+
context: TContext,
100+
info: GraphQLResolveInfo
101+
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
102+
103+
export type IsTypeOfResolverFn<T = {}, TContext = {}> = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise<boolean>;
104+
105+
export type NextResolverFn<T> = () => Promise<T>;
106+
107+
export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (
108+
next: NextResolverFn<TResult>,
109+
parent: TParent,
110+
args: TArgs,
111+
context: TContext,
112+
info: GraphQLResolveInfo
113+
) => TResult | Promise<TResult>;
114+
115+
/** Mapping of federation types */
116+
export type FederationTypes = {
117+
User: User;
118+
};
119+
120+
121+
122+
/** Mapping between all available schema types and the resolvers types */
123+
export type ResolversTypes = {
124+
Query: ResolverTypeWrapper<{}>;
125+
User: ResolverTypeWrapper<UserMapper>;
126+
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
127+
String: ResolverTypeWrapper<Scalars['String']['output']>;
128+
UserProfile: ResolverTypeWrapper<Omit<UserProfile, 'user'> & { user: ResolversTypes['User'] }>;
129+
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
130+
};
131+
132+
/** Mapping between all available schema types and the resolvers parents */
133+
export type ResolversParentTypes = {
134+
Query: {};
135+
User: UserMapper;
136+
ID: Scalars['ID']['output'];
137+
String: Scalars['String']['output'];
138+
UserProfile: Omit<UserProfile, 'user'> & { user: ResolversParentTypes['User'] };
139+
Boolean: Scalars['Boolean']['output'];
140+
};
141+
142+
export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
143+
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
144+
};
145+
146+
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], FederationType extends FederationTypes['User'] = FederationTypes['User']> = {
147+
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<FederationType, {"id":true}>, ContextType>;
148+
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
149+
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
150+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
151+
};
152+
153+
export type UserProfileResolvers<ContextType = any, ParentType extends ResolversParentTypes['UserProfile'] = ResolversParentTypes['UserProfile']> = {
154+
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
155+
user?: Resolver<ResolversTypes['User'], ParentType, ContextType>;
156+
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
157+
};
158+
159+
export type Resolvers<ContextType = any> = {
160+
Query?: QueryResolvers<ContextType>;
161+
User?: UserResolvers<ContextType>;
162+
UserProfile?: UserProfileResolvers<ContextType>;
163+
};
164+
165+
"
166+
`);
167+
});
168+
});

0 commit comments

Comments
 (0)