import { flags , SfdxCommand } from '@salesforce/command' ; import { Messages , SfdxError , Connection } from '@salesforce/core' ; import { AnyJson } from '@salesforce/ts-types' ; import { ExecuteOptions } from 'jsforce' ; import * as fs from 'fs' ; import * as path from 'path' ; const util = require ( "util" ) ; Messages . importMessagesDirectory ( __dirname ) ; const messages = Messages . loadMessages ( 'texei-sfdx-plugin' , 'data-export' ) ; let conn ; let objectList ; let lastReferenceIds = new Map < string , number > ( ) ; let globallyExcludedFields ; let globallyOverridenLookup ; export default class Export extends SfdxCommand { public static description = messages . getMessage ( 'commandDescription' ) ; public static examples = [ `sfdx texei:data:export --objects Account,Contact,MyCustomObject__c --outputdir ./data --targetusername texei` , 'sfdx texei:data:export --dataplan ./data/data-plan.json --outputdir ./data --targetusername texei' ] ; protected static flagsConfig = { outputdir : flags . string ( { char : 'd' , description : messages . getMessage ( 'outputdirFlagDescription' ) , required : true } ) , objects : flags . string ( { char : 'o' , description : messages . getMessage ( 'objectsFlagDescription' ) , required : false } ) , dataplan : flags . string ( { char : 'p' , description : messages . getMessage ( 'dataPlanFlagDescription' ) , required : false } ) } ; protected static requiresUsername = true ; protected static requiresDevhubUsername = false ; protected static requiresProject = false ; public async run ( ) { conn = this . org . getConnection ( ) ; let recordIdsMap = new Map < string , string > ( ) ; if ( this . flags . objects ) { objectList = this . flags . objects . split ( ',' ) . map ( function ( elem ) { return { "name" : elem } ; } ) ; } else if ( this . flags . dataplan ) { const readFile = util . promisify ( fs . readFile ) ; const dataPlan = JSON . parse ( await readFile ( this . flags . dataplan , "utf8" ) ) ; objectList = dataPlan . sObjects ; if ( dataPlan . excludedFields ) { globallyExcludedFields = dataPlan . excludedFields ; } if ( dataPlan . lookupOverride ) { globallyOverridenLookup = new Map < string , string > ( ) ; for ( const [ key , value ] of Object . entries ( dataPlan . lookupOverride ) ) { globallyOverridenLookup . set ( key , value as string ) ; } } } else { throw new SfdxError ( `Either objects or dataplan flag is mandatory` ) ; } let index = 1 ; for ( const obj of objectList ) { this . ux . startSpinner ( `Exporting ${ obj . name }${ obj . label ? ' (' + obj . label + ')' : '' }` , null , { stdout : true } ) ; const fileName = `${ index }-${ obj . name }${ obj . label ? '-' + obj . label : '' }.json` ; const objectRecords = await this . getsObjectRecords ( obj , recordIdsMap ) ; await this . saveFile ( objectRecords , fileName ) ; index ++ ; this . ux . stopSpinner ( `${ fileName } saved.` ) ; } return { message : 'Data exported' } ; } private async getsObjectRecords ( sobject , recordIdsMap ) { let fields = [ ] ; let lookups = [ ] ; let overriddenLookups = [ ] ; let relationshipFields = [ ] ; let userFieldsReference = [ ] ; const describeResult = await conn . sobject ( sobject . name ) . describe ( ) ; const sObjectLabel = describeResult . label ; if ( ! describeResult . createable ) { throw new SfdxError ( `Object ${ sObjectLabel } can't be created (see Salesforce documentation), so you shoudn't export it.` ) ; } let fieldsToExclude = globallyExcludedFields ? globallyExcludedFields : [ ] ; if ( sobject . excludedFields ) { fieldsToExclude = fieldsToExclude . concat ( sobject . excludedFields ) ; } for ( const field of describeResult . fields ) { if ( field . createable && ! fieldsToExclude . includes ( field . name ) ) { if ( sobject . lookupOverride && sobject . lookupOverride [ field . name ] ) { this . debug ( `Field found in override: ${ field . name }` ) ; if ( ! ( field . referenceTo && field . referenceTo . length > 0 ) ) { throw new SfdxError ( `Field ${ field . name } is listed in lookupOverride but isn't a lookup field` ) ; } else { overriddenLookups . push ( field . relationshipName ) ; sobject . lookupOverride [ field . name ] ?. split ( ',' ) ?. forEach ( relationshipField => { relationshipFields . push ( `${ field . relationshipName }.${ relationshipField }` ) ; } ) ; } } else { if ( field . referenceTo && field . referenceTo . length > 0 && field . name != 'OwnerId' && field . name != 'RecordTypeId' ) { if ( globallyOverridenLookup ?. get ( field . referenceTo [ 0 ] ) ) { this . debug ( `FOUND ${ field . name }: ${ globallyOverridenLookup . get ( field . referenceTo [ 0 ] ) }` ) ; overriddenLookups . push ( field . relationshipName ) ; globallyOverridenLookup . get ( field . referenceTo [ 0 ] ) ?. split ( ',' ) ?. forEach ( relationshipField => { relationshipFields . push ( `${ field . relationshipName }.${ relationshipField }` ) ; } ) ; } else { fields . push ( field . name ) ; if ( ! objectList . find ( x => x . name === 'User' ) && field . referenceTo . includes ( 'User' ) ) { userFieldsReference . push ( field . name ) ; } else { lookups . push ( field . name ) ; } } } else { fields . push ( field . name ) ; } } } } if ( describeResult . recordTypeInfos . length > 1 ) { fields . push ( 'RecordType.DeveloperName' ) ; } if ( sobject . name === 'Pricebook2' ) { fields . push ( 'IsStandard' ) ; } if ( relationshipFields ) { fields = fields . concat ( relationshipFields ) ; } const recordQuery = `SELECT Id, ${ fields . join ( ) }
                         FROM ${ sobject . name }
                         ${ sobject . filters ? 'WHERE ' + sobject . filters : '' }
                         ${ sobject . orderBy ? 'ORDER BY ' + sobject . orderBy : '' }` ; const recordNumber = ( ( await conn . query ( `Select count(Id) numberOfRecords from ${ sobject . name }` ) ) . records [ 0 ] as any ) . numberOfRecords ; let options = { } ; if ( recordNumber > 10000 ) { options . maxFetch = recordNumber ; } const recordResults = ( await conn . autoFetchQuery ( recordQuery , options ) ) . records ; await this . cleanJsonRecord ( sobject , sObjectLabel , recordResults , recordIdsMap , lookups , overriddenLookups , userFieldsReference ) ; const objectAttributes = { } ; objectAttributes . type = sobject . name ; if ( sobject . externalId ) { objectAttributes . externalId = sobject . externalId ; } const recordFile = { } ; recordFile . attributes = objectAttributes ; recordFile . records = recordResults ; return recordFile ; } private async cleanJsonRecord ( sobject , objectLabel , records , recordIdsMap , lookups , overriddenLookups , userFieldsReference ) { let refId = 0 ; if ( lastReferenceIds . get ( objectLabel ) ) { refId = lastReferenceIds . get ( objectLabel ) ; } for ( const record of records ) { delete record . attributes . url ; if ( recordIdsMap . get ( record . Id ) ) { record . attributes . referenceId = recordIdsMap . get ( record . Id ) ; } else { if ( sobject . name === 'Pricebook2' && record . IsStandard ) { const standardPriceBookLabel = 'StandardPriceBook' ; record . attributes . referenceId = standardPriceBookLabel ; recordIdsMap . set ( record . Id , standardPriceBookLabel ) ; } else { refId ++ ; record . attributes . referenceId = `${ objectLabel . replace ( / /g , '' ) . replace ( /'/g , '' ) }Ref${ refId }` ; recordIdsMap . set ( record . Id , record . attributes . referenceId ) ; } } for ( const lookup of lookups ) { record [ lookup ] = recordIdsMap . get ( record [ lookup ] ) ; } for ( const lookup of overriddenLookups ) { if ( record [ lookup ] ) { delete record [ lookup ] . attributes . url ; } else { delete record [ lookup ] ; } } if ( record . RecordTypeId && record . RecordType ) { record . RecordTypeId = record . RecordType . DeveloperName ; } for ( const userField of userFieldsReference ) { record [ userField ] = 'SfdxOrgUser' ; } delete record . Id ; delete record . RecordType ; delete record . OwnerId ; if ( sobject . name === 'Pricebook2' ) { delete record . IsStandard ; } } lastReferenceIds . set ( objectLabel , refId ) ; } private async saveFile ( records , fileName ) { let filePath = fileName ; if ( this . flags . outputdir ) { filePath = path . join ( this . flags . outputdir , fileName ) ; } const saveToPath = path . join ( process . cwd ( ) , filePath ) ; await fs . writeFile ( saveToPath , JSON . stringify ( records , null , 2 ) , 'utf8' , function ( err ) { if ( err ) { throw new SfdxError ( `Unable to write file at path ${ saveToPath }: ${ err }` ) ; } } ) ; } }