import { flags , SfdxCommand } from "@salesforce/command" ; import { Messages , Connection , SfdxError } from "@salesforce/core" ; import { getMetadata , getLayoutsForObject , getRecordTypesForObject } from "../../../shared/sfdxProjectFolder" ; import { AnyJson } from "@salesforce/ts-types" ; import * as path from 'path' ; var fs = require ( 'fs' ) ; var unzipper = require ( 'unzipper' ) ; const util = require ( 'util' ) ; const xml2js = require ( 'xml2js' ) ; Messages . importMessagesDirectory ( __dirname ) ; const messages = Messages . loadMessages ( "texei-sfdx-plugin" , "skinnyprofile-retrieve" ) ; const defaultProjectPath = path . join ( 'force-app' , 'main' , 'default' ) ; const defaultTimeout = 60000 ; let conn ; let retrievedProfiles = [ ] ; export default class Retrieve extends SfdxCommand { public static description = messages . getMessage ( "commandDescription" ) ; public static examples = [ "$ texei:skinnyprofile:retrieve -u MyScratchOrg" ] ; protected static flagsConfig = { timeout : flags . string ( { char : 't' , required : false , description : 'timeout(ms) for profile retrieve (Default: 60000ms)' } ) , } ; protected static requiresUsername = true ; protected static requiresDevhubUsername = false ; protected static requiresProject = true ; public nodesToRemove = [ 'userPermissions' , 'classAccesses' , 'externalDataSourceAccesses' , 'fieldPermissions' , 'objectPermissions' , 'pageAccesses' , 'tabVisibilities' , 'customMetadataTypeAccesses' ] ; public nodesHavingDefault = [ 'applicationVisibilities' , 'recordTypeVisibilities' ] ; public async run ( ) { conn = await this . org . getConnection ( ) ; let typesToRetrieve = [ ] ; let layoutsToRetrieve = [ ] ; let recordTypesToRetrieve = [ ] ; for ( const obj of await getMetadata ( "objects" ) ) { for ( const layout of await getLayoutsForObject ( obj ) ) { layoutsToRetrieve . push ( layout ) ; } for ( const recType of await getRecordTypesForObject ( obj ) ) { recordTypesToRetrieve . push ( recType ) ; } } if ( layoutsToRetrieve . length > 0 ) { typesToRetrieve . push ( { 'members' : layoutsToRetrieve , 'name' : 'Layout' } ) ; } if ( recordTypesToRetrieve . length > 0 ) { typesToRetrieve . push ( { 'members' : recordTypesToRetrieve , 'name' : 'RecordType' } ) ; } let applicationsToRetrieve = [ ] ; for ( const app of await getMetadata ( "applications" ) ) { applicationsToRetrieve . push ( app . replace ( '.app-meta.xml' , '' ) ) ; } if ( applicationsToRetrieve . length > 0 ) { typesToRetrieve . push ( { 'members' : applicationsToRetrieve , 'name' : 'CustomApplication' } ) ; } let profilesToRetrieve = await getMetadata ( "profiles" ) ; if ( profilesToRetrieve . length > 0 ) { typesToRetrieve . push ( { 'members' : profilesToRetrieve , 'name' : 'Profile' } ) ; } this . ux . startSpinner ( 'Retrieving Profiles' , null , { stdout : true } ) ; await this . retrievePackage ( typesToRetrieve ) ; this . ux . stopSpinner ( 'Done.' ) ; return { retrievedProfiles : retrievedProfiles } ; } private async retrievePackage ( packageToRetrieve ) { const maxApiVersion = await this . org . retrieveMaxApiVersion ( ) ; let mypackage = { apiVersion : maxApiVersion , singlePackage : true , unpackaged : { types : packageToRetrieve } } ; this . debug ( `DEBUG Retrieving Package:` ) ; this . debug ( JSON . stringify ( mypackage , null , 2 ) ) ; conn . metadata . pollTimeout = this . flags . timeout ? this . flags . timeout : defaultTimeout ; const parsed = await conn . metadata . retrieve ( mypackage ) . stream ( ) . pipe ( unzipper . Parse ( ) ) ; await new Promise < void > ( async ( resolve , reject ) => { this . debug ( `DEBUG Parsing retrieved package` ) ; const readFile = util . promisify ( fs . readFile ) ; const writeFile = util . promisify ( fs . writeFile ) ; try { parsed . on ( 'entry' , async ( entry ) => { this . debug ( `DEBUG Retrieved Metadata: ${ entry . path }` ) ; if ( entry . path . endsWith ( '.profile' ) ) { const profileFileName = entry . path . substring ( entry . path . lastIndexOf ( '/' ) + 1 , entry . path . length ) ; const profilePath = path . join ( defaultProjectPath , 'profiles' , `${ profileFileName }-meta.xml` ) ; const writeStream = fs . createWriteStream ( profilePath ) ; entry . pipe ( writeStream ) ; writeStream . on ( "finish" , async ( ) => { const myProfile = await this . cleanProfile ( await readFile ( profilePath , 'utf8' ) ) ; await writeFile ( profilePath , myProfile , 'utf8' ) . catch ( ( err ) => { throw new SfdxError ( `Unable to write file at path ${ profilePath }: ${ err }` ) ; } ) ; retrievedProfiles . push ( profilePath ) ; this . ux . log ( `Profile saved: ${ profilePath }` ) ; } ) ; } else { entry . autodrain ( ) ; } } ) ; parsed . on ( 'close' , ( ) => { this . debug ( `DEBUG Parsing finished.` ) ; resolve ( ) ; } ) ; } catch ( err ) { reject ( err ) ; } } ) . catch ( error => { throw new SfdxError ( `Error while parsing retrieved package: ${ error }` ) ; } ) ; } public async cleanProfile ( profile ) { var parser = new xml2js . Parser ( ) ; const parseString = util . promisify ( parser . parseString ) ; const profileJson = await parseString ( profile ) ; for ( const nodeKey in profileJson . Profile ) { if ( profileJson . Profile . hasOwnProperty ( nodeKey ) ) { if ( this . nodesToRemove . includes ( nodeKey ) ) { delete profileJson . Profile [ nodeKey ] ; } else if ( this . nodesHavingDefault . includes ( nodeKey ) ) { for ( const nodeValue in profileJson . Profile [ nodeKey ] ) { if ( profileJson . Profile [ nodeKey ] [ nodeValue ] . default [ 0 ] === 'false' ) { delete profileJson . Profile [ nodeKey ] [ nodeValue ] ; } } } } } const builder = new xml2js . Builder ( ) ; const xmlFile = builder . buildObject ( profileJson ) ; return xmlFile ; } }