@rockcarver/frodo-lib 0.12.1 → 0.12.2-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/cjs/api/BaseApi.js +36 -8
- package/cjs/api/BaseApi.js.map +1 -1
- package/cjs/api/NodeApi.js +190 -0
- package/cjs/api/NodeApi.js.map +1 -0
- package/cjs/api/NodeApi.test.js.map +1 -0
- package/cjs/api/StartupApi.js +21 -8
- package/cjs/api/StartupApi.js.map +1 -1
- package/cjs/api/StartupApi.test.js.map +1 -0
- package/cjs/api/TreeApi.js +28 -161
- package/cjs/api/TreeApi.js.map +1 -1
- package/cjs/api/TreeApi.test.js.map +1 -0
- package/cjs/index.js +15 -5
- package/cjs/index.js.map +1 -1
- package/cjs/ops/IdpOps.js +1 -1
- package/cjs/ops/IdpOps.js.map +1 -1
- package/cjs/ops/JourneyOps.js +202 -203
- package/cjs/ops/JourneyOps.js.map +1 -1
- package/cjs/ops/StartupOps.js +61 -71
- package/cjs/ops/StartupOps.js.map +1 -1
- package/cjs/ops/utils/Console.js +3 -2
- package/cjs/ops/utils/Console.js.map +1 -1
- package/esm/api/BaseApi.mjs +35 -7
- package/esm/api/NodeApi.mjs +114 -0
- package/esm/api/NodeApi.test.mjs +105 -0
- package/esm/api/StartupApi.mjs +18 -8
- package/esm/api/StartupApi.test.mjs +56 -0
- package/esm/api/TreeApi.mjs +27 -99
- package/esm/api/TreeApi.test.mjs +175 -0
- package/esm/index.mjs +7 -5
- package/esm/ops/IdpOps.mjs +1 -1
- package/esm/ops/JourneyOps.mjs +165 -154
- package/esm/ops/StartupOps.mjs +59 -62
- package/esm/ops/utils/Console.mjs +3 -2
- package/package.json +7 -4
- package/types/api/AuthenticateApi.d.ts +2 -0
- package/types/api/AuthenticateApi.d.ts.map +1 -0
- package/types/api/BaseApi.d.ts +50 -0
- package/types/api/BaseApi.d.ts.map +1 -0
- package/types/api/CirclesOfTrustApi.d.ts +24 -0
- package/types/api/CirclesOfTrustApi.d.ts.map +1 -0
- package/types/api/EmailTemplateApi.d.ts +22 -0
- package/types/api/EmailTemplateApi.d.ts.map +1 -0
- package/types/api/IdmConfigApi.d.ts +39 -0
- package/types/api/IdmConfigApi.d.ts.map +1 -0
- package/types/api/LogApi.d.ts +4 -0
- package/types/api/LogApi.d.ts.map +1 -0
- package/types/api/ManagedObjectApi.d.ts +21 -0
- package/types/api/ManagedObjectApi.d.ts.map +1 -0
- package/types/api/NodeApi.d.ts +38 -0
- package/types/api/NodeApi.d.ts.map +1 -0
- package/types/api/OAuth2ClientApi.d.ts +18 -0
- package/types/api/OAuth2ClientApi.d.ts.map +1 -0
- package/types/api/OAuth2OIDCApi.d.ts +22 -0
- package/types/api/OAuth2OIDCApi.d.ts.map +1 -0
- package/types/api/OAuth2ProviderApi.d.ts +5 -0
- package/types/api/OAuth2ProviderApi.d.ts.map +1 -0
- package/types/api/RealmApi.d.ts +30 -0
- package/types/api/RealmApi.d.ts.map +1 -0
- package/types/api/Saml2Api.d.ts +52 -0
- package/types/api/Saml2Api.d.ts.map +1 -0
- package/types/api/ScriptApi.d.ts +24 -0
- package/types/api/ScriptApi.d.ts.map +1 -0
- package/types/api/SecretsApi.d.ts +10 -0
- package/types/api/SecretsApi.d.ts.map +1 -0
- package/types/api/ServerInfoApi.d.ts +10 -0
- package/types/api/ServerInfoApi.d.ts.map +1 -0
- package/types/api/SocialIdentityProvidersApi.d.ts +31 -0
- package/types/api/SocialIdentityProvidersApi.d.ts.map +1 -0
- package/types/api/StartupApi.d.ts +14 -0
- package/types/api/StartupApi.d.ts.map +1 -0
- package/types/api/ThemeApi.d.ts +54 -0
- package/types/api/ThemeApi.d.ts.map +1 -0
- package/types/api/TreeApi.d.ts +24 -0
- package/types/api/TreeApi.d.ts.map +1 -0
- package/types/api/VariablesApi.d.ts +32 -0
- package/types/api/VariablesApi.d.ts.map +1 -0
- package/types/api/utils/ApiUtils.d.ts +29 -0
- package/types/api/utils/ApiUtils.d.ts.map +1 -0
- package/types/api/utils/Base64.d.ts +30 -0
- package/types/api/utils/Base64.d.ts.map +1 -0
- package/types/index.d.ts +26 -0
- package/types/index.d.ts.map +1 -0
- package/types/ops/AdminOps.d.ts +11 -0
- package/types/ops/AdminOps.d.ts.map +1 -0
- package/types/ops/AuthenticateOps.d.ts +6 -0
- package/types/ops/AuthenticateOps.d.ts.map +1 -0
- package/types/ops/CirclesOfTrustOps.d.ts +40 -0
- package/types/ops/CirclesOfTrustOps.d.ts.map +1 -0
- package/types/ops/ConnectionProfileOps.d.ts +47 -0
- package/types/ops/ConnectionProfileOps.d.ts.map +1 -0
- package/types/ops/EmailTemplateOps.d.ts +40 -0
- package/types/ops/EmailTemplateOps.d.ts.map +1 -0
- package/types/ops/IdmOps.d.ts +27 -0
- package/types/ops/IdmOps.d.ts.map +1 -0
- package/types/ops/IdpOps.d.ts +45 -0
- package/types/ops/IdpOps.d.ts.map +1 -0
- package/types/ops/JourneyOps.d.ts +145 -0
- package/types/ops/JourneyOps.d.ts.map +1 -0
- package/types/ops/LogOps.d.ts +5 -0
- package/types/ops/LogOps.d.ts.map +1 -0
- package/types/ops/ManagedObjectOps.d.ts +14 -0
- package/types/ops/ManagedObjectOps.d.ts.map +1 -0
- package/types/ops/OAuth2ClientOps.d.ts +24 -0
- package/types/ops/OAuth2ClientOps.d.ts.map +1 -0
- package/types/ops/OrganizationOps.d.ts +11 -0
- package/types/ops/OrganizationOps.d.ts.map +1 -0
- package/types/ops/RealmOps.d.ts +22 -0
- package/types/ops/RealmOps.d.ts.map +1 -0
- package/types/ops/SamlOps.d.ts +51 -0
- package/types/ops/SamlOps.d.ts.map +1 -0
- package/types/ops/ScriptOps.d.ts +30 -0
- package/types/ops/ScriptOps.d.ts.map +1 -0
- package/types/ops/SecretsOps.d.ts +63 -0
- package/types/ops/SecretsOps.d.ts.map +1 -0
- package/types/ops/StartupOps.d.ts +25 -0
- package/types/ops/StartupOps.d.ts.map +1 -0
- package/types/ops/ThemeOps.d.ts +66 -0
- package/types/ops/ThemeOps.d.ts.map +1 -0
- package/types/ops/VariablesOps.d.ts +39 -0
- package/types/ops/VariablesOps.d.ts.map +1 -0
- package/types/ops/utils/Console.d.ts +63 -0
- package/types/ops/utils/Console.d.ts.map +1 -0
- package/types/ops/utils/DataProtection.d.ts +6 -0
- package/types/ops/utils/DataProtection.d.ts.map +1 -0
- package/types/ops/utils/ExportImportUtils.d.ts +22 -0
- package/types/ops/utils/ExportImportUtils.d.ts.map +1 -0
- package/types/ops/utils/OpsUtils.d.ts +27 -0
- package/types/ops/utils/OpsUtils.d.ts.map +1 -0
- package/types/ops/utils/Wordwrap.d.ts +1 -0
- package/types/ops/utils/Wordwrap.d.ts.map +1 -0
- package/types/storage/SessionStorage.d.ts +47 -0
- package/types/storage/SessionStorage.d.ts.map +1 -0
- package/types/storage/StaticStorage.d.ts +14 -0
- package/types/storage/StaticStorage.d.ts.map +1 -0
package/esm/ops/JourneyOps.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
/* eslint-disable no-param-reassign */
|
|
2
1
|
import fs from 'fs';
|
|
3
|
-
import yesno from 'yesno';
|
|
4
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
3
|
import _ from 'lodash';
|
|
6
4
|
import { convertBase64TextToArray, getTypedFilename, saveJsonToFile, getRealmString, convertTextArrayToBase64, convertTextArrayToBase64Url } from './utils/ExportImportUtils';
|
|
7
5
|
import { getRealmManagedUser, replaceAll } from './utils/OpsUtils';
|
|
8
6
|
import storage from '../storage/SessionStorage';
|
|
9
|
-
import { getNode, putNode, deleteNode,
|
|
7
|
+
import { getNode, putNode, deleteNode, getNodeTypes, getNodesByType } from '../api/NodeApi';
|
|
8
|
+
import { getTrees, getTree, putTree, deleteTree } from '../api/TreeApi';
|
|
10
9
|
import { getEmailTemplate, putEmailTemplate } from '../api/EmailTemplateApi';
|
|
11
10
|
import { getScript } from '../api/ScriptApi';
|
|
12
11
|
import * as global from '../storage/StaticStorage';
|
|
@@ -21,9 +20,10 @@ import { createOrUpdateScript } from './ScriptOps';
|
|
|
21
20
|
const containerNodes = ['PageNode', 'CustomPageNode'];
|
|
22
21
|
const scriptedNodes = ['ConfigProviderNode', 'ScriptedDecisionNode', 'ClientScriptNode', 'SocialProviderHandlerNode', 'CustomScriptNode'];
|
|
23
22
|
const emailTemplateNodes = ['EmailSuspendNode', 'EmailTemplateNode'];
|
|
24
|
-
const emptyScriptPlaceholder = '[Empty]';
|
|
23
|
+
const emptyScriptPlaceholder = '[Empty]';
|
|
25
24
|
|
|
26
|
-
function
|
|
25
|
+
// use a function vs a template variable to avoid problems in loops
|
|
26
|
+
function createSingleTreeExportTemplate() {
|
|
27
27
|
return {
|
|
28
28
|
meta: {},
|
|
29
29
|
innerNodes: {},
|
|
@@ -36,10 +36,10 @@ function getSingleTreeFileDataTemplate() {
|
|
|
36
36
|
circlesOfTrust: {},
|
|
37
37
|
tree: {}
|
|
38
38
|
};
|
|
39
|
-
}
|
|
40
|
-
|
|
39
|
+
}
|
|
41
40
|
|
|
42
|
-
function
|
|
41
|
+
// use a function vs a template variable to avoid problems in loops
|
|
42
|
+
function createMultipleTreesExportTemplate() {
|
|
43
43
|
return {
|
|
44
44
|
meta: {},
|
|
45
45
|
trees: {}
|
|
@@ -113,18 +113,39 @@ async function getSaml2NodeDependencies(nodeObject, allProviders, allCirclesOfTr
|
|
|
113
113
|
};
|
|
114
114
|
return saml2NodeDependencies;
|
|
115
115
|
});
|
|
116
|
-
}
|
|
116
|
+
} // export async function getTreeNodes(treeObject) {
|
|
117
|
+
// const nodeList = Object.entries(treeObject.nodes);
|
|
118
|
+
// const results = await Promise.allSettled(
|
|
119
|
+
// nodeList.map(
|
|
120
|
+
// async ([nodeId, nodeInfo]) => await getNode(nodeId, nodeInfo['nodeType'])
|
|
121
|
+
// )
|
|
122
|
+
// );
|
|
123
|
+
// const nodes = results.filter((r) => r.status === 'fulfilled');
|
|
124
|
+
// nodes.map((f) => {
|
|
125
|
+
// return f.status;
|
|
126
|
+
// });
|
|
127
|
+
// const failedList = results.filter((r) => r.status === 'rejected');
|
|
128
|
+
// return nodes;
|
|
129
|
+
// }
|
|
130
|
+
|
|
117
131
|
/**
|
|
118
|
-
*
|
|
119
|
-
* dependencies. The export data can be written to a file as is
|
|
120
|
-
* (but it doesn't contain meta data).
|
|
121
|
-
* @param {Object} treeObject tree object
|
|
122
|
-
* @param {Object} exportData export data
|
|
123
|
-
* @param {Object} options options object
|
|
132
|
+
* Export options
|
|
124
133
|
*/
|
|
125
134
|
|
|
126
135
|
|
|
127
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Create export data for a tree with all its nodes and dependencies. The export data can be written to a file as is.
|
|
138
|
+
* @param {string} treeId tree id/name
|
|
139
|
+
* @param {ExportOptions} options export options
|
|
140
|
+
* @returns {Promise<SingleTreeExportTemplate>} a promise that resolves to an object containing the tree and all its nodes and dependencies
|
|
141
|
+
*/
|
|
142
|
+
export async function exportTree(treeId, options = {
|
|
143
|
+
useStringArrays: true,
|
|
144
|
+
deps: true,
|
|
145
|
+
verbose: false
|
|
146
|
+
}) {
|
|
147
|
+
const treeObject = await getTree(treeId);
|
|
148
|
+
const exportData = createSingleTreeExportTemplate();
|
|
128
149
|
const {
|
|
129
150
|
useStringArrays,
|
|
130
151
|
deps,
|
|
@@ -151,7 +172,7 @@ async function exportTree(treeObject, exportData, options) {
|
|
|
151
172
|
const themes = []; // get all the nodes
|
|
152
173
|
|
|
153
174
|
for (const [nodeId, nodeInfo] of Object.entries(treeObject.nodes)) {
|
|
154
|
-
nodePromises.push(getNode(nodeId, nodeInfo['nodeType'])
|
|
175
|
+
nodePromises.push(getNode(nodeId, nodeInfo['nodeType']));
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
if (verbose && nodePromises.length > 0) printMessage(' - Nodes:');
|
|
@@ -216,7 +237,7 @@ async function exportTree(treeObject, exportData, options) {
|
|
|
216
237
|
|
|
217
238
|
if (containerNodes.includes(nodeType)) {
|
|
218
239
|
for (const innerNode of nodeObject.nodes) {
|
|
219
|
-
innerNodePromises.push(getNode(innerNode._id, innerNode.nodeType)
|
|
240
|
+
innerNodePromises.push(getNode(innerNode._id, innerNode.nodeType));
|
|
220
241
|
} // frodo supports themes in platform deployments
|
|
221
242
|
|
|
222
243
|
|
|
@@ -375,15 +396,16 @@ async function exportTree(treeObject, exportData, options) {
|
|
|
375
396
|
});
|
|
376
397
|
});
|
|
377
398
|
}
|
|
399
|
+
|
|
400
|
+
return exportData;
|
|
378
401
|
}
|
|
379
402
|
/**
|
|
380
403
|
* Export journey by id/name to file
|
|
381
|
-
* @param {
|
|
382
|
-
* @param {
|
|
383
|
-
* @param {
|
|
404
|
+
* @param {string} journeyId journey id/name
|
|
405
|
+
* @param {string} file optional export file name
|
|
406
|
+
* @param {ExportOptions} options export options
|
|
384
407
|
*/
|
|
385
408
|
|
|
386
|
-
|
|
387
409
|
export async function exportJourneyToFile(journeyId, file, options) {
|
|
388
410
|
const {
|
|
389
411
|
verbose
|
|
@@ -395,26 +417,21 @@ export async function exportJourneyToFile(journeyId, file, options) {
|
|
|
395
417
|
}
|
|
396
418
|
|
|
397
419
|
if (!verbose) createProgressIndicator(undefined, `${journeyId}`, 'indeterminate');
|
|
398
|
-
await getTree(journeyId).then(async response => {
|
|
399
|
-
const treeData = response.data;
|
|
400
|
-
const fileData = getSingleTreeFileDataTemplate();
|
|
401
420
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}).catch(err => {
|
|
412
|
-
stopProgressIndicator(err.message, 'fail');
|
|
413
|
-
});
|
|
421
|
+
try {
|
|
422
|
+
const fileData = await exportTree(journeyId, options);
|
|
423
|
+
if (verbose) createProgressIndicator(undefined, `${journeyId}`, 'indeterminate');
|
|
424
|
+
saveJsonToFile(fileData, fileName);
|
|
425
|
+
stopProgressIndicator(`Exported ${journeyId['brightCyan']} to ${fileName['brightCyan']}.`, 'success');
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (verbose) createProgressIndicator(undefined, `${journeyId}`, 'indeterminate');
|
|
428
|
+
stopProgressIndicator(`Error exporting journey ${journeyId}: ${error}`, 'fail');
|
|
429
|
+
}
|
|
414
430
|
}
|
|
415
431
|
/**
|
|
416
432
|
* Export all journeys to file
|
|
417
|
-
* @param {
|
|
433
|
+
* @param {string} file optional export file name
|
|
434
|
+
* @param {ExportOptions} options export options
|
|
418
435
|
*/
|
|
419
436
|
|
|
420
437
|
export async function exportJourneysToFile(file, options) {
|
|
@@ -424,20 +441,16 @@ export async function exportJourneysToFile(file, options) {
|
|
|
424
441
|
fileName = getTypedFilename(`all${getRealmString()}Journeys`, 'journeys');
|
|
425
442
|
}
|
|
426
443
|
|
|
427
|
-
const trees =
|
|
428
|
-
const fileData =
|
|
444
|
+
const trees = await getTrees();
|
|
445
|
+
const fileData = createMultipleTreesExportTemplate();
|
|
429
446
|
createProgressIndicator(trees.length, 'Exporting journeys...');
|
|
430
447
|
|
|
431
448
|
for (const tree of trees) {
|
|
432
449
|
updateProgressIndicator(`${tree._id}`);
|
|
433
450
|
|
|
434
451
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const exportData = getSingleTreeFileDataTemplate();
|
|
438
|
-
delete exportData.meta; // eslint-disable-next-line no-await-in-loop
|
|
439
|
-
|
|
440
|
-
await exportTree(treeData, exportData, options);
|
|
452
|
+
const exportData = await exportTree(tree._id, options);
|
|
453
|
+
delete exportData.meta;
|
|
441
454
|
fileData.trees[tree._id] = exportData;
|
|
442
455
|
} catch (error) {
|
|
443
456
|
printMessage(`Error exporting journey ${tree._id}: ${error}`, 'error');
|
|
@@ -449,59 +462,39 @@ export async function exportJourneysToFile(file, options) {
|
|
|
449
462
|
}
|
|
450
463
|
/**
|
|
451
464
|
* Export all journeys to separate files
|
|
465
|
+
* @param {ExportOptions} options export options
|
|
452
466
|
*/
|
|
453
467
|
|
|
454
468
|
export async function exportJourneysToFiles(options) {
|
|
455
|
-
const trees =
|
|
469
|
+
const trees = await getTrees();
|
|
456
470
|
createProgressIndicator(trees.length, 'Exporting journeys...');
|
|
457
471
|
|
|
458
472
|
for (const tree of trees) {
|
|
459
473
|
updateProgressIndicator(`${tree._id}`);
|
|
460
|
-
const fileName = getTypedFilename(`${tree._id}`, 'journey');
|
|
474
|
+
const fileName = getTypedFilename(`${tree._id}`, 'journey');
|
|
461
475
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
try {
|
|
477
|
+
const exportData = await exportTree(tree._id, options);
|
|
478
|
+
saveJsonToFile(exportData, fileName);
|
|
479
|
+
} catch (error) {// do we need to report status here?
|
|
480
|
+
}
|
|
467
481
|
}
|
|
468
482
|
|
|
469
483
|
stopProgressIndicator('Done');
|
|
470
484
|
}
|
|
471
485
|
/**
|
|
472
|
-
*
|
|
473
|
-
* @param {String} journeyId journey id/name
|
|
474
|
-
* @returns {Object} object containing all journey data
|
|
486
|
+
* Import options
|
|
475
487
|
*/
|
|
476
488
|
|
|
477
|
-
export async function getJourneyData(journeyId) {
|
|
478
|
-
createProgressIndicator(undefined, `${journeyId}`, 'indeterminate');
|
|
479
|
-
const journeyData = getSingleTreeFileDataTemplate();
|
|
480
|
-
const treeData = (await getTree(journeyId).catch(err => {
|
|
481
|
-
stopProgressIndicator(null, 'success');
|
|
482
|
-
printMessage(err, 'error');
|
|
483
|
-
}))['data'];
|
|
484
|
-
updateProgressIndicator();
|
|
485
|
-
await exportTree(treeData, journeyData, {
|
|
486
|
-
useStringArrays: true
|
|
487
|
-
});
|
|
488
|
-
stopProgressIndicator(null, 'success');
|
|
489
|
-
return journeyData;
|
|
490
|
-
}
|
|
491
489
|
/**
|
|
492
490
|
* Helper to import a tree with all dependencies from an import data object (typically read from a file)
|
|
493
|
-
* @param {
|
|
494
|
-
* @param {
|
|
491
|
+
* @param {SingleTreeExportTemplate} treeObject tree object containing tree and all its dependencies
|
|
492
|
+
* @param {ImportOptions} options import options
|
|
495
493
|
*/
|
|
496
|
-
|
|
497
|
-
async function importTree(treeObject, options) {
|
|
498
|
-
const {
|
|
499
|
-
reUuid
|
|
500
|
-
} = options;
|
|
501
|
-
const {
|
|
502
|
-
deps
|
|
503
|
-
} = options;
|
|
494
|
+
export async function importTree(treeObject, options) {
|
|
504
495
|
const {
|
|
496
|
+
reUuid,
|
|
497
|
+
deps,
|
|
505
498
|
verbose
|
|
506
499
|
} = options;
|
|
507
500
|
if (verbose) printMessage(`\n- ${treeObject.tree._id}\n`, 'info', false);
|
|
@@ -555,8 +548,8 @@ async function importTree(treeObject, options) {
|
|
|
555
548
|
const themes = {};
|
|
556
549
|
|
|
557
550
|
for (const theme of treeObject.themes) {
|
|
558
|
-
if (verbose) printMessage(` - ${theme
|
|
559
|
-
themes[theme
|
|
551
|
+
if (verbose) printMessage(` - ${theme['_id']} (${theme['name']})`, 'info');
|
|
552
|
+
themes[theme['_id']] = theme;
|
|
560
553
|
}
|
|
561
554
|
|
|
562
555
|
try {
|
|
@@ -775,7 +768,7 @@ async function importTree(treeObject, options) {
|
|
|
775
768
|
// Set the identityResource for the tree to the selected resource.
|
|
776
769
|
|
|
777
770
|
|
|
778
|
-
if (treeObject.tree.identityResource && treeObject.tree
|
|
771
|
+
if (treeObject.tree.identityResource && treeObject.tree['identityResource'].endsWith('user')) {
|
|
779
772
|
treeObject.tree.identityResource = `managed/${getRealmManagedUser()}`;
|
|
780
773
|
if (verbose) printMessage(` - identityResource: ${treeObject.tree.identityResource}`, 'info', false);
|
|
781
774
|
}
|
|
@@ -820,7 +813,6 @@ async function importTree(treeObject, options) {
|
|
|
820
813
|
* @param {int} index Depth of recursion
|
|
821
814
|
*/
|
|
822
815
|
|
|
823
|
-
|
|
824
816
|
async function resolveDependencies(installedJorneys, journeyMap, unresolvedJourneys, resolvedJourneys, index = -1) {
|
|
825
817
|
let before = -1;
|
|
826
818
|
let after = index;
|
|
@@ -877,9 +869,9 @@ async function resolveDependencies(installedJorneys, journeyMap, unresolvedJourn
|
|
|
877
869
|
}
|
|
878
870
|
/**
|
|
879
871
|
* Import a journey from file
|
|
880
|
-
* @param {
|
|
881
|
-
* @param {
|
|
882
|
-
* @param {
|
|
872
|
+
* @param {string} journeyId journey id/name
|
|
873
|
+
* @param {string} file import file name
|
|
874
|
+
* @param {ImportOptions} options import options
|
|
883
875
|
*/
|
|
884
876
|
|
|
885
877
|
|
|
@@ -900,7 +892,7 @@ export async function importJourneyFromFile(journeyId, file, options) {
|
|
|
900
892
|
|
|
901
893
|
if (journeyData && journeyId === journeyData.tree._id) {
|
|
902
894
|
// attempt dependency resolution for single tree import
|
|
903
|
-
const installedJourneys = (await getTrees()).
|
|
895
|
+
const installedJourneys = (await getTrees()).map(x => x._id);
|
|
904
896
|
const unresolvedJourneys = {};
|
|
905
897
|
const resolvedJourneys = [];
|
|
906
898
|
createProgressIndicator(undefined, 'Resolving dependencies', 'indeterminate');
|
|
@@ -934,8 +926,8 @@ export async function importJourneyFromFile(journeyId, file, options) {
|
|
|
934
926
|
}
|
|
935
927
|
/**
|
|
936
928
|
* Import first journey from file
|
|
937
|
-
* @param {
|
|
938
|
-
* @param {
|
|
929
|
+
* @param {string} file import file name
|
|
930
|
+
* @param {ImportOptions} options import options
|
|
939
931
|
*/
|
|
940
932
|
|
|
941
933
|
export async function importFirstJourneyFromFile(file, options) {
|
|
@@ -965,7 +957,7 @@ export async function importFirstJourneyFromFile(file, options) {
|
|
|
965
957
|
|
|
966
958
|
if (journeyData && journeyId) {
|
|
967
959
|
// attempt dependency resolution for single tree import
|
|
968
|
-
const installedJourneys = (await getTrees()).
|
|
960
|
+
const installedJourneys = (await getTrees()).map(x => x._id);
|
|
969
961
|
const unresolvedJourneys = {};
|
|
970
962
|
const resolvedJourneys = [];
|
|
971
963
|
createProgressIndicator(undefined, 'Resolving dependencies', 'indeterminate');
|
|
@@ -1000,11 +992,11 @@ export async function importFirstJourneyFromFile(file, options) {
|
|
|
1000
992
|
/**
|
|
1001
993
|
* Helper to import multiple trees from a tree map
|
|
1002
994
|
* @param {Object} treesMap map of trees object
|
|
1003
|
-
* @param {
|
|
995
|
+
* @param {ImportOptions} options import options
|
|
1004
996
|
*/
|
|
1005
997
|
|
|
1006
998
|
async function importAllTrees(treesMap, options) {
|
|
1007
|
-
const installedJourneys = (await getTrees()).
|
|
999
|
+
const installedJourneys = (await getTrees()).map(x => x._id);
|
|
1008
1000
|
const unresolvedJourneys = {};
|
|
1009
1001
|
const resolvedJourneys = [];
|
|
1010
1002
|
createProgressIndicator(undefined, 'Resolving dependencies', 'indeterminate');
|
|
@@ -1036,8 +1028,8 @@ async function importAllTrees(treesMap, options) {
|
|
|
1036
1028
|
}
|
|
1037
1029
|
/**
|
|
1038
1030
|
* Import all journeys from file
|
|
1039
|
-
* @param {
|
|
1040
|
-
* @param {
|
|
1031
|
+
* @param {string} file import file name
|
|
1032
|
+
* @param {ImportOptions} options import options
|
|
1041
1033
|
*/
|
|
1042
1034
|
|
|
1043
1035
|
|
|
@@ -1050,7 +1042,7 @@ export async function importJourneysFromFile(file, options) {
|
|
|
1050
1042
|
}
|
|
1051
1043
|
/**
|
|
1052
1044
|
* Import all journeys from separate files
|
|
1053
|
-
* @param {
|
|
1045
|
+
* @param {ImportOptions} options import options
|
|
1054
1046
|
*/
|
|
1055
1047
|
|
|
1056
1048
|
export async function importJourneysFromFiles(options) {
|
|
@@ -1107,23 +1099,50 @@ export function describeTree(treeData) {
|
|
|
1107
1099
|
treeMap['nodeTypes'] = nodeTypeMap;
|
|
1108
1100
|
treeMap['scripts'] = scriptsMap;
|
|
1109
1101
|
treeMap['emailTemplates'] = emailTemplatesMap;
|
|
1102
|
+
printMessage(`\nJourney: ${treeMap['treeName']}`, 'data');
|
|
1103
|
+
printMessage('========');
|
|
1104
|
+
printMessage('\nNodes:', 'data');
|
|
1105
|
+
|
|
1106
|
+
if (Object.entries(treeMap['nodeTypes']).length) {
|
|
1107
|
+
for (const [name, count] of Object.entries(treeMap['nodeTypes'])) {
|
|
1108
|
+
printMessage(`- ${name}: ${count}`, 'data');
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (Object.entries(treeMap['scripts']).length) {
|
|
1113
|
+
printMessage('\nScripts:', 'data');
|
|
1114
|
+
|
|
1115
|
+
for (const [name, desc] of Object.entries(treeMap['scripts'])) {
|
|
1116
|
+
printMessage(`- ${name}: ${desc}`, 'data');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (Object.entries(treeMap['emailTemplates']).length) {
|
|
1121
|
+
printMessage('\nEmail Templates:', 'data');
|
|
1122
|
+
|
|
1123
|
+
for (const [id] of Object.entries(treeMap['emailTemplates'])) {
|
|
1124
|
+
printMessage(`- ${id}`, 'data');
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1110
1128
|
return treeMap;
|
|
1111
1129
|
}
|
|
1112
1130
|
/**
|
|
1113
1131
|
* Find all node configuration objects that are no longer referenced by any tree
|
|
1132
|
+
* @returns {Promise<unknown[]>} a promise that resolves to an array of orphaned nodes
|
|
1114
1133
|
*/
|
|
1115
1134
|
|
|
1116
|
-
async function findOrphanedNodes() {
|
|
1135
|
+
export async function findOrphanedNodes() {
|
|
1117
1136
|
const allNodes = [];
|
|
1118
1137
|
const orphanedNodes = [];
|
|
1119
1138
|
let types = [];
|
|
1120
|
-
const allJourneys =
|
|
1139
|
+
const allJourneys = await getTrees();
|
|
1121
1140
|
let errorMessage = '';
|
|
1122
1141
|
const errorTypes = [];
|
|
1123
1142
|
createProgressIndicator(undefined, `Counting total nodes...`, 'indeterminate');
|
|
1124
1143
|
|
|
1125
1144
|
try {
|
|
1126
|
-
types =
|
|
1145
|
+
types = await getNodeTypes();
|
|
1127
1146
|
} catch (error) {
|
|
1128
1147
|
printMessage('Error retrieving all available node types:', 'error');
|
|
1129
1148
|
printMessage(error.response.data, 'error');
|
|
@@ -1133,7 +1152,7 @@ async function findOrphanedNodes() {
|
|
|
1133
1152
|
for (const type of types) {
|
|
1134
1153
|
try {
|
|
1135
1154
|
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
|
1136
|
-
(await getNodesByType(type._id)).
|
|
1155
|
+
(await getNodesByType(type._id)).forEach(node => {
|
|
1137
1156
|
allNodes.push(node);
|
|
1138
1157
|
updateProgressIndicator(`${allNodes.length} total nodes${errorMessage}`);
|
|
1139
1158
|
});
|
|
@@ -1162,7 +1181,7 @@ async function findOrphanedNodes() {
|
|
|
1162
1181
|
|
|
1163
1182
|
if (containerNodes.includes(node.nodeType)) {
|
|
1164
1183
|
// eslint-disable-next-line no-await-in-loop
|
|
1165
|
-
const containerNode =
|
|
1184
|
+
const containerNode = await getNode(nodeId, node.nodeType);
|
|
1166
1185
|
containerNode.nodes.forEach(n => {
|
|
1167
1186
|
activeNodes.push(n._id);
|
|
1168
1187
|
updateProgressIndicator(`${activeNodes.length} active nodes`);
|
|
@@ -1182,41 +1201,27 @@ async function findOrphanedNodes() {
|
|
|
1182
1201
|
/**
|
|
1183
1202
|
* Remove orphaned nodes
|
|
1184
1203
|
* @param {[Object]} orphanedNodes Pass in an array of orphaned node configuration objects to remove
|
|
1204
|
+
* @returns {Promise<unknown[]>} a promise that resolves to an array nodes that encountered errors deleting
|
|
1185
1205
|
*/
|
|
1186
1206
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1207
|
+
export async function removeOrphanedNodes(orphanedNodes) {
|
|
1208
|
+
const errorNodes = [];
|
|
1189
1209
|
createProgressIndicator(orphanedNodes.length, 'Removing orphaned nodes...');
|
|
1190
1210
|
|
|
1191
1211
|
for (const node of orphanedNodes) {
|
|
1192
|
-
updateProgressIndicator(`Removing ${node
|
|
1212
|
+
updateProgressIndicator(`Removing ${node['_id']}...`);
|
|
1193
1213
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1214
|
+
try {
|
|
1215
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1216
|
+
await deleteNode(node['_id'], node['_type']['_id']);
|
|
1217
|
+
} catch (deleteError) {
|
|
1218
|
+
errorNodes.push(node);
|
|
1219
|
+
printMessage(` ${deleteError}`, 'error');
|
|
1220
|
+
}
|
|
1197
1221
|
}
|
|
1198
1222
|
|
|
1199
1223
|
stopProgressIndicator(`Removed ${orphanedNodes.length} orphaned nodes.`);
|
|
1200
|
-
|
|
1201
|
-
/**
|
|
1202
|
-
* Prune orphaned nodes
|
|
1203
|
-
*/
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
export async function prune() {
|
|
1207
|
-
const orphanedNodes = await findOrphanedNodes();
|
|
1208
|
-
|
|
1209
|
-
if (orphanedNodes.length > 0) {
|
|
1210
|
-
const ok = await yesno({
|
|
1211
|
-
question: 'Prune (permanently delete) orphaned nodes? (y|n):'
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
if (ok) {
|
|
1215
|
-
await removeOrphanedNodes(orphanedNodes);
|
|
1216
|
-
}
|
|
1217
|
-
} else {
|
|
1218
|
-
printMessage('No orphaned nodes found.');
|
|
1219
|
-
}
|
|
1224
|
+
return errorNodes;
|
|
1220
1225
|
}
|
|
1221
1226
|
const OOTB_NODE_TYPES_7 = ['AcceptTermsAndConditionsNode', 'AccountActiveDecisionNode', 'AccountLockoutNode', 'AgentDataStoreDecisionNode', 'AnonymousSessionUpgradeNode', 'AnonymousUserNode', 'AttributeCollectorNode', 'AttributePresentDecisionNode', 'AttributeValueDecisionNode', 'AuthLevelDecisionNode', 'ChoiceCollectorNode', 'ConsentNode', 'CookiePresenceDecisionNode', 'CreateObjectNode', 'CreatePasswordNode', 'DataStoreDecisionNode', 'DeviceGeoFencingNode', 'DeviceLocationMatchNode', 'DeviceMatchNode', 'DeviceProfileCollectorNode', 'DeviceSaveNode', 'DeviceTamperingVerificationNode', 'DisplayUserNameNode', 'EmailSuspendNode', 'EmailTemplateNode', 'IdentifyExistingUserNode', 'IncrementLoginCountNode', 'InnerTreeEvaluatorNode', 'IotAuthenticationNode', 'IotRegistrationNode', 'KbaCreateNode', 'KbaDecisionNode', 'KbaVerifyNode', 'LdapDecisionNode', 'LoginCountDecisionNode', 'MessageNode', 'MetadataNode', 'MeterNode', 'ModifyAuthLevelNode', 'OneTimePasswordCollectorDecisionNode', 'OneTimePasswordGeneratorNode', 'OneTimePasswordSmsSenderNode', 'OneTimePasswordSmtpSenderNode', 'PageNode', 'PasswordCollectorNode', 'PatchObjectNode', 'PersistentCookieDecisionNode', 'PollingWaitNode', 'ProfileCompletenessDecisionNode', 'ProvisionDynamicAccountNode', 'ProvisionIdmAccountNode', 'PushAuthenticationSenderNode', 'PushResultVerifierNode', 'QueryFilterDecisionNode', 'RecoveryCodeCollectorDecisionNode', 'RecoveryCodeDisplayNode', 'RegisterLogoutWebhookNode', 'RemoveSessionPropertiesNode', 'RequiredAttributesDecisionNode', 'RetryLimitDecisionNode', 'ScriptedDecisionNode', 'SelectIdPNode', 'SessionDataNode', 'SetFailureUrlNode', 'SetPersistentCookieNode', 'SetSessionPropertiesNode', 'SetSuccessUrlNode', 'SocialFacebookNode', 'SocialGoogleNode', 'SocialNode', 'SocialOAuthIgnoreProfileNode', 'SocialOpenIdConnectNode', 'SocialProviderHandlerNode', 'TermsAndConditionsDecisionNode', 'TimeSinceDecisionNode', 'TimerStartNode', 'TimerStopNode', 'UsernameCollectorNode', 'ValidatedPasswordNode', 'ValidatedUsernameNode', 'WebAuthnAuthenticationNode', 'WebAuthnDeviceStorageNode', 'WebAuthnRegistrationNode', 'ZeroPageLoginNode', 'product-CertificateCollectorNode', 'product-CertificateUserExtractorNode', 'product-CertificateValidationNode', 'product-KerberosNode', 'product-ReCaptchaNode', 'product-Saml2Node', 'product-WriteFederationInformationNode'];
|
|
1222
1227
|
const OOTB_NODE_TYPES_7_1 = ['PushRegistrationNode', 'GetAuthenticatorAppNode', 'MultiFactorRegistrationOptionsNode', 'OptOutMultiFactorAuthenticationNode'].concat(OOTB_NODE_TYPES_7);
|
|
@@ -1229,10 +1234,9 @@ const OOTB_NODE_TYPES_6 = ['AbstractSocialAuthLoginNode', 'AccountLockoutNode',
|
|
|
1229
1234
|
* @returns {boolean} True if the journey/tree contains any custom nodes, false otherwise.
|
|
1230
1235
|
*/
|
|
1231
1236
|
|
|
1232
|
-
async function isCustom(journey) {
|
|
1237
|
+
export async function isCustom(journey) {
|
|
1233
1238
|
let ootbNodeTypes = [];
|
|
1234
|
-
const nodeList = journey.nodes;
|
|
1235
|
-
// console.log(storage.session.getAmVersion());
|
|
1239
|
+
const nodeList = journey.nodes;
|
|
1236
1240
|
|
|
1237
1241
|
switch (storage.session.getAmVersion()) {
|
|
1238
1242
|
case '7.1.0':
|
|
@@ -1240,7 +1244,6 @@ async function isCustom(journey) {
|
|
|
1240
1244
|
break;
|
|
1241
1245
|
|
|
1242
1246
|
case '7.2.0':
|
|
1243
|
-
// console.log("here");
|
|
1244
1247
|
ootbNodeTypes = OOTB_NODE_TYPES_7_2.slice(0);
|
|
1245
1248
|
break;
|
|
1246
1249
|
|
|
@@ -1286,7 +1289,7 @@ async function isCustom(journey) {
|
|
|
1286
1289
|
|
|
1287
1290
|
if (containerNodes.includes(nodeList[node].nodeType)) {
|
|
1288
1291
|
results.push( // eslint-disable-next-line no-await-in-loop
|
|
1289
|
-
|
|
1292
|
+
await getNode(node, nodeList[node].nodeType));
|
|
1290
1293
|
}
|
|
1291
1294
|
}
|
|
1292
1295
|
}
|
|
@@ -1313,12 +1316,11 @@ async function isCustom(journey) {
|
|
|
1313
1316
|
* @param {boolean} analyze Analyze journeys/trees for custom nodes (expensive)
|
|
1314
1317
|
*/
|
|
1315
1318
|
|
|
1316
|
-
|
|
1317
1319
|
export async function listJourneys(long = false, analyze = false) {
|
|
1318
1320
|
let journeys = [];
|
|
1319
1321
|
|
|
1320
1322
|
try {
|
|
1321
|
-
journeys =
|
|
1323
|
+
journeys = await getTrees();
|
|
1322
1324
|
} catch (error) {
|
|
1323
1325
|
printMessage(`${error.message}`, 'error');
|
|
1324
1326
|
printMessage(error.response.data, 'error');
|
|
@@ -1351,11 +1353,11 @@ export async function listJourneys(long = false, analyze = false) {
|
|
|
1351
1353
|
}
|
|
1352
1354
|
/**
|
|
1353
1355
|
* Delete a journey
|
|
1354
|
-
* @param {
|
|
1356
|
+
* @param {string} journeyId journey id/name
|
|
1355
1357
|
* @param {Object} options deep=true also delete all the nodes and inner nodes, verbose=true print verbose info
|
|
1356
1358
|
*/
|
|
1357
1359
|
|
|
1358
|
-
export async function deleteJourney(journeyId, options,
|
|
1360
|
+
export async function deleteJourney(journeyId, options, progress = true) {
|
|
1359
1361
|
const {
|
|
1360
1362
|
deep
|
|
1361
1363
|
} = options;
|
|
@@ -1365,29 +1367,29 @@ export async function deleteJourney(journeyId, options, spinner = true) {
|
|
|
1365
1367
|
const status = {
|
|
1366
1368
|
nodes: {}
|
|
1367
1369
|
};
|
|
1368
|
-
if (
|
|
1369
|
-
if (
|
|
1370
|
+
if (progress) createProgressIndicator(undefined, `Deleting ${journeyId}...`, 'indeterminate');
|
|
1371
|
+
if (progress && verbose) stopProgressIndicator();
|
|
1370
1372
|
return deleteTree(journeyId).then(async deleteTreeResponse => {
|
|
1371
1373
|
status['status'] = 'success';
|
|
1372
1374
|
const nodePromises = [];
|
|
1373
1375
|
if (verbose) printMessage(`Deleted ${journeyId} (tree)`, 'info');
|
|
1374
1376
|
|
|
1375
1377
|
if (deep) {
|
|
1376
|
-
for (const [nodeId, nodeObject] of Object.entries(deleteTreeResponse.
|
|
1378
|
+
for (const [nodeId, nodeObject] of Object.entries(deleteTreeResponse.nodes)) {
|
|
1377
1379
|
// delete inner nodes (nodes inside container nodes)
|
|
1378
1380
|
if (containerNodes.includes(nodeObject['nodeType'])) {
|
|
1379
1381
|
try {
|
|
1380
1382
|
// eslint-disable-next-line no-await-in-loop
|
|
1381
|
-
const
|
|
1383
|
+
const containerNode = await getNode(nodeId, nodeObject['nodeType']);
|
|
1382
1384
|
if (verbose) printMessage(`Read ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`, 'info');
|
|
1383
1385
|
|
|
1384
|
-
for (const innerNodeObject of
|
|
1386
|
+
for (const innerNodeObject of containerNode.nodes) {
|
|
1385
1387
|
nodePromises.push(deleteNode(innerNodeObject._id, innerNodeObject.nodeType).then(response2 => {
|
|
1386
1388
|
status.nodes[innerNodeObject._id] = {
|
|
1387
1389
|
status: 'success'
|
|
1388
1390
|
};
|
|
1389
1391
|
if (verbose) printMessage(`Deleted ${innerNodeObject._id} (${innerNodeObject.nodeType}) from ${journeyId}`, 'info');
|
|
1390
|
-
return response2
|
|
1392
|
+
return response2;
|
|
1391
1393
|
}).catch(error => {
|
|
1392
1394
|
status.nodes[innerNodeObject._id] = {
|
|
1393
1395
|
status: 'error',
|
|
@@ -1398,18 +1400,27 @@ export async function deleteJourney(journeyId, options, spinner = true) {
|
|
|
1398
1400
|
} // finally delete the container node
|
|
1399
1401
|
|
|
1400
1402
|
|
|
1401
|
-
nodePromises.push(deleteNode(
|
|
1402
|
-
status.nodes[
|
|
1403
|
+
nodePromises.push(deleteNode(containerNode._id, containerNode['_type']['_id']).then(response2 => {
|
|
1404
|
+
status.nodes[containerNode._id] = {
|
|
1403
1405
|
status: 'success'
|
|
1404
1406
|
};
|
|
1405
|
-
if (verbose) printMessage(`Deleted ${
|
|
1406
|
-
return response2
|
|
1407
|
+
if (verbose) printMessage(`Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`, 'info');
|
|
1408
|
+
return response2;
|
|
1407
1409
|
}).catch(error => {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1410
|
+
var _error$response, _error$response$data;
|
|
1411
|
+
|
|
1412
|
+
if ((error === null || error === void 0 ? void 0 : (_error$response = error.response) === null || _error$response === void 0 ? void 0 : (_error$response$data = _error$response.data) === null || _error$response$data === void 0 ? void 0 : _error$response$data.code) === 500 && error.response.data.message === 'Unable to read SMS config: Node did not exist') {
|
|
1413
|
+
status.nodes[containerNode._id] = {
|
|
1414
|
+
status: 'success'
|
|
1415
|
+
};
|
|
1416
|
+
if (verbose) printMessage(`Deleted ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}`, 'info');
|
|
1417
|
+
} else {
|
|
1418
|
+
status.nodes[containerNode._id] = {
|
|
1419
|
+
status: 'error',
|
|
1420
|
+
error
|
|
1421
|
+
};
|
|
1422
|
+
if (verbose) printMessage(`Error deleting container node ${containerNode._id} (${containerNode['_type']['_id']}) from ${journeyId}: ${error.response.data.message}`, 'error');
|
|
1423
|
+
}
|
|
1413
1424
|
}));
|
|
1414
1425
|
} catch (error) {
|
|
1415
1426
|
if (verbose) printMessage(`Error getting container node ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}: ${error}`, 'error');
|
|
@@ -1421,7 +1432,7 @@ export async function deleteJourney(journeyId, options, spinner = true) {
|
|
|
1421
1432
|
status: 'success'
|
|
1422
1433
|
};
|
|
1423
1434
|
if (verbose) printMessage(`Deleted ${nodeId} (${nodeObject['nodeType']}) from ${journeyId}`, 'info');
|
|
1424
|
-
return response
|
|
1435
|
+
return response;
|
|
1425
1436
|
}).catch(error => {
|
|
1426
1437
|
status.nodes[nodeId] = {
|
|
1427
1438
|
status: 'error',
|
|
@@ -1436,7 +1447,7 @@ export async function deleteJourney(journeyId, options, spinner = true) {
|
|
|
1436
1447
|
|
|
1437
1448
|
await Promise.allSettled(nodePromises); // report status
|
|
1438
1449
|
|
|
1439
|
-
if (
|
|
1450
|
+
if (progress) {
|
|
1440
1451
|
let nodeCount = 0;
|
|
1441
1452
|
let errorCount = 0;
|
|
1442
1453
|
|
|
@@ -1471,7 +1482,7 @@ export async function deleteJourneys(options) {
|
|
|
1471
1482
|
verbose
|
|
1472
1483
|
} = options;
|
|
1473
1484
|
const status = {};
|
|
1474
|
-
const trees =
|
|
1485
|
+
const trees = await getTrees();
|
|
1475
1486
|
createProgressIndicator(trees.length, 'Deleting journeys...');
|
|
1476
1487
|
|
|
1477
1488
|
for (const tree of trees) {
|