@@ -20,7 +20,7 @@ import {
2020 PreResolveTypesProcessor ,
2121 SelectionSetProcessorConfig ,
2222 SelectionSetToObject ,
23- transformComment ,
23+ getNodeComment ,
2424 wrapTypeWithModifiers ,
2525} from '@graphql-codegen/visitor-plugin-common' ;
2626import 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
6770export 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