Skip to content

Commit 3ab45f6

Browse files
committed
Update operations/visitor.ts to satisfy tests
1 parent e89b353 commit 3ab45f6

1 file changed

Lines changed: 202 additions & 104 deletions

File tree

  • packages/plugins/typescript/operations/src

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

Lines changed: 202 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
PreResolveTypesProcessor,
2121
SelectionSetProcessorConfig,
2222
SelectionSetToObject,
23-
transformComment,
23+
getNodeComment,
2424
wrapTypeWithModifiers,
2525
} from '@graphql-codegen/visitor-plugin-common';
2626
import autoBind from 'auto-bind';
@@ -38,11 +38,9 @@ import {
3838
InputValueDefinitionNode,
3939
isEnumType,
4040
Kind,
41-
ListTypeNode,
42-
NamedTypeNode,
43-
NonNullTypeNode,
44-
ScalarTypeDefinitionNode,
41+
type TypeDefinitionNode,
4542
TypeInfo,
43+
type TypeNode,
4644
visit,
4745
visitWithTypeInfo,
4846
} from 'graphql';
@@ -62,7 +60,12 @@ export interface TypeScriptDocumentsParsedConfig extends ParsedDocumentsConfig {
6260
enumValues: ParsedEnumValuesMap;
6361
}
6462

65-
type UsedNamedInputTypes = Record<string, GraphQLNamedInputType>;
63+
type UsedNamedInputTypes = Record<
64+
string,
65+
| { type: 'GraphQLScalarType'; node: GraphQLScalarType; tsType: string }
66+
| { type: 'GraphQLEnumType'; node: GraphQLEnumType; tsType: string }
67+
| { type: 'GraphQLInputObjectType'; node: GraphQLInputObjectType; tsType: string }
68+
>;
6669

6770
export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
6871
TypeScriptDocumentsPluginConfig,
@@ -211,107 +214,184 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
211214
});
212215
}
213216

214-
ScalarTypeDefinition(node: ScalarTypeDefinitionNode): string | null {
215-
const scalarName = node.name.value;
216-
217-
// Don't generate type aliases for built-in scalars
218-
if (SCALARS[scalarName] || !this._usedNamedInputTypes[scalarName]) {
219-
return null;
220-
}
221-
222-
// Check if a custom scalar mapping is provided in config
223-
const scalarType = this.scalars?.[scalarName]?.input ?? 'any';
224-
225-
return new DeclarationBlock(this._declarationBlockConfig)
226-
.export()
227-
.asKind('type')
228-
.withName(this.convertName(node))
229-
.withContent(scalarType).string;
230-
}
231-
232217
InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string | null {
233218
const inputTypeName = node.name.value;
234219
if (!this._usedNamedInputTypes[inputTypeName]) {
235220
return null;
236221
}
237222

238223
if (isOneOfInputObjectType(this._schema.getType(inputTypeName))) {
239-
return this.getInputObjectOneOfDeclarationBlock(node).string;
224+
return new DeclarationBlock(this._declarationBlockConfig)
225+
.asKind('type')
226+
.withName(this.convertName(node))
227+
.withComment(node.description?.value)
228+
.withContent(`\n` + (node.fields || []).join('\n |')).string;
240229
}
241230

242-
return this.getInputObjectDeclarationBlock(node).string;
243-
}
244-
245-
InputValueDefinition(node: InputValueDefinitionNode): string {
246-
const comment = transformComment(node.description?.value || '', 1);
247-
const type: string = node.type as any as string;
248-
return comment + indent(`${node.name.value}: ${type};`);
249-
}
250-
251-
private getInputObjectDeclarationBlock(node: InputObjectTypeDefinitionNode): DeclarationBlock {
252231
return new DeclarationBlock(this._declarationBlockConfig)
253-
.export()
254232
.asKind('type')
255233
.withName(this.convertName(node))
256234
.withComment(node.description?.value)
257-
.withBlock((node.fields || []).join('\n'));
235+
.withBlock((node.fields || []).join('\n')).string;
258236
}
259237

260-
private getInputObjectOneOfDeclarationBlock(node: InputObjectTypeDefinitionNode): DeclarationBlock {
261-
return new DeclarationBlock(this._declarationBlockConfig)
262-
.export()
263-
.asKind('type')
264-
.withName(this.convertName(node))
265-
.withComment(node.description?.value)
266-
.withContent(`\n` + (node.fields || []).join('\n |'));
267-
}
268-
269-
private isValidVisit(ancestors: any): boolean {
270-
const currentVisitContext = this.getVisitorKindContextFromAncestors(ancestors);
271-
const isVisitingInputType = currentVisitContext.includes(Kind.INPUT_OBJECT_TYPE_DEFINITION);
272-
const isVisitingEnumType = currentVisitContext.includes(Kind.ENUM_TYPE_DEFINITION);
273-
274-
return isVisitingInputType || isVisitingEnumType;
275-
}
276-
277-
NamedType(node: NamedTypeNode, _key: any, _parent: any, _path: any, ancestors: any): string | undefined {
278-
if (!this.isValidVisit(ancestors)) {
279-
return undefined;
280-
}
281-
282-
const schemaType = this._schema.getType(node.name.value);
283-
284-
if (schemaType instanceof GraphQLScalarType) {
285-
const inputType = this.scalars?.[node.name.value]?.input ?? SCALARS[node.name.value] ?? 'any';
286-
if (inputType === 'any' && node.name.value) {
287-
return node.name.value;
238+
InputValueDefinition(
239+
node: InputValueDefinitionNode,
240+
_key?: number | string,
241+
_parent?: any,
242+
_path?: Array<string | number>,
243+
ancestors?: Array<TypeDefinitionNode>
244+
): string {
245+
const oneOfDetails = (function parseOneOf(
246+
schema: GraphQLSchema
247+
): { isOneOfInputValue: true; realParentDef: TypeDefinitionNode } | { isOneOfInputValue: false } {
248+
const realParentDef = ancestors?.[ancestors.length - 1];
249+
if (realParentDef) {
250+
const parentType = schema.getType(realParentDef.name.value);
251+
if (isOneOfInputObjectType(parentType)) {
252+
if (node.type.kind === Kind.NON_NULL_TYPE) {
253+
throw new Error(
254+
'Fields on an input object type can not be non-nullable. It seems like the schema was not validated.'
255+
);
256+
}
257+
return { isOneOfInputValue: true, realParentDef };
258+
}
288259
}
260+
return { isOneOfInputValue: false };
261+
})(this._schema);
262+
263+
// 1. Flatten GraphQL type nodes to make it easier to turn into string
264+
// GraphQL type nodes may have `NonNullType` type before each `ListType` or `NamedType`
265+
// This make it a bit harder to know whether a `ListType` or `Namedtype` is nullable without looking at the node before it.
266+
// Flattening it into an array where the nullability is in `ListType` and `NamedType` makes it easier to code,
267+
//
268+
// So, we recursively call `collectAndFlattenTypeNodes` to handle the following scenarios:
269+
// - [Thing]
270+
// - [Thing!]
271+
// - [Thing]!
272+
// - [Thing!]!
273+
const typeNodes: Array<
274+
{ type: 'ListType'; isNonNullable: boolean } | { type: 'NamedType'; isNonNullable: boolean; name: string }
275+
> = [];
276+
(function collectAndFlattenTypeNodes({
277+
currentTypeNode,
278+
isPreviousNodeNonNullable,
279+
}: {
280+
currentTypeNode: TypeNode;
281+
isPreviousNodeNonNullable: boolean;
282+
}): void {
283+
if (currentTypeNode.kind === Kind.NON_NULL_TYPE) {
284+
const nextTypeNode = currentTypeNode.type;
285+
collectAndFlattenTypeNodes({ currentTypeNode: nextTypeNode, isPreviousNodeNonNullable: true });
286+
} else if (currentTypeNode.kind === Kind.LIST_TYPE) {
287+
typeNodes.push({ type: 'ListType', isNonNullable: isPreviousNodeNonNullable });
288+
289+
const nextTypeNode = currentTypeNode.type;
290+
collectAndFlattenTypeNodes({ currentTypeNode: nextTypeNode, isPreviousNodeNonNullable: false });
291+
} else if (currentTypeNode.kind === Kind.NAMED_TYPE) {
292+
typeNodes.push({
293+
type: 'NamedType',
294+
isNonNullable: isPreviousNodeNonNullable,
295+
name: currentTypeNode.name.value,
296+
});
297+
}
298+
})({
299+
currentTypeNode: node.type,
300+
isPreviousNodeNonNullable: oneOfDetails.isOneOfInputValue, // If the InputValue is part of @oneOf input, we treat it as non-null (even if it must be null in the schema)
301+
});
289302

290-
return inputType;
291-
}
292-
293-
if (schemaType instanceof GraphQLEnumType || schemaType instanceof GraphQLInputObjectType) {
294-
return this.convertName(node.name.value);
295-
}
303+
// 2. Generate the type of a TypeScript field declaration
304+
// e.g. `field?: string`, then the `string` is the `typePart`
305+
let typePart: string = '';
306+
// We call `.reverse()` here to get the base type node first
307+
for (const typeNode of typeNodes.reverse()) {
308+
if (typeNode.type === 'NamedType') {
309+
const usedInputType = this._usedNamedInputTypes[typeNode.name];
310+
if (!usedInputType) {
311+
continue;
312+
}
296313

297-
return node.name.value;
298-
}
314+
typePart = usedInputType.tsType; // If the schema is correct, when reversing typeNodes, the first node would be `NamedType`, which means we can safely set it as the base for typePart
315+
if (usedInputType.tsType !== 'any' && !typeNode.isNonNullable) {
316+
typePart += ' | null | undefined';
317+
}
318+
continue;
319+
}
299320

300-
ListType(node: ListTypeNode, _key: any, _parent: any, _path: any, ancestors: any): string | undefined {
301-
if (!this.isValidVisit(ancestors)) {
302-
return undefined;
321+
if (typeNode.type === 'ListType') {
322+
typePart = `Array<${typePart}>`;
323+
if (!typeNode.isNonNullable) {
324+
typePart += ' | null | undefined';
325+
}
326+
}
303327
}
304328

305-
const listModifier = this.config.immutableTypes ? 'ReadonlyArray' : 'Array';
306-
return `${listModifier}<${node.type}>`;
307-
}
308-
309-
NonNullType(node: NonNullTypeNode, _key: any, _parent: any, _path: any, ancestors: any): string | undefined {
310-
if (!this.isValidVisit(ancestors)) {
311-
return undefined;
329+
// TODO: eddeee888 check if we want to support `directiveArgumentAndInputFieldMappings` for operations
330+
// if (node.directives && this.config.directiveArgumentAndInputFieldMappings) {
331+
// typePart =
332+
// getDirectiveOverrideType({
333+
// directives: node.directives,
334+
// directiveArgumentAndInputFieldMappings: this.config.directiveArgumentAndInputFieldMappings,
335+
// }) || typePart;
336+
// }
337+
338+
const addOptionalSign =
339+
!oneOfDetails.isOneOfInputValue &&
340+
!this.config.avoidOptionals.inputValue &&
341+
(node.type.kind !== Kind.NON_NULL_TYPE ||
342+
(!this.config.avoidOptionals.defaultValue && node.defaultValue !== undefined));
343+
344+
// 3. Generate the keyPart of the TypeScript field declaration
345+
// e.g. `field?: string`, then the `field?` is the `keyPart`
346+
const keyPart = `${node.name.value}${addOptionalSign ? '?' : ''}`;
347+
348+
// 4. other parts of TypeScript field declaration
349+
const commentPart = getNodeComment(node);
350+
const readonlyPart = this.config.immutableTypes ? 'readonly ' : '';
351+
352+
const currentInputValue = commentPart + indent(`${readonlyPart}${keyPart}: ${typePart};`);
353+
354+
// 5. Check if field is part of `@oneOf` input type
355+
// If yes, we must generate a union member where the current inputValue must be provieded, and the others are not
356+
// e.g.
357+
// ```graphql
358+
// input UserInput {
359+
// byId: ID
360+
// byEmail: String
361+
// byLegacyId: ID
362+
// }
363+
// ```
364+
//
365+
// Then, the generated type is:
366+
// ```ts
367+
// type UserInput =
368+
// | { byId: string | number; byEmail?: never; byLegacyId?: never }
369+
// | { byId?: never; byEmail: string; byLegacyId?: never }
370+
// | { byId?: never; byEmail?: never; byLegacyId: string | number }
371+
// ```
372+
373+
if (oneOfDetails.isOneOfInputValue) {
374+
const parentType = this._schema.getType(oneOfDetails.realParentDef.name.value);
375+
if (isOneOfInputObjectType(parentType)) {
376+
if (node.type.kind === Kind.NON_NULL_TYPE) {
377+
throw new Error(
378+
'Fields on an input object type can not be non-nullable. It seems like the schema was not validated.'
379+
);
380+
}
381+
const fieldParts: Array<string> = [];
382+
for (const fieldName of Object.keys(parentType.getFields())) {
383+
if (fieldName === node.name.value) {
384+
fieldParts.push(currentInputValue);
385+
continue;
386+
}
387+
fieldParts.push(`${readonlyPart}${fieldName}?: never;`);
388+
}
389+
return indent(`{ ${fieldParts.join(' ')} }`);
390+
}
312391
}
313392

314-
return node.type as any as string | undefined;
393+
// If field is not part of @oneOf input type, then it's a input value, just return as-is
394+
return currentInputValue;
315395
}
316396

317397
public getImports(): Array<string> {
@@ -359,22 +439,40 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
359439
return `Exact<${variablesBlock === '{}' ? `{ [key: string]: never; }` : variablesBlock}>${extraType}`;
360440
}
361441

362-
private collectInnerTypesRecursively(type: GraphQLInputObjectType, usedInputTypes: UsedNamedInputTypes): void {
363-
const fields = type.getFields();
442+
private collectInnerTypesRecursively(node: GraphQLNamedInputType, usedInputTypes: UsedNamedInputTypes): void {
443+
if (usedInputTypes[node.name]) {
444+
return;
445+
}
446+
447+
if (node instanceof GraphQLEnumType) {
448+
usedInputTypes[node.name] = {
449+
type: 'GraphQLEnumType',
450+
node,
451+
tsType: this.convertName(node.name),
452+
};
453+
return;
454+
}
455+
456+
if (node instanceof GraphQLScalarType) {
457+
usedInputTypes[node.name] = {
458+
type: 'GraphQLScalarType',
459+
node,
460+
tsType: (SCALARS[node.name] || this.config.scalars?.[node.name]?.input.type) ?? 'any',
461+
};
462+
return;
463+
}
464+
465+
// GraphQLInputObjectType
466+
usedInputTypes[node.name] = {
467+
type: 'GraphQLInputObjectType',
468+
node,
469+
tsType: this.convertName(node.name),
470+
};
471+
472+
const fields = node.getFields();
364473
for (const field of Object.values(fields)) {
365474
const fieldType = getNamedType(field.type);
366-
if (
367-
fieldType &&
368-
(fieldType instanceof GraphQLEnumType ||
369-
fieldType instanceof GraphQLInputObjectType ||
370-
fieldType instanceof GraphQLScalarType) &&
371-
!usedInputTypes[fieldType.name]
372-
) {
373-
usedInputTypes[fieldType.name] = fieldType;
374-
if (fieldType instanceof GraphQLInputObjectType) {
375-
this.collectInnerTypesRecursively(fieldType, usedInputTypes);
376-
}
377-
}
475+
this.collectInnerTypesRecursively(fieldType, usedInputTypes);
378476
}
379477
}
380478

@@ -400,13 +498,9 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
400498
(foundInputType instanceof GraphQLInputObjectType ||
401499
foundInputType instanceof GraphQLScalarType ||
402500
foundInputType instanceof GraphQLEnumType) &&
403-
!usedInputTypes[namedTypeNode.name.value] &&
404501
!isNativeNamedType(foundInputType)
405502
) {
406-
usedInputTypes[namedTypeNode.name.value] = foundInputType;
407-
if (foundInputType instanceof GraphQLInputObjectType) {
408-
this.collectInnerTypesRecursively(foundInputType, usedInputTypes);
409-
}
503+
this.collectInnerTypesRecursively(foundInputType, usedInputTypes);
410504
}
411505
},
412506
});
@@ -427,7 +521,11 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
427521
const namedType = getNamedType(fieldType);
428522

429523
if (namedType instanceof GraphQLEnumType) {
430-
usedInputTypes[namedType.name] = namedType;
524+
usedInputTypes[namedType.name] = {
525+
type: 'GraphQLEnumType',
526+
node: namedType,
527+
tsType: this.convertName(namedType.name),
528+
};
431529
}
432530
}
433531
},

0 commit comments

Comments
 (0)