import { indent , indentMultiline , NormalizedScalarsMap } from '@graphql-codegen/visitor-plugin-common' ; import { camelCase } from 'change-case' ; import { GraphQLSchema } from 'graphql' ; import { lowerCaseFirst } from 'lower-case-first' ; import { schemaTypeMap } from '../configs/swift-config' ; import { SwiftDeclarationBlock , escapeKeywords , ListType } from '../languages/swift-declaration-block' ; import { CodeGenConnectionType } from '../utils/process-connections' ; import { AppSyncModelVisitor , CodeGenField , CodeGenGenerateEnum , CodeGenModel , RawAppSyncModelConfig , ParsedAppSyncModelConfig , } from './appsync-visitor' ; import { AuthDirective , AuthStrategy } from '../utils/process-auth' ; import { printWarning } from '../utils/warn' ; import { SWIFT_SCALAR_MAP } from '../scalars' ; export interface RawAppSyncModelSwiftConfig extends RawAppSyncModelConfig { name type boolean descriptions auth emitAuthProvider ? : boolean ; name type boolean description key generateIndexRules ? : boolean ; } export interface ParsedAppSyncModelSwiftConfig extends ParsedAppSyncModelConfig { emitAuthProvider ? : boolean ; generateIndexRules ? : boolean ; } export class AppSyncSwiftVisitor < TRawConfig extends RawAppSyncModelSwiftConfig = RawAppSyncModelSwiftConfig , TPluginConfig extends ParsedAppSyncModelSwiftConfig = ParsedAppSyncModelSwiftConfig > extends AppSyncModelVisitor < TRawConfig , TPluginConfig > { protected modelExtensionImports : string [ ] = [ 'import Amplify' , 'import Foundation' ] ; protected imports : string [ ] = [ 'import Amplify' , 'import Foundation' ] ; constructor ( schema , rawConfig , additionalConfig , defaultScalars = SWIFT_SCALAR_MAP ) { super ( schema , rawConfig , additionalConfig , defaultScalars ) ; this . _parsedConfig . emitAuthProvider = rawConfig . emitAuthProvider || false ; this . _parsedConfig . generateIndexRules = rawConfig . generateIndexRules || false ; } generate ( ) { const shouldUseModelNameFieldInHasManyAndBelongsTo = true ; this . processDirectives ( shouldUseModelNameFieldInHasManyAndBelongsTo ) ; const code = [ `// swiftlint:disable all` ] ; if ( this . _parsedConfig . generate === CodeGenGenerateEnum . metadata ) { code . push ( this . generateSchema ( ) ) ; } else if ( this . _parsedConfig . generate === CodeGenGenerateEnum . loader ) { code . push ( this . generateClassLoader ( ) ) ; } else if ( this . selectedTypeIsEnum ( ) ) { code . push ( this . generateEnums ( ) ) ; } else if ( this . selectedTypeIsNonModel ( ) ) { code . push ( this . generateNonModelType ( ) ) ; } else { code . push ( this . generateStruct ( ) ) ; } return code . join ( '\n' ) ; } generateStruct ( ) { let result = [ ... this . imports , '' ] ; Object . entries ( this . getSelectedModels ( ) ) . forEach ( ( [ name , obj ] ) => { const structBlock = new SwiftDeclarationBlock ( ) . withName ( this . getModelName ( obj ) ) . access ( 'public' ) . withProtocols ( [ 'Model' ] ) ; Object . entries ( obj . fields ) . forEach ( ( [ fieldName , field ] ) => { const fieldType = this . getNativeType ( field ) ; const isVariable = field . name !== 'id' ; const listType = field . connectionInfo ? ListType . LIST : ListType . ARRAY ; structBlock . addProperty ( this . getFieldName ( field ) , fieldType , undefined , 'public' , { optional : ! this . isFieldRequired ( field ) , isList : field . isList , variable : isVariable , isEnum : this . isEnumType ( field ) , listType : field . isList ? listType : undefined , isListNullable : field . isListNullable , handleListNullabilityTransparently : this . isHasManyConnectionField ( field ) ? false : this . config . handleListNullabilityTransparently , } ) ; } ) ; const initParams = this . getWritableFields ( obj ) ; const initImpl = `self.init(${ indentMultiline ( obj . fields . map ( field => { const fieldName = escapeKeywords ( this . getFieldName ( field ) ) ; return field . isReadOnly ? `${ fieldName }: nil` : `${ fieldName }: ${ fieldName }` ; } ) . join ( ',\n' ) ) . trim ( ) })` ; if ( this . config . isTimestampFieldsAdded && this . hasReadOnlyFields ( obj ) ) { structBlock . addClassMethod ( 'init' , null , initImpl , initParams . map ( field => { const listType = field . connectionInfo ? ListType . LIST : ListType . ARRAY ; return { name : this . getFieldName ( field ) , type : this . getNativeType ( field ) , value : field . name === 'id' ? 'UUID().uuidString' : undefined , flags : { optional : field . isNullable , isList : field . isList , isEnum : this . isEnumType ( field ) , listType : field . isList ? listType : undefined , isListNullable : field . isListNullable , handleListNullabilityTransparently : this . isHasManyConnectionField ( field ) ? false : this . config . handleListNullabilityTransparently , } , } ; } ) , 'public' , { } ) ; structBlock . addClassMethod ( 'init' , null , this . getInitBody ( obj . fields ) , obj . fields . map ( field => { const listType = field . connectionInfo ? ListType . LIST : ListType . ARRAY ; return { name : this . getFieldName ( field ) , type : this . getNativeType ( field ) , value : field . name === 'id' ? 'UUID().uuidString' : undefined , flags : { optional : field . isNullable , isList : field . isList , isEnum : this . isEnumType ( field ) , listType : field . isList ? listType : undefined , isListNullable : field . isListNullable , handleListNullabilityTransparently : this . isHasManyConnectionField ( field ) ? false : this . config . handleListNullabilityTransparently , } , } ; } ) , 'internal' , { } ) ; } else { structBlock . addClassMethod ( 'init' , null , this . getInitBody ( obj . fields ) , obj . fields . map ( field => { const listType = field . connectionInfo ? ListType . LIST : ListType . ARRAY ; return { name : this . getFieldName ( field ) , type : this . getNativeType ( field ) , value : field . name === 'id' ? 'UUID().uuidString' : undefined , flags : { optional : field . isNullable , isList : field . isList , isEnum : this . isEnumType ( field ) , listType : field . isList ? listType : undefined , isListNullable : field . isListNullable , handleListNullabilityTransparently : this . isHasManyConnectionField ( field ) ? false : this . config . handleListNullabilityTransparently , } , } ; } ) , 'public' , { } ) ; } result . push ( structBlock . string ) ; } ) ; return result . join ( '\n' ) ; } generateEnums ( ) { const result = [ ... this . imports , '' ] ; Object . entries ( this . getSelectedEnums ( ) ) . forEach ( ( [ name , enumValue ] ) => { const enumDeclaration = new SwiftDeclarationBlock ( ) . asKind ( 'enum' ) . access ( 'public' ) . withProtocols ( [ 'String' , 'EnumPersistable' ] ) . withName ( this . getEnumName ( enumValue ) ) ; Object . entries ( enumValue . values ) . forEach ( ( [ name , value ] ) => { enumDeclaration . addEnumValue ( name , value ) ; } ) ; result . push ( enumDeclaration . string ) ; } ) ; return result . join ( '\n' ) ; } generateNonModelType ( ) { let result = [ ... this . imports , '' ] ; Object . entries ( this . getSelectedNonModels ( ) ) . forEach ( ( [ name , obj ] ) => { const structBlock = new SwiftDeclarationBlock ( ) . withName ( this . getModelName ( obj ) ) . access ( 'public' ) . withProtocols ( [ 'Embeddable' ] ) ; Object . values ( obj . fields ) . forEach ( field => { const fieldType = this . getNativeType ( field ) ; structBlock . addProperty ( this . getFieldName ( field ) , fieldType , undefined , 'DEFAULT' , { optional : ! this . isFieldRequired ( field ) , isList : field . isList , variable : true , isEnum : this . isEnumType ( field ) , listType : field . isList ? ListType . ARRAY : undefined , isListNullable : field . isListNullable , handleListNullabilityTransparently : this . isHasManyConnectionField ( field ) ? false : this . config . handleListNullabilityTransparently , } ) ; } ) ; result . push ( structBlock . string ) ; } ) ; return result . join ( '\n' ) ; } generateSchema ( ) { let result = [ ... this . modelExtensionImports , '' ] ; Object . values ( this . getSelectedModels ( ) ) . filter ( m => m . type === 'model' ) . forEach ( model => { const schemaDeclarations = new SwiftDeclarationBlock ( ) . asKind ( 'extension' ) . withName ( this . getModelName ( model ) ) ; this . generateCodingKeys ( this . getModelName ( model ) , model , schemaDeclarations ) , this . generateModelSchema ( this . getModelName ( model ) , model , schemaDeclarations ) ; result . push ( schemaDeclarations . string ) ; } ) ; Object . values ( this . getSelectedNonModels ( ) ) . forEach ( model => { const schemaDeclarations = new SwiftDeclarationBlock ( ) . asKind ( 'extension' ) . withName ( this . getNonModelName ( model ) ) ; this . generateCodingKeys ( this . getNonModelName ( model ) , model , schemaDeclarations ) , this . generateModelSchema ( this . getNonModelName ( model ) , model , schemaDeclarations ) ; result . push ( schemaDeclarations . string ) ; } ) ; return result . join ( '\n' ) ; } generateCodingKeys ( name , model , extensionDeclaration ) { const codingKeyEnum = new SwiftDeclarationBlock ( ) . asKind ( 'enum' ) . access ( 'public' ) . withName ( 'CodingKeys' ) . withProtocols ( [ 'String' , 'ModelKey' ] ) . withComment ( 'MARK: - CodingKeys' ) ; model . fields . forEach ( field => codingKeyEnum . addEnumValue ( this . getFieldName ( field ) , field . name ) ) ; extensionDeclaration . appendBlock ( codingKeyEnum . string ) ; extensionDeclaration . addProperty ( 'keys' , '' , 'CodingKeys.self' , 'public' , { static : true , variable : false , } ) ; } generateModelSchema ( name , model , extensionDeclaration ) { const keysName = lowerCaseFirst ( model . name ) ; const fields = model . fields . map ( field => this . generateFieldSchema ( field , keysName ) ) ; const authRules = this . generateAuthRules ( model ) ; const keyDirectives = this . config . generateIndexRules ? this . generateKeyRules ( model ) : [ ] ; const closure = [ '{ model in' , `let ${ keysName } = ${ this . getModelName ( model ) }.keys` , '' , ... ( authRules . length ? [ `model.authRules = ${ authRules }` , '' ] : [ ] ) , `model.pluralName = "${ this . pluralizeModelName ( model ) }"` , '' , ... ( keyDirectives . length ? [ 'model.attributes(' , indentMultiline ( keyDirectives . join ( ',\n' ) ) , ')' , '' ] : [ ] ) , 'model.fields(' , indentMultiline ( fields . join ( ',\n' ) ) , ')' , '}' , ] . join ( '\n' ) ; extensionDeclaration . addProperty ( 'schema' , '' , `defineSchema ${ indentMultiline ( closure ) . trim ( ) }` , 'public' , { static : true , variable : false } , ' MARK: - ModelSchema' ) ; } protected generateClassLoader ( ) { const structList = Object . values ( this . modelMap ) . map ( typeObj => `${ this . getModelName ( typeObj ) }.self` ) ; const result = [ ... this . modelExtensionImports , '' ] ; const classDeclaration = new SwiftDeclarationBlock ( ) . access ( 'public' ) . withName ( 'AmplifyModels' ) . asKind ( 'class' ) . withProtocols ( [ 'AmplifyModelRegistration' ] ) . final ( ) . withComment ( 'Contains the set of classes that conforms to the `Model` protocol.' ) ; classDeclaration . addProperty ( 'version' , 'String' , `"${ this . computeVersion ( ) }"` , 'public' , { } ) ; const body = structList . map ( modelClass => `ModelRegistry.register(modelType: ${ modelClass })` ) . join ( '\n' ) ; classDeclaration . addClassMethod ( 'registerModels' , null , body , [ { type : 'ModelRegistry.Type' , name : 'registry' , flags : { } , value : undefined } ] , 'public' , { } ) ; result . push ( classDeclaration . string ) ; return result . join ( '\n' ) ; } private getInitBody ( fields ) { let result = fields . map ( field => { const fieldName = escapeKeywords ( this . getFieldName ( field ) ) ; return indent ( `self.${ fieldName } = ${ fieldName }` ) ; } ) ; return result . join ( '\n' ) ; } protected getListType ( typeStr , field ) { return `${ typeStr }` ; } private generateFieldSchema ( field , modelKeysName ) { if ( field . type === 'ID' && field . name === 'id' ) { return `.id()` ; } let ofType ; let isReadOnly = '' ; const isEnumType = this . isEnumType ( field ) ; const isModelType = this . isModelType ( field ) ; const isNonModelType = this . isNonModelType ( field ) ; const name = `${ modelKeysName }.${ this . getFieldName ( field ) }` ; const typeName = this . getSwiftModelTypeName ( field ) ; const { connectionInfo } = field ; const isRequiredField = ! this . isHasManyConnectionField ( field ) && this . config . handleListNullabilityTransparently ? this . isRequiredField ( field ) : this . isFieldRequired ( field ) ; const isRequired = isRequiredField ? '.required' : '.optional' ; if ( connectionInfo ) { if ( connectionInfo . kind === CodeGenConnectionType . HAS_MANY ) { return `.hasMany(${ name }, is: ${ isRequired }, ofType: ${ typeName }, associatedWith: ${ this . getModelName ( connectionInfo . connectedModel ) }.keys.${ this . getFieldName ( connectionInfo . associatedWith ) })` ; } if ( connectionInfo . kind === CodeGenConnectionType . HAS_ONE ) { return `.hasOne(${ name }, is: ${ isRequired }, ofType: ${ typeName }, associatedWith: ${ this . getModelName ( connectionInfo . connectedModel ) }.keys.${ this . getFieldName ( connectionInfo . associatedWith ) }, targetName: "${ connectionInfo . targetName }")` ; } if ( connectionInfo . kind === CodeGenConnectionType . BELONGS_TO ) { return `.belongsTo(${ name }, is: ${ isRequired }, ofType: ${ typeName }, targetName: "${ connectionInfo . targetName }")` ; } } if ( field . isList ) { if ( isModelType ) { ofType = `.collection(of: ${ this . getSwiftModelTypeName ( field ) })` ; } else { ofType = `.embeddedCollection(of: ${ this . getSwiftModelTypeName ( field ) })` ; } } else { if ( isEnumType ) { ofType = `.enum(type: ${ typeName })` ; } else if ( isModelType ) { ofType = `.model(${ typeName })` ; } else if ( isNonModelType ) { ofType = `.embedded(type: ${ typeName })` ; } else { ofType = typeName ; } } if ( field . isReadOnly ) { isReadOnly = 'isReadOnly: true' ; } const args = [ `${ name }` , `is: ${ isRequired }` , isReadOnly , `ofType: ${ ofType }` ] . filter ( arg => arg ) . join ( ', ' ) ; return `.field(${ args })` ; } private getSwiftModelTypeName ( field ) { if ( this . isEnumType ( field ) ) { return `${ this . getEnumName ( field . type ) }.self` ; } if ( this . isModelType ( field ) ) { return `${ this . getModelName ( this . modelMap [ field . type ] ) }.self` ; } if ( this . isNonModelType ( field ) ) { return `${ this . getNonModelName ( this . nonModelMap [ field . type ] ) }.self` ; } if ( field . type in schemaTypeMap ) { if ( field . isList ) { return `${ this . getNativeType ( field ) }.self` ; } return schemaTypeMap [ field . type ] ; } return '.string' ; } protected getEnumValue ( value ) { return camelCase ( value ) ; } param field protected isFieldRequired ( field ) { if ( this . isHasManyConnectionField ( field ) ) { return false ; } return ! field . isNullable ; } protected generateKeyRules ( model ) { return model . directives . filter ( directive => directive . name === 'key' ) . map ( directive => { const name = directive . arguments . name ? `"${ directive . arguments . name }"` : 'nil' ; const fields = directive . arguments . fields . map ( ( field ) => `"${ field }"` ) . join ( ', ' ) ; return `.index(fields: [${ fields }], name: ${ name })` ; } ) ; } protected isHasManyConnectionField ( field ) { if ( field . connectionInfo && field . connectionInfo . kind === CodeGenConnectionType . HAS_MANY ) { return true ; } return false ; } protected generateAuthRules ( model ) { const authDirectives = ( model . directives . filter ( d => d . name === 'auth' ) as any ) as AuthDirective [ ] ; const rules = [ ] ; authDirectives . forEach ( directive => { directive . arguments ?. rules . forEach ( rule => { const authRule = [ ] ; switch ( rule . allow ) { case AuthStrategy . owner : authRule . push ( 'allow: .owner' ) ; authRule . push ( `ownerField: "${ rule . ownerField }"` ) ; authRule . push ( `identityClaim: "${ rule . identityClaim }"` ) ; break ; case AuthStrategy . private : authRule . push ( 'allow: .private' ) ; break ; case AuthStrategy . public : authRule . push ( 'allow: .public' ) ; break ; case AuthStrategy . groups : authRule . push ( 'allow: .groups' ) ; authRule . push ( `groupClaim: "${ rule . groupClaim }"` ) ; if ( rule . groups ) { authRule . push ( `groups: [${ rule . groups ?. map ( group => `"${ group }"` ) . join ( ', ' ) }]` ) ; } else { authRule . push ( `groupsField: "${ rule . groupField }"` ) ; } break ; default : printWarning ( `Model ${ model . name } has auth with authStrategy ${ rule . allow } of which is not yet supported in DataStore.` ) ; return ; } if ( rule . provider != null && this . config . emitAuthProvider ) { authRule . push ( `provider: .${ rule . provider }` ) ; } authRule . push ( `operations: [${ rule . operations ?. map ( op => `.${ op }` ) . join ( ', ' ) }]` ) ; rules . push ( `rule(${ authRule . join ( ', ' ) })` ) ; } ) ; } ) ; if ( rules . length ) { return [ '[' , `${ indentMultiline ( rules . join ( ',\n' ) ) }` , ']' ] . join ( '\n' ) ; } return '' ; } protected getWritableFields ( model ) { return model . fields . filter ( f => ! f . isReadOnly ) ; } protected hasReadOnlyFields ( model ) { return model . fields . filter ( f => f . isReadOnly ) . length !== 0 ; } }