diff --git a/src/cli/journey/journey-describe.ts b/src/cli/journey/journey-describe.ts index 83cdc1fd6..363d12623 100644 --- a/src/cli/journey/journey-describe.ts +++ b/src/cli/journey/journey-describe.ts @@ -43,6 +43,10 @@ export default function setup() { "Override version. Notation: '..' e.g. '7.2.0'. Override detected version with any version. This is helpful in order to check if journeys in one environment would be compatible running in another environment (e.g. in preparation of migrating from on-prem to ForgeRock Identity Cloud." ) ) + .addOption( + new Option( + '-u, --usage', + 'List all uses of the journey as an inner node for other journies.')) .action( // implement command logic inside action handler async (host, realm, user, password, options, command) => { @@ -119,7 +123,8 @@ export default function setup() { if (!options.markdown) { const outcome = await describeJourney( journeyData, - createFileParamTreeExportResolver(options.file) + createFileParamTreeExportResolver(options.file), + options.usage ); if (!outcome) process.exitCode = 1; } @@ -154,7 +159,7 @@ export default function setup() { const treeData = await exportJourney(journey['_id']); // ANSI text output if (!options.markdown) { - const outcome = await describeJourney(treeData); + const outcome = await describeJourney(treeData, undefined, options.usage); if (!outcome) process.exitCode = 1; } // Markdown output @@ -175,7 +180,7 @@ export default function setup() { const treeData = await exportJourney(options.journeyId); // ANSI text output if (!options.markdown) { - const outcome = await describeJourney(treeData); + const outcome = await describeJourney(treeData, undefined, options.usage); if (!outcome) process.exitCode = 1; } // Markdown output diff --git a/src/ops/JourneyOps.ts b/src/ops/JourneyOps.ts index 6246957f9..1d97e509c 100644 --- a/src/ops/JourneyOps.ts +++ b/src/ops/JourneyOps.ts @@ -13,6 +13,7 @@ import { import fs from 'fs'; import { + createKeyValueTable, createProgressIndicator, createTable, debugMessage, @@ -30,6 +31,7 @@ import * as Script from './ScriptOps'; import * as Theme from './ThemeOps'; import { cloneDeep, errorHandler } from './utils/OpsUtils'; import wordwrap from './utils/Wordwrap'; +import { getFullExportConfig } from '../utils/Config'; const { getTypedFilename, @@ -644,7 +646,8 @@ function describeTreeDescendentsMd( */ export async function describeJourney( journeyData: SingleTreeExportInterface, - resolveTreeExport: TreeExportResolverInterface = onlineTreeExportResolver + resolveTreeExport: TreeExportResolverInterface = onlineTreeExportResolver, + usage = false ): Promise { const errors: Error[] = []; try { @@ -717,6 +720,34 @@ export async function describeJourney( 'data' ); } + // Usage + if (usage) { + try { + const journeysExport = await exportJourneys({ + useStringArrays: true, + deps: false, + coords: false, + }); + let journey = journeyData as SingleTreeExportInterface & { + locations: string[]; + }; + const journeyName = + typeof journey.tree === 'string' ? journey.tree : journey.tree?._id; + journey.locations = getJourneyLocations(journeysExport, journeyName); + const table = createKeyValueTable(); + table.push([ + `Usage Locations (${journey.locations.length} total)`['brightCyan'], + journey.locations.length > 0 ? journey.locations[0] : '', + ]); + for (let i = 1; i < journey.locations.length; i++) { + table.push(['', journey.locations[i]]); + } + printMessage(table.toString(), 'data'); + return true; + } catch (error) { + return false; + } + } // Dependency Tree try { @@ -1173,3 +1204,39 @@ export async function deleteJourneys( } return false; } + +/** + * Helper that finds all locations where a journey is being used as an inner journey in another journey + * @param journeysExport export data containing journeys + * @param journeyName ID of the journey to search for as an inner journey + */ +function getJourneyLocations( + journeysExport: MultiTreeExportInterface, + journeyName: string +): string[] { + const locations: string[] = []; + for (const journeyData of Object.values(journeysExport.trees)) { + interface InnerTreeNode { + _id: string; + _type?: { _id?: string }; + tree?: string | { _id: string }; + } + + for (const node of Object.values( + journeyData.nodes ?? {} + ) as InnerTreeNode[]) { + const innerTreeName = + typeof node.tree === 'string' ? node.tree : node.tree?._id; + + if ( + node._type?._id === 'InnerTreeEvaluatorNode' && + innerTreeName === journeyName + ) { + locations.push( + `journey.${journeyData.tree?._id ?? journeyData.tree._id}` + ); + } + } + } + return locations; +}