Skip to content

[resolvers][federation] Fix fields or types being wrong generated when marked with @external #10287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: federation-fixes
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/twenty-planets-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typescript-resolvers': patch
'@graphql-codegen/plugin-helpers': patch
---

Fix fields or object types marked with @external being wrongly generated
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,14 @@ export interface ParsedResolversConfig extends ParsedConfig {
}

type FieldDefinitionPrintFn = (
parentName: string,
parent: {
node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode;
typeName: string;
},
avoidResolverOptionals: boolean
) => { value: string | null; meta: { federation?: { isResolveReference: boolean } } };
export type FieldDefinitionResult = [{ node: FieldDefinitionNode }, FieldDefinitionPrintFn];

export interface RootResolver {
content: string;
generatedResolverTypes: {
Expand Down Expand Up @@ -1519,123 +1524,130 @@ export class BaseResolversVisitor<
return `ParentType extends ${parentType} = ${parentType}`;
}

FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionPrintFn {
FieldDefinition(node: FieldDefinitionNode, key: string | number, parent: any): FieldDefinitionResult {
const hasArguments = node.arguments && node.arguments.length > 0;
const declarationKind = 'type';

return (parentName, avoidResolverOptionals) => {
const original: FieldDefinitionNode = parent[key];
const parentType = this.schema.getType(parentName);
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};

if (this._federation.skipField({ fieldNode: original, parentType })) {
return { value: null, meta };
}
return [
{ node },
(parentNode, avoidResolverOptionals) => {
const parentName = parentNode.typeName;

const original: FieldDefinitionNode = parent[key];
const parentType = this.schema.getType(parentName);
const meta: ReturnType<FieldDefinitionPrintFn>['meta'] = {};

const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node: parentNode.node });
const shouldGenerateField = fieldsToGenerate.some(field => field.name === node.name);
if (!shouldGenerateField) {
return { value: null, meta };
}

const contextType = this.getContextType(parentName, node);

let argsType = hasArguments
? this.convertName(
parentName +
(this.config.addUnderscoreToArgsType ? '_' : '') +
this.convertName(node.name, {
useTypesPrefix: false,
useTypesSuffix: false,
}) +
'Args',
{
useTypesPrefix: true,
},
true
)
: null;
const contextType = this.getContextType(parentName, node);

let argsType = hasArguments
? this.convertName(
parentName +
(this.config.addUnderscoreToArgsType ? '_' : '') +
this.convertName(node.name, {
useTypesPrefix: false,
useTypesSuffix: false,
}) +
'Args',
{
useTypesPrefix: true,
},
true
)
: null;

const avoidInputsOptionals = this.config.avoidOptionals.inputValue;

if (argsType !== null) {
const argsToForceRequire = original.arguments.filter(
arg => !!arg.defaultValue || arg.type.kind === 'NonNullType'
);

if (argsToForceRequire.length > 0) {
argsType = this.applyRequireFields(argsType, argsToForceRequire);
} else if (original.arguments.length > 0 && avoidInputsOptionals !== true) {
argsType = this.applyOptionalFields(argsType, original.arguments);
}
}

const avoidInputsOptionals = this.config.avoidOptionals.inputValue;
const parentTypeSignature = this._federation.transformFieldParentType({
fieldNode: original,
parentType,
parentTypeSignature: this.getParentTypeForSignature(node),
federationTypeSignature: 'FederationType',
});

if (argsType !== null) {
const argsToForceRequire = original.arguments.filter(
arg => !!arg.defaultValue || arg.type.kind === 'NonNullType'
);
const { mappedTypeKey, resolverType } = ((): { mappedTypeKey: string; resolverType: string } => {
const baseType = getBaseTypeNode(original.type);
const realType = baseType.name.value;
const typeToUse = this.getTypeToUse(realType);
/**
* Turns GraphQL type to TypeScript types (`mappedType`) e.g.
* - String! -> ResolversTypes['String']>
* - String -> Maybe<ResolversTypes['String']>
* - [String] -> Maybe<Array<Maybe<ResolversTypes['String']>>>
* - [String!]! -> Array<ResolversTypes['String']>
*/
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type);

const subscriptionType = this._schema.getSubscriptionType();
const isSubscriptionType = subscriptionType && subscriptionType.name === parentName;

if (isSubscriptionType) {
return {
mappedTypeKey: `${mappedType}, "${node.name}"`,
resolverType: 'SubscriptionResolver',
};
}

if (argsToForceRequire.length > 0) {
argsType = this.applyRequireFields(argsType, argsToForceRequire);
} else if (original.arguments.length > 0 && avoidInputsOptionals !== true) {
argsType = this.applyOptionalFields(argsType, original.arguments);
}
}
const directiveMappings =
node.directives
?.map(directive => this._directiveResolverMappings[directive.name as any])
.filter(Boolean)
.reverse() ?? [];

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;
const typeToUse = this.getTypeToUse(realType);
/**
* Turns GraphQL type to TypeScript types (`mappedType`) e.g.
* - String! -> ResolversTypes['String']>
* - String -> Maybe<ResolversTypes['String']>
* - [String] -> Maybe<Array<Maybe<ResolversTypes['String']>>>
* - [String!]! -> Array<ResolversTypes['String']>
*/
const mappedType = this._variablesTransformer.wrapAstTypeWithModifiers(typeToUse, original.type);

const subscriptionType = this._schema.getSubscriptionType();
const isSubscriptionType = subscriptionType && subscriptionType.name === parentName;

if (isSubscriptionType) {
return {
mappedTypeKey: `${mappedType}, "${node.name}"`,
resolverType: 'SubscriptionResolver',
mappedTypeKey: mappedType,
resolverType: directiveMappings[0] ?? 'Resolver',
};
}
})();

const signature: {
name: string;
modifier: string;
type: string;
genericTypes: string[];
} = {
name: node.name as any,
modifier: avoidResolverOptionals ? '' : '?',
type: resolverType,
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
};

const directiveMappings =
node.directives
?.map(directive => this._directiveResolverMappings[directive.name as any])
.filter(Boolean)
.reverse() ?? [];
if (this._federation.isResolveReferenceField(node)) {
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
return { value: '', meta };
}
signature.type = 'ReferenceResolver';
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
meta.federation = { isResolveReference: true };
}

return {
mappedTypeKey: mappedType,
resolverType: directiveMappings[0] ?? 'Resolver',
value: indent(
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
', '
)}>${this.getPunctuation(declarationKind)}`
),
meta,
};
})();

const signature: {
name: string;
modifier: string;
type: string;
genericTypes: string[];
} = {
name: node.name as any,
modifier: avoidResolverOptionals ? '' : '?',
type: resolverType,
genericTypes: [mappedTypeKey, parentTypeSignature, contextType, argsType].filter(f => f),
};

if (this._federation.isResolveReferenceField(node)) {
if (!this._federation.getMeta()[parentType.name].hasResolveReference) {
return { value: '', meta };
}
signature.type = 'ReferenceResolver';
signature.genericTypes = [mappedTypeKey, parentTypeSignature, contextType];
meta.federation = { isResolveReference: true };
}

return {
value: indent(
`${signature.name}${signature.modifier}: ${signature.type}<${signature.genericTypes.join(
', '
)}>${this.getPunctuation(declarationKind)}`
),
meta,
};
};
},
];
}

private getFieldContextType(parentName: string, node: FieldDefinitionNode): string {
Expand Down Expand Up @@ -1707,7 +1719,12 @@ export class BaseResolversVisitor<
return `Partial<${argsType}>`;
}

ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string | null {
const fieldsToGenerate = this._federation.findFieldNodesToGenerate({ node });
if (fieldsToGenerate.length === 0) {
return null;
}

const declarationKind = 'type';
const name = this.convertName(node, {
suffix: this.config.resolverTypeSuffix,
Expand All @@ -1728,15 +1745,17 @@ export class BaseResolversVisitor<
return false;
})();

const fieldsContent = (node.fields as unknown as FieldDefinitionPrintFn[]).map(f => {
return f(
typeName,
(rootType === 'query' && this.config.avoidOptionals.query) ||
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
(rootType === false && this.config.avoidOptionals.resolvers)
).value;
});
const fieldsContent = (node.fields as unknown as FieldDefinitionResult[])
.map(([_, f]) => {
return f(
{ node, typeName },
(rootType === 'query' && this.config.avoidOptionals.query) ||
(rootType === 'mutation' && this.config.avoidOptionals.mutation) ||
(rootType === 'subscription' && this.config.avoidOptionals.subscription) ||
(rootType === false && this.config.avoidOptionals.resolvers)
).value;
})
.filter(v => v);

if (!rootType && this._parsedSchemaMeta.typesWithIsTypeOf[typeName]) {
fieldsContent.push(
Expand All @@ -1748,6 +1767,10 @@ export class BaseResolversVisitor<
);
}

if (fieldsContent.length === 0) {
return null;
}

const genericTypes: string[] = [
`ContextType = ${this.config.contextType.type}`,
this.transformParentGenericType(parentType),
Expand Down Expand Up @@ -1958,8 +1981,8 @@ export class BaseResolversVisitor<

// 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)
const fields = (node.fields as unknown as FieldDefinitionResult[]).map(([_, f]) =>
f({ node, typeName }, this.config.avoidOptionals.resolvers)
);
for (const field of fields) {
if (field.meta.federation?.isResolveReference) {
Expand Down
Loading
Loading