import { BaseVisitor , buildScalars , DEFAULT_SCALARS , NormalizedScalarsMap , ParsedConfig , RawConfig , } from '@graphql-codegen/visitor-plugin-common' ; import { camelCase , constantCase , pascalCase } from 'change-case' ; import { plural } from 'pluralize' ; import crypto from 'crypto' ; import { DefinitionNode , DirectiveNode , EnumTypeDefinitionNode , FieldDefinitionNode , GraphQLNamedType , GraphQLSchema , Kind , ObjectTypeDefinitionNode , parse , valueFromASTUntyped , } from 'graphql' ; import { addFieldToModel , getDirective , removeFieldFromModel } from '../utils/fieldUtils' ; import { getTypeInfo } from '../utils/get-type-info' ; import { CodeGenConnectionType , CodeGenFieldConnection , processConnections } from '../utils/process-connections' ; import { sortFields } from '../utils/sort' ; import { printWarning } from '../utils/warn' ; import { processAuthDirective } from '../utils/process-auth' ; import { processConnectionsV2 } from '../utils/process-connections-v2' ; import { graphqlName , toUpper } from 'graphql-transformer-common' ; import { processPrimaryKey } from '../utils/process-primary-key' ; import { processIndex } from '../utils/process-index' ; export enum CodeGenGenerateEnum { metadata = 'metadata' , code = 'code' , loader = 'loader' } export interface RawAppSyncModelConfig extends RawConfig { name type string description example target : string ; name type string description default selectedType ? : string ; name type string description default generate ? : CodeGenGenerateEnum ; name type string descriptions directives ? : string ; name type boolean descriptions isTimestampFieldsAdded ? : boolean ; name type boolean descriptions handleListNullabilityTransparently ? : boolean ; name type boolean descriptions usePipelinedTransformer ? : boolean ; name type number descriptions transformerVersion ? : number ; } export interface ParsedAppSyncModelConfig extends ParsedConfig { selectedType ? : string ; generate ? : CodeGenGenerateEnum ; target ? : string ; isTimestampFieldsAdded ? : boolean ; handleListNullabilityTransparently ? : boolean ; usePipelinedTransformer ? : boolean ; transformerVersion ? : number ; } export type CodeGenArgumentsMap = Record < string , any > ; export type CodeGenDirective = { name : string ; arguments : CodeGenArgumentsMap ; } ; export type CodeGenFieldDirective = CodeGenDirective & { fieldName : string ; } ; export type CodeGenDirectives = CodeGenDirective [ ] ; export type CodeGenField = TypeInfo & { name : string ; directives : CodeGenDirectives ; connectionInfo ? : CodeGenFieldConnection ; isReadOnly ? : boolean ; } ; export type TypeInfo = { type : string ; isList : boolean ; isNullable : boolean ; isListNullable ? : boolean ; baseType ? : GraphQLNamedType | null ; } ; export type CodeGenModel = { name : string ; type : 'model' ; directives : CodeGenDirectives ; fields : CodeGenField [ ] ; } ; export type CodeGenEnum = { name : string ; type : 'enum' ; values : CodeGenEnumValueMap ; } ; export type CodeGenModelMap = { [ modelName ] : CodeGenModel ; } ; export type CodeGenEnumValueMap = { [ enumConvertedName ] : string ; } ; export type CodeGenEnumMap = Record < string , CodeGenEnum > ; type ManyToManyContext = { model : CodeGenModel ; field : CodeGenField ; directive : CodeGenDirective ; } ; const DEFAULT_CREATED_TIME = 'createdAt' ; const DEFAULT_UPDATED_TIME = 'updatedAt' ; export class AppSyncModelVisitor < TRawConfig extends RawAppSyncModelConfig = RawAppSyncModelConfig , TPluginConfig extends ParsedAppSyncModelConfig = ParsedAppSyncModelConfig > extends BaseVisitor < TRawConfig , TPluginConfig > { protected READ_ONLY_FIELDS = [ 'id' ] ; protected SCALAR_TYPE_MAP : Record < string , string > = { } ; protected modelMap : CodeGenModelMap = { } ; protected nonModelMap : CodeGenModelMap = { } ; protected enumMap : CodeGenEnumMap = { } ; protected typesToSkip : string [ ] = [ ] ; constructor ( protected _schema , rawConfig , additionalConfig , defaultScalars = DEFAULT_SCALARS ) { super ( rawConfig , { ... additionalConfig , scalars : buildScalars ( _schema , rawConfig . scalars || '' , defaultScalars ) , target : rawConfig . target , isTimestampFieldsAdded : rawConfig . isTimestampFieldsAdded , handleListNullabilityTransparently : rawConfig . handleListNullabilityTransparently , usePipelinedTransformer : rawConfig . usePipelinedTransformer , transformerVersion : rawConfig . transformerVersion , } ) ; const typesUsedInDirectives = [ ] ; if ( rawConfig . directives ) { const directiveSchema = parse ( rawConfig . directives ) ; directiveSchema . definitions . forEach ( ( definition ) => { if ( definition . kind === Kind . ENUM_TYPE_DEFINITION || definition . kind === Kind . INPUT_OBJECT_TYPE_DEFINITION ) { typesUsedInDirectives . push ( definition . name . value ) ; } } ) ; } this . typesToSkip = [ this . _schema . getQueryType ( ) , this . _schema . getMutationType ( ) , this . _schema . getSubscriptionType ( ) ] . filter ( t => t ) . map ( t => ( t && t . name ) || '' ) ; this . typesToSkip . push ( ... typesUsedInDirectives ) ; } ObjectTypeDefinition ( node , index ? , parent ? ) { if ( this . typesToSkip . includes ( node . name . value ) ) { return ; } const directives = this . getDirectives ( node . directives ) ; const fields = ( node . fields as unknown ) as CodeGenField [ ] ; const modelDirective = directives . find ( directive => directive . name === 'model' ) ; if ( modelDirective ) { const model = { name : node . name . value , type : 'model' , directives , fields , } ; this . ensureIdField ( model ) ; this . addTimestampFields ( model , modelDirective ) ; this . sortFields ( model ) ; this . modelMap [ node . name . value ] = model ; } else { const nonModel = { name : node . name . value , type : 'model' , directives , fields , } ; this . nonModelMap [ node . name . value ] = nonModel ; } } FieldDefinition ( node ) { const directive = this . getDirectives ( node . directives ) ; return { name : node . name . value , directives : directive , ... getTypeInfo ( node . type , this . _schema ) , } ; } EnumTypeDefinition ( node ) { if ( this . typesToSkip . includes ( node . name . value ) ) { return ; } const enumName = this . getEnumName ( node . name . value ) ; const values = node . values ? node . values . reduce ( ( acc , val ) => { acc [ this . getEnumValue ( val . name . value ) ] = val . name . value ; return acc ; } , { } as any ) : { } ; this . enumMap [ node . name . value ] = { name : enumName , type : 'enum' , values , } ; } processDirectives ( shouldUseModelNameFieldInHasManyAndBelongsTo ) { if ( this . config . usePipelinedTransformer || this . config . transformerVersion === 2 ) { this . processV2KeyDirectives ( ) ; this . processConnectionDirectivesV2 ( shouldUseModelNameFieldInHasManyAndBelongsTo ) ; } else { this . processConnectionDirective ( ) ; } this . processAuthDirectives ( ) ; } generate ( ) { const shouldUseModelNameFieldInHasManyAndBelongsTo = false ; this . processDirectives ( shouldUseModelNameFieldInHasManyAndBelongsTo ) ; return '' ; } private getDirectives ( directives ) { if ( directives ) { return directives . map ( d => ( { name : d . name . value , arguments : this . getDirectiveArguments ( d ) , } ) ) ; } return [ ] ; } private getDirectiveArguments ( directive ) { const directiveArguments = { } ; if ( directive . arguments ) { directive . arguments . reduce ( ( acc , arg ) => { directiveArguments [ arg . name . value ] = valueFromASTUntyped ( arg . value ) ; return directiveArguments ; } , directiveArguments ) ; } return directiveArguments ; } /**
     * Returns an object that contains all the models that need codegen to be run
     *
     */ protected getSelectedModels ( ) { if ( this . _parsedConfig . selectedType ) { const selectedModel = this . modelMap [ this . _parsedConfig . selectedType ] ; return selectedModel ? { [ this . _parsedConfig . selectedType ] : selectedModel } : { } ; } return this . modelMap ; } protected getSelectedNonModels ( ) { if ( this . _parsedConfig . selectedType ) { const selectedModel = this . nonModelMap [ this . _parsedConfig . selectedType ] ; return selectedModel ? { [ this . _parsedConfig . selectedType ] : selectedModel } : { } ; } return this . nonModelMap ; } protected getSelectedEnums ( ) { if ( this . _parsedConfig . selectedType ) { const selectedModel = this . enumMap [ this . _parsedConfig . selectedType ] ; return selectedModel ? { [ this . _parsedConfig . selectedType ] : selectedModel } : { } ; } return this . enumMap ; } protected selectedTypeIsEnum ( ) { if ( this . _parsedConfig && this . _parsedConfig . selectedType ) { if ( this . _parsedConfig . selectedType in this . enumMap ) { return true ; } } return false ; } protected selectedTypeIsNonModel ( ) { if ( this . _parsedConfig && this . _parsedConfig . selectedType ) { if ( this . _parsedConfig . selectedType in this . nonModelMap ) { return true ; } } return false ; } param field protected getNativeType ( field ) { const typeName = field . type ; let typeNameStr = '' ; if ( typeName in this . scalars ) { typeNameStr = this . scalars [ typeName ] ; } else if ( this . isModelType ( field ) ) { typeNameStr = this . getModelName ( this . modelMap [ typeName ] ) ; } else if ( this . isEnumType ( field ) ) { typeNameStr = this . getEnumName ( this . enumMap [ typeName ] ) ; } else if ( this . isNonModelType ( field ) ) { typeNameStr = this . getNonModelName ( this . nonModelMap [ typeName ] ) ; } else { throw new Error ( `Unknown type ${ typeName } for field ${ field . name }. Did you forget to add the @model directive` ) ; } return field . isList ? this . getListType ( typeNameStr , field ) : typeNameStr ; } protected getListType ( typeStr , field ) { return `List<${ typeStr }>` ; } protected getFieldName ( field ) { return field . name ; } protected getEnumName ( enumField ) { if ( typeof enumField === 'string' ) { return pascalCase ( enumField ) ; } return pascalCase ( enumField . name ) ; } protected getModelName ( model ) { return model . name ; } protected getNonModelName ( model ) { return model . name ; } protected getEnumValue ( value ) { return constantCase ( value ) ; } protected isEnumType ( field ) { const typeName = field . type ; return typeName in this . enumMap ; } protected isModelType ( field ) { const typeName = field . type ; return typeName in this . modelMap ; } protected isNonModelType ( field ) { const typeName = field . type ; return typeName in this . nonModelMap ; } protected computeVersion ( ) { const typeArr = [ ] ; Object . values ( { ... this . modelMap , ... this . nonModelMap } ) . forEach ( ( obj ) => { const directives = this . config . usePipelinedTransformer || this . config . transformerVersion === 2 ? obj . directives . filter ( dir => dir . name === 'primaryKey' || dir . name === 'index' ) : obj . directives . filter ( dir => dir . name === 'key' ) ; const fields = obj . fields . map ( ( field ) => { const fieldDirectives = this . config . usePipelinedTransformer ? field . directives . filter ( field => field . name === 'hasOne' || field . name === 'belongsTo' || field . name === 'hasMany' || field . name === 'manyToMany' ) : field . directives . filter ( field => field . name === 'connection' ) ; return { name : field . name , directives : fieldDirectives , type : field . type , isNullable : field . isNullable , isList : field . isList , isListNullable : field . isListNullable , } ; } ) . sort ( ( a , b ) => sortFields ( a , b ) ) ; typeArr . push ( { name : obj . name , directives , fields , } ) ; } ) ; typeArr . sort ( sortFields ) ; return crypto . createHash ( 'MD5' ) . update ( JSON . stringify ( typeArr ) ) . digest ( ) . toString ( 'hex' ) ; } param model protected sortFields ( model ) { model . fields = model . fields . reduce ( ( acc , field ) => { if ( field . name === 'id' ) { acc . unshift ( field ) ; } else { acc . push ( field ) ; } return acc ; } , [ ] as CodeGenField [ ] ) ; } protected ensureIdField ( model ) { const idField = model . fields . find ( field => field . name === 'id' ) ; if ( idField ) { if ( idField . type !== 'ID' ) { throw new Error ( `id field on ${ model . name } should be of type ID` ) ; } idField . isNullable = false ; } else { model . fields . splice ( 0 , 0 , { name : 'id' , type : 'ID' , isNullable : false , isList : false , directives : [ ] , } ) ; } } protected processConnectionDirective ( ) { Object . values ( this . modelMap ) . forEach ( model => { model . fields . forEach ( field => { const connectionInfo = processConnections ( field , model , this . modelMap ) ; if ( connectionInfo ) { if ( connectionInfo . kind === CodeGenConnectionType . HAS_MANY || connectionInfo . kind === CodeGenConnectionType . HAS_ONE ) { addFieldToModel ( connectionInfo . connectedModel , connectionInfo . associatedWith ) ; } else if ( connectionInfo . targetName !== 'id' ) { removeFieldFromModel ( model , connectionInfo . targetName ) ; } field . connectionInfo = connectionInfo ; } } ) ; const modelTypes = Object . values ( this . modelMap ) . map ( model => model . name ) ; model . fields = model . fields . filter ( field => { const fieldType = field . type ; const connectionInfo = field . connectionInfo ; if ( modelTypes . includes ( fieldType ) && connectionInfo === undefined ) { printWarning ( `Model ${ model . name } has field ${ field . name } of type ${ field . type } but its not connected. Add a @connection directive if want to connect them.` ) ; return false ; } return true ; } ) ; } ) ; } protected generateIntermediateModel ( firstModel , secondModel , firstField , secondField , relationName ) { const firstModelKeyFieldName = `${ camelCase ( firstModel . name ) }ID` ; const secondModelKeyFieldName = `${ camelCase ( secondModel . name ) }ID` ; let intermediateModel = { name : relationName , type : 'model' , directives : [ { name : 'model' , arguments : { } } ] , fields : [ { type : 'ID' , isNullable : false , isList : false , name : 'id' , directives : [ ] , } , { type : 'ID' , isNullable : false , isList : false , name : firstModelKeyFieldName , directives : [ { name : 'index' , arguments : { name : 'by' + firstModel . name , sortKeyFields : [ secondModelKeyFieldName ] } } ] , } , { type : 'ID' , isNullable : false , isList : false , name : secondModelKeyFieldName , directives : [ { name : 'index' , arguments : { name : 'by' + secondModel . name , sortKeyFields : [ firstModelKeyFieldName ] } } ] , } , { type : firstModel . name , isNullable : false , isList : false , name : camelCase ( firstModel . name ) , directives : [ { name : 'belongsTo' , arguments : { fields : [ firstModelKeyFieldName ] } } ] , } , { type : secondModel . name , isNullable : false , isList : false , name : camelCase ( secondModel . name ) , directives : [ { name : 'belongsTo' , arguments : { fields : [ secondModelKeyFieldName ] } } ] , } , ] , } ; return intermediateModel ; } protected determinePrimaryKeyFieldname ( model ) { let primaryKeyFieldName = 'id' ; model . fields . forEach ( field => { field . directives . forEach ( dir => { if ( dir . name === 'primaryKey' ) { primaryKeyFieldName = field . name ; } } ) ; } ) ; return primaryKeyFieldName ; } protected convertManyToManyDirectives ( contexts ) { contexts . forEach ( context => { let directiveIndex = context . field . directives . indexOf ( context . directive , 0 ) ; if ( directiveIndex > - 1 ) { context . field . directives . splice ( directiveIndex , 1 ) ; context . field . type = graphqlName ( toUpper ( context . directive . arguments . relationName ) ) ; context . field . directives . push ( { name : 'hasMany' , arguments : { indexName : `by${ context . model . name }` , fields : [ this . determinePrimaryKeyFieldname ( context . model ) ] } , } ) ; } else { throw new Error ( 'manyToMany directive not found on manyToMany field...' ) ; } } ) ; } protected processManyToManyDirectives ( ) { let manyDirectiveMap = new Map < string , Array < ManyToManyContext > > ( ) ; Object . values ( this . modelMap ) . forEach ( model => { model . fields . forEach ( field => { field . directives . forEach ( dir => { if ( dir . name === 'manyToMany' ) { let relationName = graphqlName ( toUpper ( dir . arguments . relationName ) ) ; let existingRelation = manyDirectiveMap . get ( relationName ) ; if ( existingRelation ) { existingRelation . push ( { model : model , field : field , directive : dir } ) ; } else { manyDirectiveMap . set ( relationName , [ { model : model , field : field , directive : dir } ] ) ; } } } ) ; } ) ; } ) ; manyDirectiveMap . forEach ( ( value , key ) => { if ( value . length != 2 ) { throw new Error ( `Error for relation: '${ value [ 0 ] . directive . arguments . relationName }', there should be two matching manyToMany directives and found: ${ value . length }` ) ; } let intermediateModel = this . generateIntermediateModel ( value [ 0 ] . model , value [ 1 ] . model , value [ 0 ] . field , value [ 1 ] . field , graphqlName ( toUpper ( value [ 0 ] . directive . arguments . relationName ) ) ) ; const modelDirective = intermediateModel . directives . find ( directive => directive . name === 'model' ) ; if ( modelDirective ) { this . ensureIdField ( intermediateModel ) ; this . addTimestampFields ( intermediateModel , modelDirective ) ; this . sortFields ( intermediateModel ) ; } this . modelMap [ intermediateModel . name ] = intermediateModel ; this . convertManyToManyDirectives ( value ) ; } ) ; } protected processConnectionDirectivesV2 ( shouldUseModelNameFieldInHasManyAndBelongsTo ) { this . processManyToManyDirectives ( ) ; Object . values ( this . modelMap ) . forEach ( model => { model . fields . forEach ( field => { const connectionInfo = processConnectionsV2 ( field , model , this . modelMap , shouldUseModelNameFieldInHasManyAndBelongsTo ) ; if ( connectionInfo ) { if ( connectionInfo . kind === CodeGenConnectionType . HAS_MANY ) { addFieldToModel ( connectionInfo . connectedModel , connectionInfo . associatedWith ) ; } else if ( connectionInfo . kind === CodeGenConnectionType . HAS_ONE ) { addFieldToModel ( model , { name : connectionInfo . targetName , directives : [ ] , type : 'ID' , isList : false , isNullable : field . isNullable , } ) ; } field . connectionInfo = connectionInfo ; } } ) ; const modelTypes = Object . values ( this . modelMap ) . map ( model => model . name ) ; model . fields = model . fields . filter ( field => { const fieldType = field . type ; const connectionInfo = field . connectionInfo ; if ( modelTypes . includes ( fieldType ) && connectionInfo === undefined ) { printWarning ( `Model ${ model . name } has field ${ field . name } of type ${ field . type } but its not connected. Add the appropriate ${ field . isList ? '@hasMany' : '@hasOne' }/@belongsTo directive if you want to connect them.` ) ; return false ; } return true ; } ) ; } ) ; Object . values ( this . modelMap ) . forEach ( model => { model . fields . forEach ( field => { const connectionInfo = field . connectionInfo ; if ( connectionInfo && connectionInfo . kind !== CodeGenConnectionType . HAS_MANY && connectionInfo . kind !== CodeGenConnectionType . HAS_ONE && connectionInfo . targetName !== 'id' ) { removeFieldFromModel ( model , connectionInfo . targetName ) ; } } ) ; } ) ; } protected processV2KeyDirectives ( ) { Object . values ( this . modelMap ) . forEach ( model => { processPrimaryKey ( model ) ; processIndex ( model ) ; } ) ; } protected processAuthDirectives ( ) { Object . values ( this . modelMap ) . forEach ( model => { const filteredDirectives = model . directives . filter ( d => d . name !== 'auth' ) ; const authDirectives = processAuthDirective ( model . directives ) ; model . directives = [ ... filteredDirectives , ... authDirectives ] ; model . fields . forEach ( field => { const nonAuthDirectives = field . directives . filter ( d => d . name != 'auth' ) ; const authDirectives = processAuthDirective ( field . directives ) ; field . directives = [ ... nonAuthDirectives , ... authDirectives ] ; } ) ; } ) ; } protected pluralizeModelName ( model ) { return plural ( model . name ) ; } param model protected addTimestampFields ( model , directive ) { if ( ! this . config . isTimestampFieldsAdded ) { return ; } if ( directive . name !== 'model' ) { return ; } if ( directive . arguments && directive . arguments . hasOwnProperty ( 'timestamps' ) && directive . arguments . timestamps === null ) { return ; } const timestamps = directive . arguments . timestamps ; const createdAtField = { name : timestamps ?. createdAt || DEFAULT_CREATED_TIME , directives : [ ] , type : 'AWSDateTime' , isList : false , isNullable : true , isReadOnly : true , } ; const updatedAtField = { name : timestamps ?. updatedAt || DEFAULT_UPDATED_TIME , directives : [ ] , type : 'AWSDateTime' , isList : false , isNullable : true , isReadOnly : true , } ; addFieldToModel ( model , createdAtField ) ; addFieldToModel ( model , updatedAtField ) ; } param field protected isRequiredField ( field ) { return ! ( this . config . handleListNullabilityTransparently ? ( field . isList ? field . isListNullable : field . isNullable ) : field . isNullable ) ; } get models ( ) { return this . modelMap ; } get enums ( ) { return this . enumMap ; } get nonModels ( ) { return this . nonModelMap ; } }