Skip to content

Commit 4b49f6f

Browse files
authored
[typescript-resolvers] Extract union types to ResolversUnionTypes (#9069)
1 parent 80c9e94 commit 4b49f6f

File tree

7 files changed

+690
-129
lines changed

7 files changed

+690
-129
lines changed

.changeset/brave-wasps-tap.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/visitor-plugin-common': patch
3+
'@graphql-codegen/typescript-resolvers': patch
4+
---
5+
6+
Extract union types to ResolversUnionTypes

dev-test/modules/types.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
162162
info: GraphQLResolveInfo
163163
) => TResult | Promise<TResult>;
164164

165+
/** Mapping of union types */
166+
export type ResolversUnionTypes = {
167+
PaymentOption: CreditCard | Paypal;
168+
};
169+
165170
/** Mapping between all available schema types and the resolvers types */
166171
export type ResolversTypes = {
167172
Article: ResolverTypeWrapper<Article>;
@@ -173,7 +178,7 @@ export type ResolversTypes = {
173178
ID: ResolverTypeWrapper<Scalars['ID']>;
174179
Int: ResolverTypeWrapper<Scalars['Int']>;
175180
Mutation: ResolverTypeWrapper<{}>;
176-
PaymentOption: ResolversTypes['CreditCard'] | ResolversTypes['Paypal'];
181+
PaymentOption: ResolverTypeWrapper<ResolversUnionTypes['PaymentOption']>;
177182
Paypal: ResolverTypeWrapper<Paypal>;
178183
Query: ResolverTypeWrapper<{}>;
179184
String: ResolverTypeWrapper<Scalars['String']>;
@@ -193,7 +198,7 @@ export type ResolversParentTypes = {
193198
ID: Scalars['ID'];
194199
Int: Scalars['Int'];
195200
Mutation: {};
196-
PaymentOption: ResolversParentTypes['CreditCard'] | ResolversParentTypes['Paypal'];
201+
PaymentOption: ResolversUnionTypes['PaymentOption'];
197202
Paypal: Paypal;
198203
Query: {};
199204
String: Scalars['String'];

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

+143-49
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,8 @@ export class BaseResolversVisitor<
560560
protected _usedMappers: { [key: string]: boolean } = {};
561561
protected _resolversTypes: ResolverTypes = {};
562562
protected _resolversParentTypes: ResolverParentTypes = {};
563+
protected _hasReferencedResolversUnionTypes = false;
564+
protected _resolversUnionTypes: Record<string, string> = {};
563565
protected _rootTypeNames = new Set<string>();
564566
protected _globalDeclarations = new Set<string>();
565567
protected _federation: ApolloFederation;
@@ -624,6 +626,7 @@ export class BaseResolversVisitor<
624626
name => this.getParentTypeToUse(name),
625627
namedType => !isEnumType(namedType)
626628
);
629+
this._resolversUnionTypes = this.createResolversUnionTypes();
627630
this._fieldContextTypeMap = this.createFieldContextTypeMap();
628631
this._directiveContextTypesMap = this.createDirectivedContextType();
629632
this._directiveResolverMappings = rawConfig.directiveResolverMappings ?? {};
@@ -754,10 +757,9 @@ export class BaseResolversVisitor<
754757
} else if (isScalar) {
755758
prev[typeName] = applyWrapper(this._getScalar(typeName));
756759
} else if (isUnionType(schemaType)) {
757-
prev[typeName] = schemaType
758-
.getTypes()
759-
.map(type => getTypeToUse(type.name))
760-
.join(' | ');
760+
this._hasReferencedResolversUnionTypes = true;
761+
const resolversType = this.convertName('ResolversUnionTypes');
762+
prev[typeName] = applyWrapper(`${resolversType}['${typeName}']`);
761763
} else if (isEnumType(schemaType)) {
762764
prev[typeName] = this.convertName(typeName, { useTypesPrefix: this.config.enumPrefix }, true);
763765
} else {
@@ -766,44 +768,11 @@ export class BaseResolversVisitor<
766768
}
767769

768770
if (shouldApplyOmit && prev[typeName] !== 'any' && isObjectType(schemaType)) {
769-
const fields = schemaType.getFields();
770-
const relevantFields: {
771-
addOptionalSign: boolean;
772-
fieldName: string;
773-
replaceWithType: string;
774-
}[] = this._federation
775-
.filterFieldNames(Object.keys(fields))
776-
.filter(fieldName => {
777-
const field = fields[fieldName];
778-
const baseType = getBaseType(field.type);
779-
780-
// Filter out fields of types that are not included
781-
if (shouldInclude && !shouldInclude(baseType)) {
782-
return false;
783-
}
784-
return true;
785-
})
786-
.map(fieldName => {
787-
const field = fields[fieldName];
788-
const baseType = getBaseType(field.type);
789-
const isUnion = isUnionType(baseType);
790-
791-
if (!this.config.mappers[baseType.name] && !isUnion && !this._shouldMapType[baseType.name]) {
792-
return null;
793-
}
794-
795-
const addOptionalSign = !this.config.avoidOptionals && !isNonNullType(field.type);
796-
797-
return {
798-
addOptionalSign,
799-
fieldName,
800-
replaceWithType: wrapTypeWithModifiers(getTypeToUse(baseType.name), field.type, {
801-
wrapOptional: this.applyMaybe,
802-
wrapArray: this.wrapWithArray,
803-
}),
804-
};
805-
})
806-
.filter(a => a);
771+
const relevantFields = this.getRelevantFieldsToOmit({
772+
schemaType,
773+
getTypeToUse,
774+
shouldInclude,
775+
});
807776

808777
if (relevantFields.length > 0) {
809778
// Puts ResolverTypeWrapper on top of an entire type
@@ -819,15 +788,15 @@ export class BaseResolversVisitor<
819788
}
820789

821790
if (!isMapped && hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
822-
// Make sure the inner type has no ResolverTypeWrapper
823-
const name = clearWrapper(isScalar ? this._getScalar(typeName) : prev[typeName]);
824-
const replaced = replacePlaceholder(this.config.defaultMapper.type, name);
791+
const originalTypeName = isScalar ? this._getScalar(typeName) : prev[typeName];
825792

826-
// Don't wrap Union with ResolverTypeWrapper, each inner type already has it
827793
if (isUnionType(schemaType)) {
828-
prev[typeName] = replaced;
794+
// Don't clear ResolverTypeWrapper from Unions
795+
prev[typeName] = replacePlaceholder(this.config.defaultMapper.type, originalTypeName);
829796
} else {
830-
prev[typeName] = applyWrapper(replacePlaceholder(this.config.defaultMapper.type, name));
797+
const name = clearWrapper(originalTypeName);
798+
const replaced = replacePlaceholder(this.config.defaultMapper.type, name);
799+
prev[typeName] = applyWrapper(replaced);
831800
}
832801
}
833802

@@ -837,7 +806,7 @@ export class BaseResolversVisitor<
837806

838807
protected replaceFieldsInType(
839808
typeName: string,
840-
relevantFields: { addOptionalSign: boolean; fieldName: string; replaceWithType: string }[]
809+
relevantFields: ReturnType<typeof this.getRelevantFieldsToOmit>
841810
): string {
842811
this._globalDeclarations.add(OMIT_TYPE);
843812
return `Omit<${typeName}, ${relevantFields.map(f => `'${f.fieldName}'`).join(' | ')}> & { ${relevantFields
@@ -880,6 +849,64 @@ export class BaseResolversVisitor<
880849
return `Array<${t}>`;
881850
}
882851

852+
protected createResolversUnionTypes(): Record<string, string> {
853+
if (!this._hasReferencedResolversUnionTypes) {
854+
return {};
855+
}
856+
857+
const allSchemaTypes = this._schema.getTypeMap();
858+
const typeNames = this._federation.filterTypeNames(Object.keys(allSchemaTypes));
859+
860+
const unionTypes = typeNames.reduce((res, typeName) => {
861+
const schemaType = allSchemaTypes[typeName];
862+
863+
if (isUnionType(schemaType)) {
864+
const referencedTypes = schemaType.getTypes().map(unionMemberType => {
865+
const isUnionMemberMapped = this.config.mappers[unionMemberType.name];
866+
867+
// 1. If mapped without placehoder, just use it without doing extra checks
868+
if (isUnionMemberMapped && !hasPlaceholder(isUnionMemberMapped.type)) {
869+
return isUnionMemberMapped.type;
870+
}
871+
872+
// 2. Work out value for union member type
873+
// 2a. By default, use the typescript type
874+
let unionMemberValue = this.convertName(unionMemberType.name, {}, true);
875+
876+
// 2b. Find fields to Omit if needed.
877+
// - If no field to Omit, "type with maybe Omit" is typescript type i.e. no Omit
878+
// - If there are fields to Omit, "type with maybe Omit"
879+
const fieldsToOmit = this.getRelevantFieldsToOmit({
880+
schemaType: unionMemberType,
881+
getTypeToUse: this.getTypeToUse,
882+
});
883+
if (fieldsToOmit.length > 0) {
884+
unionMemberValue = this.replaceFieldsInType(unionMemberValue, fieldsToOmit);
885+
}
886+
887+
// 2c. If union member is mapped with placeholder, use the "type with maybe Omit" as {T}
888+
if (isUnionMemberMapped && hasPlaceholder(isUnionMemberMapped.type)) {
889+
return replacePlaceholder(isUnionMemberMapped.type, unionMemberValue);
890+
}
891+
892+
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
893+
const hasDefaultMapper = !!this.config.defaultMapper?.type;
894+
const isScalar = this.config.scalars[typeName];
895+
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
896+
const finalTypename = isScalar ? this._getScalar(typeName) : unionMemberValue;
897+
return replacePlaceholder(this.config.defaultMapper.type, finalTypename);
898+
}
899+
900+
return unionMemberValue;
901+
});
902+
res[typeName] = referencedTypes.map(type => `( ${type} )`).join(' | '); // Must wrap every union member in explicit "( )" to separate the members
903+
}
904+
return res;
905+
}, {});
906+
907+
return unionTypes;
908+
}
909+
883910
protected createFieldContextTypeMap(): FieldContextTypeMap {
884911
return this.config.fieldContextTypes.reduce<FieldContextTypeMap>((prev, fieldContextType) => {
885912
const items = fieldContextType.split('#');
@@ -935,6 +962,24 @@ export class BaseResolversVisitor<
935962
).string;
936963
}
937964

965+
public buildResolversUnionTypes(): string {
966+
if (Object.keys(this._resolversUnionTypes).length === 0) {
967+
return '';
968+
}
969+
970+
const declarationKind = 'type';
971+
return new DeclarationBlock(this._declarationBlockConfig)
972+
.export()
973+
.asKind(declarationKind)
974+
.withName(this.convertName('ResolversUnionTypes'))
975+
.withComment('Mapping of union types')
976+
.withBlock(
977+
Object.entries(this._resolversUnionTypes)
978+
.map(([typeName, value]) => indent(`${typeName}: ${value}${this.getPunctuation(declarationKind)}`))
979+
.join('\n')
980+
).string;
981+
}
982+
938983
public get schema(): GraphQLSchema {
939984
return this._schema;
940985
}
@@ -1478,6 +1523,55 @@ export class BaseResolversVisitor<
14781523
SchemaDefinition() {
14791524
return null;
14801525
}
1526+
1527+
private getRelevantFieldsToOmit({
1528+
schemaType,
1529+
shouldInclude,
1530+
getTypeToUse,
1531+
}: {
1532+
schemaType: GraphQLObjectType;
1533+
getTypeToUse: (name: string) => string;
1534+
shouldInclude?: (type: GraphQLNamedType) => boolean;
1535+
}): {
1536+
addOptionalSign: boolean;
1537+
fieldName: string;
1538+
replaceWithType: string;
1539+
}[] {
1540+
const fields = schemaType.getFields();
1541+
return this._federation
1542+
.filterFieldNames(Object.keys(fields))
1543+
.filter(fieldName => {
1544+
const field = fields[fieldName];
1545+
const baseType = getBaseType(field.type);
1546+
1547+
// Filter out fields of types that are not included
1548+
if (shouldInclude && !shouldInclude(baseType)) {
1549+
return false;
1550+
}
1551+
return true;
1552+
})
1553+
.map(fieldName => {
1554+
const field = fields[fieldName];
1555+
const baseType = getBaseType(field.type);
1556+
const isUnion = isUnionType(baseType);
1557+
1558+
if (!this.config.mappers[baseType.name] && !isUnion && !this._shouldMapType[baseType.name]) {
1559+
return null;
1560+
}
1561+
1562+
const addOptionalSign = !this.config.avoidOptionals && !isNonNullType(field.type);
1563+
1564+
return {
1565+
addOptionalSign,
1566+
fieldName,
1567+
replaceWithType: wrapTypeWithModifiers(getTypeToUse(baseType.name), field.type, {
1568+
wrapOptional: this.applyMaybe,
1569+
wrapArray: this.wrapWithArray,
1570+
}),
1571+
};
1572+
})
1573+
.filter(a => a);
1574+
}
14811575
}
14821576

14831577
function replacePlaceholder(pattern: string, typename: string): string {

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

+2
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
245245

246246
const resolversTypeMapping = visitor.buildResolversTypes();
247247
const resolversParentTypeMapping = visitor.buildResolversParentTypes();
248+
const resolversUnionTypesMapping = visitor.buildResolversUnionTypes();
248249
const { getRootResolver, getAllDirectiveResolvers, mappersImports, unusedMappers, hasScalars } = visitor;
249250

250251
if (hasScalars()) {
@@ -282,6 +283,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
282283
prepend,
283284
content: [
284285
header,
286+
resolversUnionTypesMapping,
285287
resolversTypeMapping,
286288
resolversParentTypeMapping,
287289
...visitorResult.definitions.filter(d => typeof d === 'string'),

0 commit comments

Comments
 (0)