import { camelCase } from 'lodash' ; import { ChildRelationship , Field , Rest , SalesforceFieldType , SFieldProperties , SObjectDescribe } from '../../ts-force' ; import { ClassDeclaration , DecoratorStructure , JSDocStructure , PropertyDeclarationStructure , Scope , SourceFile , ImportDeclarationStructure , StructureKind , WriterFunctions } from 'ts-morph' ; import { SObjectConfig } from './config' ; import { cleanAPIName , replaceSource } from './util' ; export const TS_FORCE_IMPORTS = { kind : StructureKind . ImportDeclaration , moduleSpecifier : 'ts-force' , namedImports : [ { name : 'Rest' } , { name : 'RestObject' } , { name : 'QueryOpts' } , { name : 'SObject' } , { name : 'sField' } , { name : 'SalesforceFieldType' } , { name : 'SFLocation' } , { name : 'SFieldProperties' } , { name : 'FieldResolver' } , { name : 'SOQLQueryParams' } , { name : 'buildQuery' } , { name : 'FieldProps' } , { name : 'PicklistConst' } , { name : 'CalendarDate' } ] } ; const SUPER_CLASS = 'RestObject' ; interface SalesforceDecoratorProps { apiName : string ; createable : boolean ; updateable : boolean ; required : boolean ; externalId : boolean ; childRelationship : boolean ; reference : any ; salesforceLabel : string ; salesforceType : SalesforceFieldType ; } export class SObjectGenerator { private sObjectConfig : SObjectConfig ; private allConfigsMap : Map < string , SObjectConfig > ; private dependsOn : Set < string > ; private sourceFile : SourceFile ; private singleFileMode : boolean ; private client ; private fieldsTypeAlias : string ; private pickLists : Map < string , [ string , string ] [ ] > ; param { SourceFile } sourceFile param { string [ ] } sObjectConfigs memberof constructor ( out , sObjectConfig , allConfigs ) { this . sObjectConfig = sObjectConfig ; this . pickLists = new Map < string , [ string , string ] [ ] > ( ) ; if ( typeof out === 'string' ) { this . sourceFile = replaceSource ( out ) ; this . singleFileMode = false ; } else { this . sourceFile = out ; this . singleFileMode = true ; } this . client = new Rest ( ) ; this . fieldsTypeAlias = `${ sObjectConfig . className }Fields` ; this . allConfigsMap = new Map ( allConfigs . map < [ string , SObjectConfig ] > ( x => [ x . apiName . toLowerCase ( ) , x ] ) ) ; this . dependsOn = new Set < string > ( ) ; } public async generateFile ( ) { try { this . sourceFile . addTypeAlias ( { name : this . fieldsTypeAlias , isExported : true , type : `Partial<FieldProps<${ this . sObjectConfig . className }>>` } ) ; await this . generateSObjectClass ( this . sObjectConfig ) ; if ( ! this . singleFileMode ) { this . sourceFile . addImportDeclaration ( TS_FORCE_IMPORTS ) ; this . sourceFile . addImportDeclaration ( { moduleSpecifier : './' , namedImports : [ ... this . dependsOn ] . filter ( c => c !== this . sObjectConfig . className ) . map ( c => { return { name : c } ; } ) } ) ; } } catch ( e ) { throw e ; } return this . sourceFile ; } public async generateSObjectClass ( sobConfig ) { let sobDescribe ; try { sobDescribe = await this . retrieveDescribe ( sobConfig . apiName ) ; } catch ( e ) { throw new Error ( `Could not retrieve describe metadata for ${ sobConfig . apiName }. Check SObject spelling and authorization ` ) ; } let props = [ ] ; props . push ( ... this . generateChildrenProps ( sobConfig , sobDescribe . childRelationships ) ) ; props . push ( ... this . generateFieldProps ( sobConfig , sobDescribe . fields ) ) ; let className = this . sObjectConfig . className ; let classDeclaration = this . generateClass ( sobConfig , className , props ) ; classDeclaration . addProperty ( { name : 'API_NAME' , scope : Scope . Public , isStatic : true , type : `'${ sobConfig . apiName }'` , initializer : `'${ sobConfig . apiName }'` } ) ; classDeclaration . addProperty ( { name : '_TYPE_' , scope : Scope . Public , isReadonly : true , type : `'${ sobConfig . apiName }'` , initializer : `'${ sobConfig . apiName }'` } ) ; classDeclaration . addProperty ( { name : '__UUID' , scope : Scope . Public , isStatic : true , initializer : `Symbol()` } ) ; classDeclaration . addProperty ( { name : '_fields' , scope : Scope . Private , isStatic : true , type : `{[P in keyof FieldProps<${ className }>]: SFieldProperties;}` } ) ; classDeclaration . addGetAccessor ( { name : 'FIELDS' , scope : Scope . Public , isStatic : true , statements : `return this._fields = this._fields ? this._fields : ${ className }.getPropertiesMeta<FieldProps<${ className }>,${ className }>(${ className })` } ) ; classDeclaration . addMethod ( { name : 'retrieve' , isStatic : true , scope : Scope . Public , parameters : [ { name : 'qryParam' , type : `((fields: FieldResolver<${ className }>) => SOQLQueryParams) | string` } , { name : 'opts' , type : 'QueryOpts' , hasQuestionToken : true } ] , returnType : `Promise<${ className }[]>` , isAsync : true , statements : `
            const qry = typeof qryParam === 'function' ? buildQuery(${ className }, qryParam) : qryParam;
            return await ${ SUPER_CLASS }.query<${ className }>(${ className }, qry, opts);
            ` } ) ; classDeclaration . addMethod ( { name : 'fromSFObject' , isStatic : true , scope : Scope . Public , parameters : [ { name : 'sob' , type : 'SObject' } ] , returnType : `${ className }` , statements : `return new ${ className }().mapFromQuery(sob);` } ) ; if ( this . sObjectConfig . generatePicklists ) { this . generatePickistConst ( classDeclaration ) ; } classDeclaration . forget ( ) ; } private async retrieveDescribe ( apiName ) { return await this . client . getSObjectDescribe ( apiName ) ; } private generateClass ( sobConfig , className , props ) { let classDeclaration = this . sourceFile . addClass ( { name : className , extends : SUPER_CLASS , isExported : true , properties : props , docs : [ { description : `Generated class for ${ sobConfig . apiName }` } ] } ) ; const interfaceParamName = 'fields' ; const constr = classDeclaration . addConstructor ( ) ; constr . addParameter ( { name : interfaceParamName , type : this . fieldsTypeAlias , hasQuestionToken : true } ) ; constr . addParameter ( { name : 'restInstance' , type : 'Rest' , hasQuestionToken : true } ) ; const propsInit = props . map ( prop => { return `this.${ prop . name } = void 0;` ; } ) . join ( '\n' ) ; let constructorBody = `super('${ sobConfig . apiName }', restInstance);
                            ${ propsInit }
                            this.__UUID = ${ sobConfig . className }.__UUID;
                            this.initObject(${ interfaceParamName });
                            return new Proxy(this, this.safeUpdateProxyHandler);` ; constr . setBodyText ( constructorBody ) ; return classDeclaration ; } private generatePickistConst ( classDeclaration ) { if ( this . pickLists . size ) { let picklistsConst = classDeclaration . addProperty ( { name : 'PICKLIST' , scope : Scope . Public , isStatic : true , isReadonly : true } ) ; let picklistConsts = { } ; this . pickLists . forEach ( ( values , field ) => { picklistConsts [ field ] = WriterFunctions . object ( values . reduce ( ( obj , pv ) => { obj [ pv [ 0 ] ] = writer => writer . quote ( pv [ 1 ] ) ; return obj ; } , { } ) ) ; } ) ; let x = writer => { WriterFunctions . object ( picklistConsts ) ( writer ) ; writer . write ( ' as const' ) ; } ; picklistsConst . setInitializer ( x ) ; } } private sanitizeProperty ( sobConfig , apiName , reference ) { let fieldMapping ; if ( sobConfig . fieldMappings ) { fieldMapping = sobConfig . fieldMappings . find ( mapping => { return mapping . apiName . toLowerCase ( ) === apiName . toLowerCase ( ) ; } ) ; } if ( fieldMapping ) { return fieldMapping . propName ; } else if ( sobConfig . autoConvertNames ) { let s = cleanAPIName ( apiName , sobConfig . keepNamespaces ) ; s += reference && ! apiName . endsWith ( 'Id' ) ? 'Id' : '' ; return camelCase ( s ) ; } else { return apiName ; } } private sanatizePicklistName ( picklistLabel , usedNames ) { let name = picklistLabel . split ( ' ' ) . join ( '_' ) ; name = name . replace ( /[^0-9a-z_]/gi , '' ) ; if ( Number . isInteger ( Number ( name . charAt ( 0 ) ) ) ) { name = '_' + name ; } name = name . toUpperCase ( ) ; name = this . makeNameUnique ( name , usedNames ) ; return name ; } private makeNameUnique ( name , usedNames ) { while ( usedNames . has ( name ) ) { name = `${ name }_dup` ; } return name ; } private generateChildrenProps ( sobConfig , children ) { const uniqueNames = new Set < string > ( ) ; let props = [ ] ; children . forEach ( child => { try { let relatedSobConfig = this . allConfigsMap . get ( child . childSObject . toLowerCase ( ) ) ; if ( ! relatedSobConfig || child . childSObject === sobConfig . apiName || child . deprecatedAndHidden === true || child . relationshipName === null ) { return ; } this . dependsOn . add ( relatedSobConfig . className ) ; let referenceClass = relatedSobConfig . className ; let decoratorProps = { apiName : child . relationshipName , required : false , createable : false , updateable : false , childRelationship : true , reference : referenceClass , externalId : false , salesforceLabel : child . relationshipName , salesforceType : SalesforceFieldType . REFERENCE } ; let propName = this . sanitizeProperty ( sobConfig , child . relationshipName , false ) ; propName = this . makeNameUnique ( propName , uniqueNames ) ; uniqueNames . add ( propName ) ; props . push ( { name : propName , kind : StructureKind . Property , type : `${ referenceClass }[]` , scope : Scope . Public , decorators : [ this . generateDecorator ( decoratorProps ) ] , hasQuestionToken : true } ) ; } catch ( e ) { throw e ; } } ) ; return props ; } private generateFieldProps ( sobConfig , fields ) { let props = [ ] ; const uniqueNames = new Set < string > ( ) ; fields . forEach ( field => { try { let docs = [ ] ; if ( field . inlineHelpText != null ) { docs . push ( { kind : StructureKind . JSDoc , description : field . inlineHelpText } ) ; } const referenceTo = field . referenceTo . length > 1 ? 'name' : ( field . referenceTo . length === 1 ? field . referenceTo [ 0 ] . toLowerCase ( ) : null ) ; if ( referenceTo && this . allConfigsMap . has ( referenceTo ) && field . type === SalesforceFieldType . REFERENCE && field . relationshipName !== null ) { let relatedSobConfig = this . allConfigsMap . get ( referenceTo ) ; this . dependsOn . add ( relatedSobConfig . className ) ; let referenceClass = relatedSobConfig . className ; let decoratorProps = { apiName : field . relationshipName , required : false , createable : false , updateable : false , childRelationship : false , reference : referenceClass , externalId : false , salesforceLabel : field . label , salesforceType : SalesforceFieldType . REFERENCE } ; props . push ( { name : this . sanitizeProperty ( sobConfig , field . relationshipName , false ) , type : referenceClass , scope : Scope . Public , kind : StructureKind . Property , decorators : [ this . generateDecorator ( decoratorProps ) ] , hasQuestionToken : true , docs : docs } ) ; } let propName = this . sanitizeProperty ( sobConfig , field . name , field . type === SalesforceFieldType . REFERENCE ) ; propName = this . makeNameUnique ( propName , uniqueNames ) ; uniqueNames . add ( propName ) ; let prop = { kind : StructureKind . Property , name : propName , type : this . mapSObjectType ( field . type ) + '| null' , scope : Scope . Public , isReadonly : ! ( field . createable || field . updateable ) , decorators : [ this . getDecorator ( field ) ] , hasQuestionToken : true , docs : docs } ; if ( this . sObjectConfig . generatePicklists ) { if ( field . picklistValues . length ) { let usedNames = new Set < string > ( ) ; let values = field . picklistValues . map < [ string , string ] > ( pv => { let name = this . sanatizePicklistName ( pv . value , usedNames ) ; usedNames . add ( name ) ; return [ name , pv . value ] ; } ) ; this . pickLists . set ( prop . name , values ) ; if ( this . sObjectConfig . enforcePicklistValues && ( this . sObjectConfig . enforcePicklistValues === 'RESTRICTED' && field . restrictedPicklist || this . sObjectConfig . enforcePicklistValues === 'ALL' ) ) { let picklist = `PicklistConst<typeof ${ sobConfig . className }.PICKLIST.${ prop . name }>` ; if ( field . type === SalesforceFieldType . MULTIPICKLIST ) { prop . type = `${ picklist }[]` ; } else { prop . type = picklist ; } } } } props . push ( prop ) ; } catch ( e ) { throw e ; } } ) ; return props ; } private mapSObjectType ( sfType ) { switch ( sfType ) { case SalesforceFieldType . DATETIME : return 'Date' ; case SalesforceFieldType . DATE : return 'CalendarDate' ; case SalesforceFieldType . BOOLEAN : return 'boolean' ; case SalesforceFieldType . DOUBLE : case SalesforceFieldType . INTEGER : case SalesforceFieldType . CURRENCY : case SalesforceFieldType . INT : case SalesforceFieldType . PERCENT : return 'number' ; case SalesforceFieldType . LOCATION : return 'SFLocation' ; case SalesforceFieldType . MULTIPICKLIST : return 'string[]' ; case SalesforceFieldType . REFERENCE : case SalesforceFieldType . STRING : case SalesforceFieldType . PICKLIST : case SalesforceFieldType . ID : default : return 'string' ; } } private mapTypeToEnum ( sfType ) { return `SalesforceFieldType.${ sfType . toUpperCase ( ) }` ; } private getDecorator ( field ) { let decoratorProps = { apiName : field . name , createable : field . createable , updateable : field . updateable , required : ( field . createable || field . updateable ) && field . nillable === false , externalId : field . externalId , childRelationship : false , reference : null , salesforceLabel : field . label , salesforceType : field . type } ; return this . generateDecorator ( decoratorProps ) ; } private generateDecorator ( decoratorProps ) { let ref = decoratorProps . reference != null ? `()=>{return ${ decoratorProps . reference }}` : 'undefined' ; let sfType = decoratorProps . salesforceType ? `${ this . mapTypeToEnum ( decoratorProps . salesforceType ) }` : 'undefined' ; let label = decoratorProps . salesforceLabel ? decoratorProps . salesforceLabel . replace ( /'/g , "\\'" ) : '' ; let props = { apiName : `'${ decoratorProps . apiName }'` , createable : `${ decoratorProps . createable }` , updateable : `${ decoratorProps . updateable }` , required : `${ decoratorProps . required }` , reference : `${ ref }` , childRelationship : `${ decoratorProps . childRelationship }` , salesforceType : `${ sfType }` , salesforceLabel : `'${ label }'` , externalId : `${ decoratorProps . externalId }` } ; let propsString = Object . keys ( props ) . map ( key => { return `${ key }: ${ props [ key ] }` ; } ) . join ( ', ' ) ; return { kind : StructureKind . Decorator , name : `sField` , arguments : [ `{${ propsString }}` ] } ; } }