import { ok } from 'assert' ; import JSON5 from 'json5' ; import { castArray } from 'lodash' ; import pupa from 'pupa' ; import { PlainObject } from 'simplytyped' ; import { ClassDeclarationStructure , ExportSpecifierStructure , StatementStructures , StructureKind , } from 'ts-morph' ; import { getGraphqlImport } from '../helpers/get-graphql-import' ; import { getOutputTypeName } from '../helpers/get-output-type-name' ; import { getPropertyType } from '../helpers/get-property-type' ; import { ImportDeclarationMap } from '../helpers/import-declaration-map' ; import { createObjectSettings , ObjectSetting , ObjectSettings , } from '../helpers/object-settings' ; import { propertyStructure } from '../helpers/property-structure' ; import { EventArguments , OutputType } from '../types' ; const nestjsGraphql = '@nestjs/graphql' ; export function modelOutputType ( outputType , args ) { const { getSourceFile , models , config , modelFields , fieldSettings , eventEmitter } = args ; const model = models . get ( outputType . name ) ; ok ( model , `Cannot find model by name ${ outputType . name }` ) ; const sourceFile = getSourceFile ( { name : outputType . name , type : 'model' , } ) ; const sourceFileStructure = sourceFile . getStructure ( ) ; const exportDeclaration = getExportDeclaration ( model . name , sourceFileStructure . statements as StatementStructures [ ] ) ; const importDeclarations = new ImportDeclarationMap ( ) ; const classStructure = { kind : StructureKind . Class , isExported : true , name : outputType . name , decorators : [ { name : 'ObjectType' , arguments : [ ] , } , ] , properties : [ ] , } ; ( sourceFileStructure . statements as StatementStructures [ ] ) . push ( classStructure ) ; ok ( classStructure . decorators , 'classStructure.decorators is undefined' ) ; const decorator = classStructure . decorators . find ( d => d . name === 'ObjectType' ) ; ok ( decorator , 'ObjectType decorator not found' ) ; let modelSettings ; if ( model . documentation ) { const objectTypeOptions = { } ; const { documentation , settings } = createObjectSettings ( { text : model . documentation , config , } ) ; if ( documentation ) { if ( ! classStructure . leadingTrivia ) { classStructure . leadingTrivia = `/** ${ documentation } */\n` ; } objectTypeOptions . description = documentation ; } decorator . arguments = settings . getObjectTypeArguments ( objectTypeOptions ) ; modelSettings = settings ; } importDeclarations . add ( 'Field' , nestjsGraphql ) ; importDeclarations . add ( 'ObjectType' , nestjsGraphql ) ; for ( const field of outputType . fields ) { let fileType = 'model' ; const { location , isList , type , namespace } = field . outputType ; let outputTypeName = String ( type ) ; if ( namespace !== 'model' ) { fileType = 'output' ; outputTypeName = getOutputTypeName ( outputTypeName ) ; } const modelField = modelFields . get ( model . name ) ?. get ( field . name ) ; const settings = fieldSettings . get ( model . name ) ?. get ( field . name ) ; const fieldType = settings ?. getFieldType ( { name : outputType . name , output : true , } ) ; const propertySettings = settings ?. getPropertyType ( { name : outputType . name , output : true , } ) ; const propertyType = castArray ( propertySettings ?. name || getPropertyType ( { location , type : outputTypeName , } ) ) ; propertyType . splice ( 1 , propertyType . length ) ; if ( field . isNullable && ! isList ) { propertyType . push ( 'null' ) ; } let graphqlType ; if ( fieldType ) { graphqlType = fieldType . name ; importDeclarations . create ( { ... fieldType } ) ; } else { const graphqlImport = getGraphqlImport ( { sourceFile , fileType , location , isId : modelField ?. isId , noTypeId : config . noTypeId , typeName : outputTypeName , getSourceFile , } ) ; graphqlType = graphqlImport . name ; if ( graphqlImport . name !== outputType . name && graphqlImport . specifier ) { importDeclarations . add ( graphqlImport . name , graphqlImport . specifier ) ; } } const property = propertyStructure ( { name : field . name , isNullable : field . isNullable , hasExclamationToken : true , hasQuestionToken : location === 'outputObjectTypes' , propertyType , isList , } ) ; if ( typeof property . leadingTrivia === 'string' && modelField ?. documentation ) { property . leadingTrivia += `/** ${ modelField . documentation } */\n` ; } classStructure . properties ?. push ( property ) ; if ( propertySettings ) { importDeclarations . create ( { ... propertySettings } ) ; } ok ( property . decorators , 'property.decorators is undefined' ) ; if ( settings ?. shouldHideField ( { name : outputType . name , output : true } ) ) { importDeclarations . add ( 'HideField' , nestjsGraphql ) ; property . decorators . push ( { name : 'HideField' , arguments : [ ] } ) ; } else { property . decorators . push ( { name : 'Field' , arguments : [ isList ? `() => [${ graphqlType }]` : `() => ${ graphqlType }` , JSON5 . stringify ( { nullable : Boolean ( field . isNullable ) , defaultValue : [ 'number' , 'string' , 'boolean' ] . includes ( typeof modelField ?. default ) ? modelField ?. default : undefined , description : modelField ?. documentation , } ) , ] , } ) ; for ( const setting of settings || [ ] ) { if ( shouldBeDecorated ( setting ) && ( setting . match ?. ( field . name ) ?? true ) ) { property . decorators . push ( { name : setting . name , arguments : setting . arguments as string [ ] , } ) ; ok ( setting . from , "Missed 'from' part in configuration or field setting" ) ; importDeclarations . create ( setting ) ; } } for ( const decorate of config . decorate ) { if ( decorate . isMatchField ( field . name ) && decorate . isMatchType ( outputTypeName ) ) { property . decorators . push ( { name : decorate . name , arguments : decorate . arguments ?. map ( x => pupa ( x , { propertyType } ) ) , } ) ; importDeclarations . create ( decorate ) ; } } } eventEmitter . emitSync ( 'ClassProperty' , property , { location , isList , propertyType , } ) ; } for ( const setting of modelSettings || [ ] ) { if ( shouldBeDecorated ( setting ) ) { classStructure . decorators . push ( { name : setting . name , arguments : setting . arguments as string [ ] , } ) ; importDeclarations . create ( setting ) ; } } if ( exportDeclaration ) { sourceFile . set ( { statements : [ exportDeclaration , '\n' , classStructure ] , } ) ; const classDeclaration = sourceFile . getClassOrThrow ( model . name ) ; const commentedText = classDeclaration . getText ( ) . split ( '\n' ) . map ( x => `// ${ x }` ) ; classDeclaration . remove ( ) ; sourceFile . addStatements ( [ '\n' , ... commentedText ] ) ; } else { sourceFile . set ( { statements : [ ... importDeclarations . toStatements ( ) , classStructure ] , } ) ; } } function shouldBeDecorated ( setting ) { return ( setting . kind === 'Decorator' && ( setting . output || setting . model ) && ! ( setting . output && setting . model ) ) ; } function getExportDeclaration ( name , statements ) { return statements . find ( structure => { return ( structure . kind === StructureKind . ExportDeclaration && ( structure . namedExports as ExportSpecifierStructure [ ] ) . some ( o => ( o . alias || o . name ) === name ) ) ; } ) ; }