file copyright author Vivek M. Chawla <@VivekMChawla>
 *  version license summary description import { AnyJson } from '@salesforce/ts-types' ; import { JsonMap } from '@salesforce/ts-types' ; import { Observable } from 'rxjs' ; import { SfdxFalconDebug } from '../../../../modules/sfdx-falcon-debug' ; import { SfdxFalconError } from '../../../../modules/sfdx-falcon-error' ; import { SfdxFalconResult } from '../../../../modules/sfdx-falcon-result' ; import { SfdxFalconResultStatus } from '../../../../modules/sfdx-falcon-result' ; import { SfdxFalconResultType } from '../../../../modules/sfdx-falcon-result' ; import { ListrContext } from '../../../../modules/sfdx-falcon-types' ; import { ListrExecutionOptions } from '../../../../modules/sfdx-falcon-types' ; import { ListrObservable } from '../../../../modules/sfdx-falcon-types' ; import { ListrObject } from '../../../../modules/sfdx-falcon-types' ; import { SfdxCliLogLevel } from '../../../../modules/sfdx-falcon-types' ; import { ActionOptions } from '../../types' ; import { CompileOptions } from '../../types' ; import { ExecutionOptions } from '../../types' ; import { TargetOrg } from '../../types' ; import { SfdxFalconProject } from '../../../../modules/sfdx-falcon-project' ; import { SfdxFalconRecipe } from '../../../../modules/sfdx-falcon-recipe' ; import { SfdxFalconRecipeJson } from '../../../../modules/sfdx-falcon-recipe' ; import { AppxEngineAction } from '../appx/actions' ; const listr = require ( 'listr' ) ; const falconUpdateRenderer = require ( 'falcon-listr-update-renderer' ) ; const dbgNs = 'ENGINE:appx:' ; export interface AppxEngineContext { compileOptions : CompileOptions ; recipeObserver : ListrObservable ; executing : boolean ; initialized : boolean ; haltOnError : boolean ; skipGroups : string [ ] ; skipActions : string [ ] ; devHubAlias : string ; projectContext : SfdxFalconProject ; logLevel : SfdxCliLogLevel ; targetOrg : TargetOrg ; } export interface AppxEngineActionContext extends AppxEngineContext { listrExecOptions : ListrExecutionOptions ; } export type AppxEngineActionFunction = ( actionContext : AppxEngineActionContext , actionOptions : ActionOptions ) => Promise < SfdxFalconResult > ; export interface AppxEngineHandler { handlerName : string ; } export interface AppxEngineStepGroup { stepGroupName : string ; alias : string ; description : string ; recipeSteps : AppxEngineStep [ ] ; } export interface AppxEngineStep { stepName : string ; description : string ; action : string ; options : JsonMap ; onSuccess ? : string ; onError ? : string ; } export interface AppxEngineStepResult { status : AppxEngineStepResultStatus ; message : string ; data : AnyJson ; } export enum AppxEngineStepResultStatus { SUCCESS = 'SUCCESS' , WARNING = 'WARNING' , ERROR = 'ERROR' } export interface FalconEngineResultDetail { recipeName : string ; engineName : string ; engineContext : AppxEngineContext ; projectContext : SfdxFalconProject ; supportedActions : IterableIterator < string > ; } class summary description public abstract export abstract class AppxRecipeEngine { method param { SfdxFalconRecipeJson } recipe returns { void } description protected static protected static validateOuterRecipe ( recipe ) { if ( typeof recipe . options === 'undefined' ) { throw new Error ( `ERROR_INVALID_RECIPE: Recipes in the AppX family (eg. '${ recipe . recipeType }' ` + `must provide values in the 'options' key of your recipe` ) ; } if ( Array . isArray ( recipe . options . skipGroups ) === false ) { throw new Error ( `ERROR_INVALID_RECIPE: An array of strings must be provided in the ` + `'options.skipGroups' key of your recipe.  The value provided was of type ` + `${ typeof recipe . options . skipGroups }` ) ; } if ( Array . isArray ( recipe . options . skipActions ) === false ) { throw new Error ( `ERROR_INVALID_RECIPE: An array of strings must be provided for the ` + `'options.skipActions' key of your recipe.  The value you provided was of type ` + `${ typeof recipe . options . skipActions }` ) ; } if ( typeof recipe . options . haltOnError !== 'boolean' ) { throw new Error ( `ERROR_INVALID_RECIPE: A boolean value must be provided for the ` + `'options.haltOnError' key of your recipe.  The value you provided was of type ` + `${ typeof recipe . options . haltOnError }` ) ; } if ( Array . isArray ( recipe . options . targetOrgs ) === false || ( recipe . options . targetOrgs as unknown as TargetOrg [ ] ) . length < 1 ) { throw new Error ( `ERROR_INVALID_RECIPE: An array with at least one Target Org must be provided in the ` + `'options.targetOrgs' key of your recipe.` ) ; } for ( const targetOrg of recipe . options . targetOrgs as unknown as TargetOrg [ ] ) { AppxRecipeEngine . validateTargetOrg ( targetOrg ) ; } if ( Array . isArray ( recipe . recipeStepGroups ) === false ) { throw new Error ( `ERROR_INVALID_RECIPE: An array of Recipe Step Groups must be provided at the root ` + `level of your recipe. The value you provided was of type ` + `${ typeof recipe . recipeStepGroups }` ) ; } for ( const recipeStepGroup of recipe . recipeStepGroups as unknown as AppxEngineStepGroup [ ] ) { AppxRecipeEngine . validateRecipeStepGroup ( recipeStepGroup ) ; } if ( Array . isArray ( recipe . handlers ) === false ) { throw new Error ( `ERROR_INVALID_RECIPE: An array of Handlers must be provided at the root ` + `level of your recipe. The value you provided was of type ` + `${ typeof recipe . handlers }` ) ; } for ( const handler of recipe . handlers as unknown as AppxEngineHandler [ ] ) { AppxRecipeEngine . validateHandler ( handler ) ; } return ; } method param { AppxEngineHandler } handler returns { void } description private static private static validateHandler ( handler ) { } method param { AppxEngineStep } recipeStep returns { void } description private static private static validateRecipeStep ( recipeStep ) { } method param { AppxRecipeStepGroup } stepGroup returns { void } description private static private static validateRecipeStepGroup ( stepGroup ) { if ( typeof stepGroup . stepGroupName !== 'string' || stepGroup . stepGroupName === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: Missing string value for 'stepGroupName' in  ` + `one of your 'recipeStepGroup' definitions ` + `(type provided: ${ typeof stepGroup . stepGroupName })` ) ; } if ( typeof stepGroup . alias !== 'string' || stepGroup . alias === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: Missing string value for 'alias' in the ` + `recipeStepGroup '${ stepGroup . stepGroupName }' ` + `(type provided: ${ typeof stepGroup . alias })` ) ; } if ( typeof stepGroup . description !== 'string' || stepGroup . description === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: Missing string value 'description' in ` + `one of your 'recipeStepGroup' definitions ` + `(type provided: ${ typeof stepGroup . description })` ) ; } if ( Array . isArray ( stepGroup . recipeSteps ) === false ) { throw new Error ( `ERROR_INVALID_RECIPE: Missing array of Recipe Steps in ` + `one of your 'recipeStepGroup' definitions. ` + `(type provided: ${ typeof stepGroup . recipeSteps })` ) ; } for ( const recipeStep of stepGroup . recipeSteps ) { AppxRecipeEngine . validateRecipeStep ( recipeStep ) ; } } method param { TargetOrg } targetOrg returns { void } description private private static validateTargetOrg ( targetOrg ) { if ( typeof targetOrg . orgName !== 'string' || targetOrg . orgName === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: A string value must be provided for the ` + `'orgName' key in each targetOrg in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . orgName }` ) ; } if ( typeof targetOrg . alias !== 'string' || targetOrg . alias === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: A string value must be provided for the ` + `'alias' key in each targetOrg in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . alias }` ) ; } if ( typeof targetOrg . description !== 'string' || targetOrg . description === '' ) { throw new Error ( `ERROR_INVALID_RECIPE: A string value must be provided for the ` + `'description' key in each targetOrg in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . description }` ) ; } if ( typeof targetOrg . isScratchOrg !== 'boolean' ) { throw new Error ( `ERROR_INVALID_RECIPE: A boolean value must be provided for the ` + `'isScratchOrg' key in each targetOrg in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . isScratchOrg }` ) ; } if ( targetOrg . isScratchOrg === true && ( typeof targetOrg . scratchDefJson !== 'string' || targetOrg . scratchDefJson === '' ) ) { throw new Error ( `ERROR_INVALID_RECIPE: If targetOrg.isScratchOrg is TRUE then a string value must be provided for the ` + `'scratchDefJson' key in that targetOrg's definition in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . scratchDefJson }` ) ; } if ( targetOrg . isScratchOrg === false && ( typeof targetOrg . orgReqsJson !== 'string' || targetOrg . orgReqsJson === '' ) ) { throw new Error ( `ERROR_INVALID_RECIPE: If targetOrg.isScratchOrg is FALSE then a string value must be provided for the ` + `'orgReqsJson' key in that targetOrg's definition in your recipe.  The value you provided was of type ` + `${ typeof targetOrg . orgReqsJson }` ) ; } } protected actionExecutorMap : Map < string , AppxEngineAction > ; protected listrTasks : ListrObject ; protected recipe : SfdxFalconRecipe ; protected falconEngineResult : SfdxFalconResult ; protected falconEngineResultDetail : FalconEngineResultDetail ; protected preBuildStepGroups : AppxEngineStepGroup [ ] ; protected postBuildStepGroups : AppxEngineStepGroup [ ] ; protected engineContext : AppxEngineContext ; constructs param { string } engineName param { string } recipeName description constructor ( engineName , recipeName ) { this . engineContext = { } as AppxEngineContext ; this . preBuildStepGroups = new Array < AppxEngineStepGroup > ( ) ; this . postBuildStepGroups = new Array < AppxEngineStepGroup > ( ) ; this . listrTasks = null ; this . falconEngineResult = new SfdxFalconResult ( `${ engineName }:${ recipeName }` , SfdxFalconResultType . ENGINE , { startNow : false , bubbleError : true , bubbleFailure : true } ) ; this . falconEngineResultDetail = { projectContext : null , recipeName : recipeName , engineName : engineName , engineContext : null , supportedActions : null } ; } method param { ExecutionOptions } executionOptions returns { Promise < SfdxFalconResult > } description public async public async execute ( executionOptions = { } ) { if ( this . listrTasks == null ) { throw new Error ( 'ERROR_RECIPE_NOT_COMPILED: AppxRecipeEngine.execute() called before compiling a Recipe' ) ; } this . startExecution ( ) ; await this . executeEngine ( executionOptions ) . then ( listrContext => { this . onSuccess ( listrContext ) ; } ) . catch ( listrError => { this . onError ( listrError ) ; } ) ; this . endExecution ( ) ; return this . falconEngineResult ; } protected abstract async executeEngine ( executionOptions ) ; protected abstract async initializeActionMap ( ) ; protected abstract async initializePostBuildStepGroups ( ) ; protected abstract async initializePreBuildStepGroups ( ) ; protected abstract async initializeRecipeEngineContext ( ) ; protected abstract async initializeSkipActions ( ) ; protected abstract async initializeSkipGroups ( ) ; protected abstract async initializeTargetOrg ( ) ; method param { SfdxFalconRecipe } recipe param { CompileOptions } compileOptions returns { Promise < void > } description protected async protected async compile ( recipe , compileOptions ) { if ( recipe . validated !== true ) { throw new Error ( `ERROR_INVALID_RECIPE: Can not compile a recipe that has not been validated` ) ; } else { this . recipe = recipe ; } this . engineContext . compileOptions = compileOptions ; await this . initializeRecipeEngineContext ( ) ; await this . initializeTargetOrg ( ) ; await this . initializePreBuildStepGroups ( ) ; await this . initializePostBuildStepGroups ( ) ; await this . initializeSkipActions ( ) ; await this . initializeSkipGroups ( ) ; await this . initializeActionMap ( ) ; this . initializeEngineResultDetail ( ) ; this . validateEngine ( ) ; this . compileAllTasks ( ) ; SfdxFalconDebug . obj ( `${ dbgNs }compile:this:` , this , `this: ` ) ; return ; } method returns { void } description private private compileAllTasks ( ) { if ( this . engineContext . initialized !== true ) { throw new Error ( `ERROR_ENGINE_NOT_INITIALIZED: The engine must be fully initialized before ` + `compiling all tasks.` ) ; } SfdxFalconDebug . obj ( `${ dbgNs }compileAllTasks:preBuildStepGroups:` , this . preBuildStepGroups , `preBuildStepGroups: ` ) ; SfdxFalconDebug . obj ( `${ dbgNs }compileAllTasks:recipeStepGroups:` , this . recipe . recipeStepGroups , `recipeStepGroups: ` ) ; SfdxFalconDebug . obj ( `${ dbgNs }compileAllTasks:postBuildStepGroups:` , this . postBuildStepGroups , `postBuildStepGroups: ` ) ; const completeRecipeStepGroups = [ ... this . preBuildStepGroups , ... this . recipe . recipeStepGroups , ... this . postBuildStepGroups ] ; SfdxFalconDebug . obj ( `${ dbgNs }compileAllTasks:completeRecipeStepGroups:` , completeRecipeStepGroups , `completeRecipeStepGroups: ` ) ; this . listrTasks = this . compileParentTasks ( completeRecipeStepGroups as AppxEngineStepGroup [ ] ) ; return ; } method param { AppxEngineStepGroup [ ] } recipeStepGroups returns { ListrObject } description private private compileParentTasks ( recipeStepGroups ) { const parentTasks = new listr ( { concurrent : false , collapse : false , renderer : falconUpdateRenderer } ) ; for ( const recipeStepGroup of recipeStepGroups ) { if ( this . skipGroup ( recipeStepGroup . alias ) === true ) { continue ; } if ( this . stepGroupHasActiveTasks ( recipeStepGroup ) === false ) { continue ; } parentTasks . add ( { title : recipeStepGroup . stepGroupName , task : listrContext => this . compileSubTasks ( recipeStepGroup ) } ) ; } return parentTasks ; } method param { AppxRecipeStepGroup } recipeStepGroup returns { ListrObject } description private private compileSubTasks ( recipeStepGroup ) { if ( recipeStepGroup . recipeSteps . length < 1 ) { throw new Error ( `ERROR_NO_STEPS: The Recipe Step Group '${ recipeStepGroup . stepGroupName }' contains no Steps` ) ; } const listrSubTasks = new listr ( { concurrent : false , collapse : false , renderer : falconUpdateRenderer } ) ; for ( const recipeStep of recipeStepGroup . recipeSteps ) { if ( this . skipAction ( recipeStep . action ) === true ) { continue ; } listrSubTasks . add ( { title : recipeStep . stepName , task : ( listrContext , thisTask ) => { return new Observable ( observer => { const listrExecOptions = { listrContext : listrContext , listrTask : thisTask , observer : observer } ; this . executeStep ( recipeStep , listrExecOptions ) . then ( resolvedPromise => { const falconActionResult = SfdxFalconResult . wrap ( resolvedPromise , SfdxFalconResultType . FUNCTION , 'executeStep:then' ) ; falconActionResult . debugResult ( 'LISTR TASK DEBUG - AppxRecipeEngine:executeStep (Promise Resolved)' , 'LISTR_TASK_DEBUG:' ) ; try { this . falconEngineResult . addChild ( falconActionResult ) ; this . falconEngineResult . debugResult ( 'LISTR TASK DEBUG - AppxRecipeEngine:executeStep (Child Added)' , 'LISTR_TASK_DEBUG:' ) ; observer . complete ( ) ; } catch ( bubbledError ) { falconActionResult . debugResult ( 'LISTR TASK DEBUG - AppxRecipeEngine:executeStep (WARNING: Error Bubbled by a Resolved Promise)' , 'LISTR_TASK_DEBUG:' ) ; const unexpectedError = new SfdxFalconError ( `An ERROR was mistakenly bubbled by a Resolved Promise.` , `UnexpectedError` , `${ dbgNs }executeStep` , SfdxFalconError . wrap ( bubbledError ) ) ; unexpectedError . setData ( falconActionResult ) ; observer . error ( unexpectedError ) ; } } ) . catch ( rejectedPromise => { const falconActionResult = SfdxFalconResult . wrapRejectedPromise ( rejectedPromise , SfdxFalconResultType . UNKNOWN , `ActionResult (REJECTED)` ) ; falconActionResult . debugResult ( 'LISTR TASK DEBUG - AppxRecipeEngine:executeStep (Promise Rejected)' , 'LISTR_TASK_DEBUG:' ) ; try { this . falconEngineResult . addChild ( falconActionResult ) ; this . falconEngineResult . debugResult ( 'LISTR TASK DEBUG - Rejected Promise Error Suppressed' , 'LISTR_TASK_DEBUG:' ) ; observer . complete ( ) ; } catch ( bubbledError ) { falconActionResult . debugResult ( 'LISTR TASK DEBUG - Rejected Promise Error Bubbled' , 'LISTR_TASK_DEBUG:' ) ; observer . error ( bubbledError ) ; } } ) ; } ) ; } } ) ; } return listrSubTasks ; } method returns { void } description private private endExecution ( ) { } method param { AppxEngineStep } recipeStep param { ListrExecutionOptions } executionOptions returns { Promise < SfdxFalconResult > } description private async private async executeStep ( recipeStep , listrExecOptions ) { const actionContext = { ... this . engineContext , listrExecOptions : listrExecOptions } ; const actionExecutor = this . actionExecutorMap . get ( recipeStep . action ) ; if ( typeof actionExecutor === 'undefined' ) { throw new Error ( `ERROR_UNKNOWN_ACTION: '${ recipeStep . action }' is not recognized ` + `by the ${ this . recipe . recipeType } eninge` ) ; } return await actionExecutor . execute ( actionContext , recipeStep . options ) ; } method returns { void } description private private initializeEngineResultDetail ( ) { this . falconEngineResultDetail . projectContext = this . engineContext . projectContext ; this . falconEngineResultDetail . engineContext = this . engineContext ; this . falconEngineResultDetail . supportedActions = this . actionExecutorMap . keys ( ) ; this . falconEngineResult . setDetail ( this . falconEngineResultDetail ) ; } method param { unknown } resultOrError returns { void } description private private onError ( resultOrError ) { if ( ! SfdxFalconResult . validate ( resultOrError , SfdxFalconResultType . ENGINE , SfdxFalconResultStatus . ERROR ) ) { SfdxFalconDebug . obj ( `${ dbgNs }onError:resultOrError:` , { resultOrError : resultOrError } , `resultOrError: ` ) ; const falconError = new SfdxFalconError ( `ERROR_UNEXPECTED_LISTR_RESULT: Engine ${ this . falconEngineResult . name } ` + `got an unexpected result from listr.run()` ) ; this . falconEngineResult . throw ( falconError ) ; } const lastActionResult = this . falconEngineResult . children [ this . falconEngineResult . children . length - 1 ] ; SfdxFalconDebug . obj ( `${ dbgNs }onError:lastActionResult:` , lastActionResult , `lastActionResult: ` ) ; SfdxFalconDebug . obj ( `${ dbgNs }onError:falconEngineResult:` , this . falconEngineResult , `falconEngineResult: ` ) ; throw this . falconEngineResult ; } method param { ListrContext } listrContext returns { void } description private private onSuccess ( listrContext ) { SfdxFalconDebug . obj ( `${ dbgNs }onSuccess:listrContext:` , listrContext , `listrContext: ` ) ; if ( this . falconEngineResult . status === SfdxFalconResultStatus . FAILURE ) { if ( this . engineContext . haltOnError ) { } } this . falconEngineResult . success ( ) ; return ; } method param { string } actionToCheck returns { boolean } description private private skipAction ( actionToCheck = '' ) { return this . engineContext . skipActions . includes ( actionToCheck ) ; } method param { string } groupToCheck returns { boolean } description private private skipGroup ( groupToCheck = '' ) { return this . engineContext . skipGroups . includes ( groupToCheck ) ; } method returns { void } description private private startExecution ( ) { } method param { AppxEngineStepGroup } stepGroupToCheck returns { boolean } description version private private stepGroupHasActiveTasks ( stepGroupToCheck ) { if ( stepGroupToCheck . recipeSteps . length < 1 ) { return false ; } for ( const step of stepGroupToCheck . recipeSteps ) { if ( this . skipAction ( step . action ) === false ) { return true ; } } return false ; } method returns { void } description private private validateEngine ( ) { if ( this . actionExecutorMap instanceof Map && this . actionExecutorMap . size < 1 ) { throw new Error ( `ERROR_INVALID_ENGINE: actionExecutorMap is invalid or empty. ` + `Check your implementation of initializeActionMap() to troubleshoot.` ) ; } if ( Array . isArray ( this . preBuildStepGroups ) === false ) { throw new Error ( `ERROR_INVALID_ENGINE: preBuildStepGroups is not an array. ` + `Check your implementation of initializePreBuildStepGroups() to troubleshoot.` ) ; } if ( Array . isArray ( this . postBuildStepGroups ) === false ) { throw new Error ( `ERROR_INVALID_ENGINE: postBuildStepGroups is not an array. ` + `Check your implementation of initializePostBuildStepGroups() to troubleshoot.` ) ; } if ( ( ! this . engineContext . targetOrg ) || ( ! this . engineContext . targetOrg . alias ) ) { throw new Error ( `ERROR_INVALID_ENGINE_CONTEXT: engineContext.targetOrg is invalid or empty. ` + `Check your implementation of initializeTargetOrg() to troubleshoot.` ) ; } if ( Array . isArray ( this . engineContext . skipActions ) === false ) { throw new Error ( `ERROR_INVALID_ENGINE_CONTEXT: engineContext.skipActions is invalid. ` + `Check your implementation of initializeSkipActions() to troubleshoot.` ) ; } if ( Array . isArray ( this . engineContext . skipGroups ) === false ) { throw new Error ( `ERROR_INVALID_ENGINE_CONTEXT: engineContext.skipGroups is invalid. ` + `Check your implementation of initializeSkipGroups() to troubleshoot.` ) ; } this . engineContext . initialized = true ; return ; } }