@sonisoft/now-sdk-ext-core 6.6.3 → 6.8.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/README.md +6 -0
- package/dist/comm/http/FormHtmlParser.d.ts +34 -0
- package/dist/comm/http/FormHtmlParser.js +227 -0
- package/dist/comm/http/FormHtmlParser.js.map +1 -0
- package/dist/comm/http/FormRecordModels.d.ts +120 -0
- package/dist/comm/http/FormRecordModels.js +6 -0
- package/dist/comm/http/FormRecordModels.js.map +1 -0
- package/dist/comm/http/FormRecordWriter.d.ts +99 -0
- package/dist/comm/http/FormRecordWriter.js +385 -0
- package/dist/comm/http/FormRecordWriter.js.map +1 -0
- package/dist/comm/http/HTTPRequest.d.ts +4 -0
- package/dist/comm/http/RequestHandler.d.ts +1 -0
- package/dist/comm/http/RequestHandler.js +34 -22
- package/dist/comm/http/RequestHandler.js.map +1 -1
- package/dist/comm/http/internal/FormTransportModels.d.ts +26 -0
- package/dist/comm/http/internal/FormTransportModels.js +2 -0
- package/dist/comm/http/internal/FormTransportModels.js.map +1 -0
- package/dist/exception/FormSubmitError.d.ts +13 -0
- package/dist/exception/FormSubmitError.js +19 -0
- package/dist/exception/FormSubmitError.js.map +1 -0
- package/dist/exception/WorkflowValidationError.d.ts +24 -0
- package/dist/exception/WorkflowValidationError.js +24 -0
- package/dist/exception/WorkflowValidationError.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/sn/metadata/MetadataFormWriter.d.ts +13 -0
- package/dist/sn/metadata/MetadataFormWriter.js +264 -0
- package/dist/sn/metadata/MetadataFormWriter.js.map +1 -0
- package/dist/sn/metadata/MetadataModels.d.ts +44 -0
- package/dist/sn/metadata/MetadataModels.js +2 -0
- package/dist/sn/metadata/MetadataModels.js.map +1 -0
- package/dist/sn/metadata/MetadataScopeError.d.ts +5 -0
- package/dist/sn/metadata/MetadataScopeError.js +10 -0
- package/dist/sn/metadata/MetadataScopeError.js.map +1 -0
- package/dist/sn/metadata/MetadataVerificationError.d.ts +11 -0
- package/dist/sn/metadata/MetadataVerificationError.js +20 -0
- package/dist/sn/metadata/MetadataVerificationError.js.map +1 -0
- package/dist/sn/metadata/internal/MetadataPreferences.d.ts +12 -0
- package/dist/sn/metadata/internal/MetadataPreferences.js +27 -0
- package/dist/sn/metadata/internal/MetadataPreferences.js.map +1 -0
- package/dist/sn/metadata/internal/MetadataReader.d.ts +9 -0
- package/dist/sn/metadata/internal/MetadataReader.js +55 -0
- package/dist/sn/metadata/internal/MetadataReader.js.map +1 -0
- package/dist/sn/variables/VariableDefinitions.d.ts +23 -0
- package/dist/sn/variables/VariableDefinitions.js +106 -0
- package/dist/sn/variables/VariableDefinitions.js.map +1 -0
- package/dist/sn/variables/VariableModels.d.ts +49 -0
- package/dist/sn/variables/VariableModels.js +6 -0
- package/dist/sn/variables/VariableModels.js.map +1 -0
- package/dist/sn/workflow/WorkflowConversionModels.d.ts +209 -0
- package/dist/sn/workflow/WorkflowConversionModels.js +2 -0
- package/dist/sn/workflow/WorkflowConversionModels.js.map +1 -0
- package/dist/sn/workflow/WorkflowDiagramClient.d.ts +70 -0
- package/dist/sn/workflow/WorkflowDiagramClient.js +151 -0
- package/dist/sn/workflow/WorkflowDiagramClient.js.map +1 -0
- package/dist/sn/workflow/WorkflowFlowConversion.d.ts +19 -0
- package/dist/sn/workflow/WorkflowFlowConversion.js +1013 -0
- package/dist/sn/workflow/WorkflowFlowConversion.js.map +1 -0
- package/dist/sn/workflow/WorkflowFlowHints.d.ts +3 -0
- package/dist/sn/workflow/WorkflowFlowHints.js +36 -0
- package/dist/sn/workflow/WorkflowFlowHints.js.map +1 -0
- package/dist/sn/workflow/WorkflowFluentGenerator.d.ts +17 -0
- package/dist/sn/workflow/WorkflowFluentGenerator.js +348 -0
- package/dist/sn/workflow/WorkflowFluentGenerator.js.map +1 -0
- package/dist/sn/workflow/WorkflowGraphML.d.ts +39 -0
- package/dist/sn/workflow/WorkflowGraphML.js +162 -0
- package/dist/sn/workflow/WorkflowGraphML.js.map +1 -0
- package/dist/sn/workflow/WorkflowManager.d.ts +236 -6
- package/dist/sn/workflow/WorkflowManager.js +1443 -15
- package/dist/sn/workflow/WorkflowManager.js.map +1 -1
- package/dist/sn/workflow/WorkflowModels.d.ts +738 -0
- package/dist/sn/workflow/WorkflowStructure.d.ts +137 -0
- package/dist/sn/workflow/WorkflowStructure.js +368 -0
- package/dist/sn/workflow/WorkflowStructure.js.map +1 -0
- package/dist/sn/workflow/WorkflowViews.d.ts +104 -0
- package/dist/sn/workflow/WorkflowViews.js +408 -0
- package/dist/sn/workflow/WorkflowViews.js.map +1 -0
- package/package.json +2 -1
|
@@ -1,11 +1,39 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { Logger } from "../../util/Logger.js";
|
|
2
3
|
import { ServiceNowRequest } from "../../comm/http/ServiceNowRequest.js";
|
|
4
|
+
import { SessionManager } from "../../comm/http/SessionManager.js";
|
|
3
5
|
import { TableAPIRequest } from "../../comm/http/TableAPIRequest.js";
|
|
6
|
+
import { FormRecordWriter } from "../../comm/http/FormRecordWriter.js";
|
|
7
|
+
import { InvalidParameterException } from "../../exception/InvalidParameterException.js";
|
|
8
|
+
import { WorkflowValidationError } from "../../exception/WorkflowValidationError.js";
|
|
9
|
+
import { READ_ONLY } from "../../policy/PolicyTypes.js";
|
|
10
|
+
import { VariableDefinitions } from "../variables/VariableDefinitions.js";
|
|
11
|
+
import { WorkflowDiagramClient } from "./WorkflowDiagramClient.js";
|
|
12
|
+
import { planFlowConversion } from "./WorkflowFlowConversion.js";
|
|
13
|
+
import { generateFluentFlow } from "./WorkflowFluentGenerator.js";
|
|
14
|
+
const SYS_ID = /^[0-9a-f]{32}$/i;
|
|
15
|
+
const VERSION_FIELDS = [
|
|
16
|
+
'sys_id', 'workflow', 'name', 'table', 'description', 'published', 'active', 'checked_out', 'checked_out_by',
|
|
17
|
+
'checked_out_by.name', 'condition', 'condition_type', 'start', 'full_sequences', 'sys_created_on', 'sys_updated_on'
|
|
18
|
+
].join(',');
|
|
19
|
+
const CONDITION_FIELDS = [
|
|
20
|
+
'sys_id', 'activity', 'name', 'condition', 'order', 'else_flag', 'error', 'event', 'event_name',
|
|
21
|
+
'short_description', 'skip_during_generate'
|
|
22
|
+
].join(',');
|
|
4
23
|
/**
|
|
5
|
-
* Manages ServiceNow
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
24
|
+
* Manages legacy ServiceNow workflows (the Workflow Editor's wf_* records).
|
|
25
|
+
*
|
|
26
|
+
* Two families of methods live here:
|
|
27
|
+
*
|
|
28
|
+
* - **Editor-parity methods** (`newWorkflow`, `checkout`, `addActivity`, `publish`, …)
|
|
29
|
+
* drive the same server endpoints the Workflow Editor uses — its diagram processor for
|
|
30
|
+
* structure and lifecycle, and the classic `wf_activity` form for activity
|
|
31
|
+
* configuration — so checkout's deep copy, publish's validation and version swap, and
|
|
32
|
+
* activity variables (`sys_variable_value`) all behave exactly as they do in the UI.
|
|
33
|
+
* Editing methods require a draft checked out to the current user.
|
|
34
|
+
* - **Record methods** (`createWorkflow`, `createActivity`, …, `createCompleteWorkflow`)
|
|
35
|
+
* insert wf_* records directly through the Table API. They predate the editor-parity
|
|
36
|
+
* methods and are kept for compatibility.
|
|
9
37
|
*/
|
|
10
38
|
export class WorkflowManager {
|
|
11
39
|
static WF_WORKFLOW = 'wf_workflow';
|
|
@@ -13,14 +41,23 @@ export class WorkflowManager {
|
|
|
13
41
|
static WF_ACTIVITY = 'wf_activity';
|
|
14
42
|
static WF_TRANSITION = 'wf_transition';
|
|
15
43
|
static WF_CONDITION = 'wf_condition';
|
|
44
|
+
/** The form view the Workflow Editor uses for activity dialogs. */
|
|
45
|
+
static ACTIVITY_VIEW = 'diagrammer';
|
|
16
46
|
_logger = new Logger("WorkflowManager");
|
|
17
47
|
_req;
|
|
18
48
|
_tableAPI;
|
|
19
49
|
_instance;
|
|
50
|
+
_diagram;
|
|
51
|
+
_forms;
|
|
52
|
+
_variables;
|
|
53
|
+
_currentUser;
|
|
20
54
|
constructor(instance) {
|
|
21
55
|
this._instance = instance;
|
|
22
56
|
this._req = new ServiceNowRequest(instance);
|
|
23
57
|
this._tableAPI = new TableAPIRequest(instance);
|
|
58
|
+
this._diagram = new WorkflowDiagramClient(instance);
|
|
59
|
+
this._forms = new FormRecordWriter(instance);
|
|
60
|
+
this._variables = new VariableDefinitions(instance);
|
|
24
61
|
}
|
|
25
62
|
/**
|
|
26
63
|
* Create a new workflow record.
|
|
@@ -121,6 +158,9 @@ export class WorkflowManager {
|
|
|
121
158
|
throw new Error('Workflow version sys_id is required');
|
|
122
159
|
}
|
|
123
160
|
this._logger.info(`Creating activity: ${options.name} for version ${options.workflowVersionSysId}`);
|
|
161
|
+
if (options.variables !== undefined) {
|
|
162
|
+
return await this.createActivityThroughForm(options);
|
|
163
|
+
}
|
|
124
164
|
const body = {
|
|
125
165
|
name: options.name,
|
|
126
166
|
workflow_version: options.workflowVersionSysId
|
|
@@ -235,7 +275,11 @@ export class WorkflowManager {
|
|
|
235
275
|
throw new Error(`Failed to create condition '${options.name}'. Status: ${response?.status ?? 'unknown'}`);
|
|
236
276
|
}
|
|
237
277
|
/**
|
|
238
|
-
*
|
|
278
|
+
* Set a workflow version's start activity and publish it.
|
|
279
|
+
*
|
|
280
|
+
* Publishes through the Workflow Editor's publish (without its validation prompt, as
|
|
281
|
+
* this method never validated), so the version swap, path computation and update-set
|
|
282
|
+
* capture match a publish from the editor. Use {@link publish} for validation.
|
|
239
283
|
*
|
|
240
284
|
* @param options Publish options including version sys_id and start activity sys_id
|
|
241
285
|
* @throws Error if the API call fails
|
|
@@ -248,16 +292,26 @@ export class WorkflowManager {
|
|
|
248
292
|
throw new Error('Start activity sys_id is required');
|
|
249
293
|
}
|
|
250
294
|
this._logger.info(`Publishing workflow version ${options.versionSysId} with start activity ${options.startActivitySysId}`);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
};
|
|
255
|
-
const response = await this._tableAPI.put(WorkflowManager.WF_WORKFLOW_VERSION, options.versionSysId, body);
|
|
256
|
-
if (response && response.status === 200 && response.bodyObject?.result) {
|
|
257
|
-
this._logger.info(`Successfully published workflow version ${options.versionSysId}`);
|
|
258
|
-
return;
|
|
295
|
+
const response = await this._tableAPI.put(WorkflowManager.WF_WORKFLOW_VERSION, options.versionSysId, { start: options.startActivitySysId });
|
|
296
|
+
if (!(response && response.status === 200 && response.bodyObject?.result)) {
|
|
297
|
+
throw new Error(`Failed to publish workflow version ${options.versionSysId}. Status: ${response?.status ?? 'unknown'}`);
|
|
259
298
|
}
|
|
260
|
-
|
|
299
|
+
let answered = false;
|
|
300
|
+
try {
|
|
301
|
+
answered = !!(await this._diagram.publish(options.versionSysId, false)).graph?.published;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
// The processor publishes, then fails to draw a diagram holding an activity with no
|
|
305
|
+
// type (which these record methods can create) and answers with an empty page.
|
|
306
|
+
this._logger.debug(`publish_novalidate gave no diagram for ${options.versionSysId}: ${error.message}`);
|
|
307
|
+
}
|
|
308
|
+
if (!answered) {
|
|
309
|
+
const version = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${options.versionSysId}`, 'published', 1);
|
|
310
|
+
if (str(version[0]?.published) !== 'true') {
|
|
311
|
+
throw new Error(`Failed to publish workflow version ${options.versionSysId}: the instance did not publish it`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
this._logger.info(`Successfully published workflow version ${options.versionSysId}`);
|
|
261
315
|
}
|
|
262
316
|
/**
|
|
263
317
|
* Create a complete workflow from a single specification.
|
|
@@ -318,7 +372,8 @@ export class WorkflowManager {
|
|
|
318
372
|
width: activitySpec.width,
|
|
319
373
|
height: activitySpec.height,
|
|
320
374
|
script: activitySpec.script,
|
|
321
|
-
vars: activitySpec.vars
|
|
375
|
+
vars: activitySpec.vars,
|
|
376
|
+
variables: activitySpec.variables
|
|
322
377
|
});
|
|
323
378
|
// Store by id if provided, and always by index
|
|
324
379
|
if (activitySpec.id) {
|
|
@@ -384,5 +439,1378 @@ export class WorkflowManager {
|
|
|
384
439
|
startActivity: startActivityKey
|
|
385
440
|
};
|
|
386
441
|
}
|
|
442
|
+
// ============================================================
|
|
443
|
+
// Reading workflows
|
|
444
|
+
// ============================================================
|
|
445
|
+
/**
|
|
446
|
+
* Find workflows, with the state of their published and checked-out versions.
|
|
447
|
+
*/
|
|
448
|
+
async findWorkflows(options = {}) {
|
|
449
|
+
const parts = [];
|
|
450
|
+
if (options.name)
|
|
451
|
+
parts.push(`nameLIKE${queryValue(options.name, 'name')}`);
|
|
452
|
+
if (options.table)
|
|
453
|
+
parts.push(`table=${queryValue(options.table, 'table')}`);
|
|
454
|
+
if (options.query)
|
|
455
|
+
parts.push(options.query);
|
|
456
|
+
parts.push('ORDERBYname');
|
|
457
|
+
const limit = options.limit ?? 50;
|
|
458
|
+
const workflows = await this.read(WorkflowManager.WF_WORKFLOW, parts.join('^'), 'sys_id,name,table,description', limit);
|
|
459
|
+
return await this.summarize(workflows);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Resolve a workflow by name, wf_workflow sys_id, or the sys_id of any of its versions.
|
|
463
|
+
*
|
|
464
|
+
* @throws InvalidParameterException when nothing or more than one workflow matches
|
|
465
|
+
*/
|
|
466
|
+
async resolveWorkflow(nameOrSysId) {
|
|
467
|
+
const ref = (nameOrSysId ?? '').trim();
|
|
468
|
+
if (!ref)
|
|
469
|
+
throw new InvalidParameterException('A workflow name or sys_id is required');
|
|
470
|
+
let rows = [];
|
|
471
|
+
if (SYS_ID.test(ref)) {
|
|
472
|
+
rows = await this.read(WorkflowManager.WF_WORKFLOW, `sys_id=${ref}`, 'sys_id,name,table,description', 1);
|
|
473
|
+
if (!rows.length) {
|
|
474
|
+
const version = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${ref}`, 'workflow', 1);
|
|
475
|
+
if (version.length) {
|
|
476
|
+
rows = await this.read(WorkflowManager.WF_WORKFLOW, `sys_id=${str(version[0].workflow)}`, 'sys_id,name,table,description', 1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!rows.length) {
|
|
481
|
+
rows = await this.read(WorkflowManager.WF_WORKFLOW, `name=${queryValue(ref, 'workflow name')}`, 'sys_id,name,table,description', 10);
|
|
482
|
+
}
|
|
483
|
+
if (!rows.length)
|
|
484
|
+
throw new InvalidParameterException(`No workflow found for '${ref}'`);
|
|
485
|
+
if (rows.length > 1) {
|
|
486
|
+
throw new InvalidParameterException(`'${ref}' matches ${rows.length} workflows; use a sys_id: ${rows.map(r => `${str(r.sys_id)} (${str(r.table)})`).join(', ')}`);
|
|
487
|
+
}
|
|
488
|
+
return (await this.summarize(rows))[0];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* List a workflow's versions, newest first.
|
|
492
|
+
*/
|
|
493
|
+
async getWorkflowVersions(workflowSysId) {
|
|
494
|
+
this.requireSysId(workflowSysId, 'workflowSysId');
|
|
495
|
+
const rows = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `workflow=${workflowSysId}^ORDERBYDESCsys_created_on`, VERSION_FIELDS, 1000);
|
|
496
|
+
return rows.map(toVersionSummary);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Read everything that makes up a workflow version: activities with their variables
|
|
500
|
+
* and exits, transitions and stages.
|
|
501
|
+
*/
|
|
502
|
+
async getWorkflowDefinition(versionSysId, options = {}) {
|
|
503
|
+
this.requireSysId(versionSysId, 'versionSysId');
|
|
504
|
+
const version = await this.readVersion(versionSysId);
|
|
505
|
+
const [activityRows, conditionRows, transitionRows, stageRows] = await Promise.all([
|
|
506
|
+
this.read(WorkflowManager.WF_ACTIVITY, `workflow_version=${versionSysId}^ORDERBYsys_created_on`, 'sys_id,name,activity_definition,activity_definition.name,x,y,width,height,stage,parent,input', 10000),
|
|
507
|
+
this.read(WorkflowManager.WF_CONDITION, `activity.workflow_version=${versionSysId}^ORDERBYorder`, CONDITION_FIELDS, 10000),
|
|
508
|
+
this.read(WorkflowManager.WF_TRANSITION, `from.workflow_version=${versionSysId}`, 'sys_id,from,to,condition', 10000),
|
|
509
|
+
this.read('wf_stage', `workflow_version=${versionSysId}^ORDERBYorder`, 'sys_id,name,value,order', 1000),
|
|
510
|
+
]);
|
|
511
|
+
const conditions = conditionRows.map(toCondition);
|
|
512
|
+
const activities = activityRows.map(row => ({
|
|
513
|
+
sysId: str(row.sys_id),
|
|
514
|
+
name: str(row.name),
|
|
515
|
+
definitionSysId: str(row.activity_definition),
|
|
516
|
+
definitionName: str(row['activity_definition.name']),
|
|
517
|
+
x: optNum(row.x),
|
|
518
|
+
y: optNum(row.y),
|
|
519
|
+
width: optNum(row.width),
|
|
520
|
+
height: optNum(row.height),
|
|
521
|
+
stage: str(row.stage) || undefined,
|
|
522
|
+
parent: str(row.parent) || undefined,
|
|
523
|
+
input: str(row.input),
|
|
524
|
+
conditions: conditions.filter(c => c.activitySysId === str(row.sys_id)),
|
|
525
|
+
}));
|
|
526
|
+
if (options.includeVariables !== false && activities.length) {
|
|
527
|
+
const values = await this.readVariableValues(activities.map(a => a.sysId));
|
|
528
|
+
for (const activity of activities)
|
|
529
|
+
activity.variables = values.get(activity.sysId) ?? {};
|
|
530
|
+
}
|
|
531
|
+
const definition = {
|
|
532
|
+
workflowSysId: str(version.workflow),
|
|
533
|
+
versionSysId,
|
|
534
|
+
name: str(version.name),
|
|
535
|
+
table: str(version.table),
|
|
536
|
+
description: str(version.description),
|
|
537
|
+
condition: str(version.condition),
|
|
538
|
+
conditionType: str(version.condition_type),
|
|
539
|
+
published: str(version.published) === 'true',
|
|
540
|
+
active: str(version.active) === 'true',
|
|
541
|
+
checkedOut: str(version.checked_out),
|
|
542
|
+
checkedOutBy: str(version.checked_out_by),
|
|
543
|
+
start: str(version.start),
|
|
544
|
+
fullSequences: str(version.full_sequences).split(',').filter(Boolean),
|
|
545
|
+
activities,
|
|
546
|
+
transitions: transitionRows.map((row) => ({
|
|
547
|
+
sysId: str(row.sys_id), from: str(row.from), to: str(row.to), condition: str(row.condition),
|
|
548
|
+
})),
|
|
549
|
+
stages: stageRows.map((row) => ({
|
|
550
|
+
sysId: str(row.sys_id), name: str(row.name), value: str(row.value), order: num(row.order),
|
|
551
|
+
})),
|
|
552
|
+
};
|
|
553
|
+
if (options.includeStatus !== false) {
|
|
554
|
+
const graph = await this._diagram.get(versionSysId);
|
|
555
|
+
definition.status = {
|
|
556
|
+
readOnly: graph.readOnly,
|
|
557
|
+
canCheckout: graph.canCheckout,
|
|
558
|
+
canForceCheckout: graph.canForceCheckout,
|
|
559
|
+
canPublish: graph.canPublish,
|
|
560
|
+
statusDisplay: graph.statusDisplay,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return definition;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Read a version's diagram as the Workflow Editor sees it.
|
|
567
|
+
*/
|
|
568
|
+
async getWorkflowGraph(versionSysId) {
|
|
569
|
+
this.requireSysId(versionSysId, 'versionSysId');
|
|
570
|
+
return await this._diagram.get(versionSysId);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* List activity types — core activities and, unless excluded, Activity Designer ones.
|
|
574
|
+
*/
|
|
575
|
+
async listActivityDefinitions(options = {}) {
|
|
576
|
+
const parts = [];
|
|
577
|
+
if (options.name)
|
|
578
|
+
parts.push(`nameLIKE${queryValue(options.name, 'name')}`);
|
|
579
|
+
if (options.category)
|
|
580
|
+
parts.push(`category=${queryValue(options.category, 'category')}`);
|
|
581
|
+
if (options.includeDesigner === false)
|
|
582
|
+
parts.push('sys_class_name=wf_activity_definition');
|
|
583
|
+
parts.push('ORDERBYname');
|
|
584
|
+
const rows = await this.read('wf_element_definition', parts.join('^'), 'sys_id,name,category,sys_class_name', options.limit ?? 500);
|
|
585
|
+
return rows.map(toDefinitionSummary);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Describe an activity type as the instance defines it: its own description, the
|
|
589
|
+
* variables its activities carry (types, defaults, choices, hints) or — for Activity
|
|
590
|
+
* Designer types — its inputs and outputs, and the exits a new activity starts with.
|
|
591
|
+
* With `includeScript`, also its implementation, which shows how each variable is used
|
|
592
|
+
* and which results drive the exits.
|
|
593
|
+
*
|
|
594
|
+
* @param nameOrSysId Definition sys_id or exact name (e.g. "Timer")
|
|
595
|
+
*/
|
|
596
|
+
async getActivityDefinition(nameOrSysId, options = {}) {
|
|
597
|
+
const summary = await this.resolveActivityDefinition(nameOrSysId);
|
|
598
|
+
const designer = summary.sysClassName === 'wf_element_activity';
|
|
599
|
+
const ownFields = designer
|
|
600
|
+
? `input_meta,output_meta${options.includeScript ? ',output_process_script' : ''}`
|
|
601
|
+
: (options.includeScript ? 'script' : '');
|
|
602
|
+
const [variables, defaults, base, own] = await Promise.all([
|
|
603
|
+
designer ? Promise.resolve([]) : this._variables.list(`var__m_${summary.sysId}`),
|
|
604
|
+
this.read('wf_condition_default', `activity_definition=${summary.sysId}^ORDERBYorder`, 'name,condition,order,else_flag,error,skip_during_generate', 200),
|
|
605
|
+
this.read('wf_element_definition', `sys_id=${summary.sysId}`, 'description,attributes', 1),
|
|
606
|
+
ownFields ? this.read(summary.sysClassName, `sys_id=${summary.sysId}`, ownFields, 1) : Promise.resolve([]),
|
|
607
|
+
]);
|
|
608
|
+
const detail = {
|
|
609
|
+
...summary,
|
|
610
|
+
description: str(base[0]?.description),
|
|
611
|
+
attributes: str(base[0]?.attributes),
|
|
612
|
+
variables,
|
|
613
|
+
defaultConditions: defaults.map((row) => ({
|
|
614
|
+
name: str(row.name),
|
|
615
|
+
condition: str(row.condition),
|
|
616
|
+
order: num(row.order),
|
|
617
|
+
elseFlag: str(row.else_flag) === 'true',
|
|
618
|
+
error: str(row.error) === 'true',
|
|
619
|
+
skipDuringGenerate: str(row.skip_during_generate) === 'true',
|
|
620
|
+
})),
|
|
621
|
+
};
|
|
622
|
+
const row = own[0];
|
|
623
|
+
if (designer && row) {
|
|
624
|
+
detail.inputs = designerFields(str(row.input_meta));
|
|
625
|
+
detail.outputs = designerFields(str(row.output_meta));
|
|
626
|
+
if (options.includeScript)
|
|
627
|
+
detail.script = str(row.output_process_script);
|
|
628
|
+
}
|
|
629
|
+
else if (row && options.includeScript) {
|
|
630
|
+
detail.script = str(row.script);
|
|
631
|
+
}
|
|
632
|
+
return detail;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Learn how an activity type is configured on this instance: samples activities of the
|
|
636
|
+
* type in published workflows and reports, per variable (or Activity Designer input),
|
|
637
|
+
* how often it is set and its most common values. Use it to follow the instance's own
|
|
638
|
+
* conventions — which fields approvals reference, how task values are templated, … .
|
|
639
|
+
*
|
|
640
|
+
* @param nameOrSysId Definition sys_id or exact name
|
|
641
|
+
*/
|
|
642
|
+
async getActivityUsage(nameOrSysId, options = {}) {
|
|
643
|
+
const sampleSize = Math.min(Math.max(options.sampleSize ?? 100, 1), 500);
|
|
644
|
+
const perField = options.examplesPerField ?? 5;
|
|
645
|
+
const maxLength = options.maxValueLength ?? 300;
|
|
646
|
+
const definition = await this.getActivityDefinition(nameOrSysId);
|
|
647
|
+
const designer = definition.sysClassName === 'wf_element_activity';
|
|
648
|
+
const query = `activity_definition=${definition.sysId}^workflow_version.published=true`;
|
|
649
|
+
const activities = await this.read(WorkflowManager.WF_ACTIVITY, `${query}^ORDERBYDESCsys_updated_on`, designer ? 'sys_id,input' : 'sys_id', sampleSize);
|
|
650
|
+
const total = await this.count(WorkflowManager.WF_ACTIVITY, query);
|
|
651
|
+
const defs = new Map(definition.variables.map(v => [v.element, v]));
|
|
652
|
+
const tallies = new Map();
|
|
653
|
+
const add = (field, value) => {
|
|
654
|
+
if (value === '')
|
|
655
|
+
return;
|
|
656
|
+
const kept = value.length > maxLength ? `${value.slice(0, maxLength)}…` : value;
|
|
657
|
+
const counts = tallies.get(field) ?? new Map();
|
|
658
|
+
const entry = counts.get(kept) ?? { count: 0, isDefault: isDefaultValue(defs.get(field), value) };
|
|
659
|
+
entry.count++;
|
|
660
|
+
counts.set(kept, entry);
|
|
661
|
+
tallies.set(field, counts);
|
|
662
|
+
};
|
|
663
|
+
let order;
|
|
664
|
+
if (designer) {
|
|
665
|
+
order = (definition.inputs ?? []).map(i => i.name);
|
|
666
|
+
for (const activity of activities) {
|
|
667
|
+
let input = {};
|
|
668
|
+
try {
|
|
669
|
+
input = JSON.parse(str(activity.input) || '{}');
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
for (const [name, value] of Object.entries(input)) {
|
|
675
|
+
if (!order.includes(name))
|
|
676
|
+
order.push(name);
|
|
677
|
+
add(name, typeof value === 'string' ? value : JSON.stringify(value));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
order = definition.variables.map(v => v.element);
|
|
683
|
+
const values = await this.readVariableValues(activities.map(a => str(a.sys_id)));
|
|
684
|
+
for (const byElement of values.values()) {
|
|
685
|
+
for (const [element, value] of Object.entries(byElement))
|
|
686
|
+
add(element, value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
definitionSysId: definition.sysId,
|
|
691
|
+
definitionName: definition.name,
|
|
692
|
+
publishedActivities: total,
|
|
693
|
+
sampled: activities.length,
|
|
694
|
+
fields: order.map(name => {
|
|
695
|
+
const counts = [...(tallies.get(name)?.entries() ?? [])].sort((a, b) => b[1].count - a[1].count);
|
|
696
|
+
return {
|
|
697
|
+
name,
|
|
698
|
+
setCount: counts.reduce((sum, [, e]) => sum + e.count, 0),
|
|
699
|
+
examples: counts.slice(0, perField).map(([value, e]) => ({ value, count: e.count, isDefault: e.isDefault })),
|
|
700
|
+
};
|
|
701
|
+
}),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Export one workflow version as a complete, self-contained definition: the workflow
|
|
706
|
+
* and version properties, inputs, stages, every activity with its type, labelled
|
|
707
|
+
* variables (defaults marked) and exits, the transitions, the activity types it uses
|
|
708
|
+
* (described from the instance, including whether they pause the workflow), display
|
|
709
|
+
* names for referenced records, the subflows it calls, and what uses it.
|
|
710
|
+
*
|
|
711
|
+
* @param workflow Workflow name or sys_id, or a version sys_id (exported as is)
|
|
712
|
+
*/
|
|
713
|
+
async exportWorkflow(workflow, options = {}) {
|
|
714
|
+
const versionSysId = await this.resolveVersionToRead(workflow, options.version ?? 'current');
|
|
715
|
+
const version = (await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${versionSysId}`, `${VERSION_FIELDS},order,run_multiple,after_business_rules,stage_field,on_cancel`, 1))[0];
|
|
716
|
+
const workflowSysId = str(version.workflow);
|
|
717
|
+
const definition = await this.getWorkflowDefinition(versionSysId, { includeStatus: false });
|
|
718
|
+
const typeIds = [...new Set(definition.activities.map(a => a.definitionSysId))];
|
|
719
|
+
// A session runs one transaction at a time; a wide fan-out only queues (or is refused with 202).
|
|
720
|
+
const details = await mapLimit(typeIds, 2, id => this.getActivityDefinition(id));
|
|
721
|
+
const waits = await mapLimit(details, 2, d => this.typeWaits(d));
|
|
722
|
+
const activityTypes = {};
|
|
723
|
+
details.forEach((d, i) => {
|
|
724
|
+
activityTypes[d.sysId] = {
|
|
725
|
+
sysId: d.sysId, name: d.name, category: d.category, sysClassName: d.sysClassName,
|
|
726
|
+
attributes: d.attributes, description: d.description, waits: waits[i],
|
|
727
|
+
...(d.inputs ? { inputs: d.inputs, outputs: d.outputs } : {}),
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
const variableDefs = new Map(details.map(d => [d.sysId, new Map(d.variables.map(v => [v.element, v]))]));
|
|
731
|
+
const [wfRows, inputRows, subflowRows, catalogRows, parentRows] = await Promise.all([
|
|
732
|
+
this.read(WorkflowManager.WF_WORKFLOW, `sys_id=${workflowSysId}`, 'sys_id,name,table,description,sys_scope.scope', 1),
|
|
733
|
+
this.read('var_dictionary', `model_id=${workflowSysId}^ORDERBYorder`, 'element,column_label,internal_type,mandatory,default_value', 500),
|
|
734
|
+
this.read('wf_workflow_instance', `workflow_version=${versionSysId}`, 'activity,workflow,workflow.name', 1000),
|
|
735
|
+
this.read('sc_cat_item', `workflow=${workflowSysId}^ORDERBYname`, 'sys_id,name,active', 500),
|
|
736
|
+
this.read('wf_workflow_instance', `workflow=${workflowSysId}^workflow_version.published=true`, 'workflow_version,workflow_version.workflow,workflow_version.name', 500),
|
|
737
|
+
]);
|
|
738
|
+
const subflows = new Map(subflowRows.map(r => [str(r.activity), { workflowSysId: str(r.workflow), name: str(r['workflow.name']) }]));
|
|
739
|
+
// Collect the sys_ids variables point at, by table, so they can be named once.
|
|
740
|
+
const wanted = new Map();
|
|
741
|
+
const want = (table, id) => {
|
|
742
|
+
if (!table || !SYS_ID.test(id))
|
|
743
|
+
return;
|
|
744
|
+
wanted.set(table, (wanted.get(table) ?? new Set()).add(id));
|
|
745
|
+
};
|
|
746
|
+
for (const activity of definition.activities) {
|
|
747
|
+
const defs = variableDefs.get(activity.definitionSysId);
|
|
748
|
+
for (const [element, value] of Object.entries(activity.variables ?? {})) {
|
|
749
|
+
const def = defs?.get(element);
|
|
750
|
+
if (def?.reference && (def.internalType === 'reference' || def.internalType === 'glide_list')) {
|
|
751
|
+
for (const id of value.split(','))
|
|
752
|
+
want(def.reference, id.trim());
|
|
753
|
+
}
|
|
754
|
+
for (const match of value.matchAll(/variables\.([0-9a-f]{32})/gi))
|
|
755
|
+
want('item_option_new', match[1]);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const references = await this.displayNames(wanted);
|
|
759
|
+
const activities = definition.activities.map(activity => {
|
|
760
|
+
const defs = variableDefs.get(activity.definitionSysId);
|
|
761
|
+
const variables = [];
|
|
762
|
+
for (const [element, value] of Object.entries(activity.variables ?? {})) {
|
|
763
|
+
if (value === '')
|
|
764
|
+
continue;
|
|
765
|
+
const def = defs?.get(element);
|
|
766
|
+
const exported = {
|
|
767
|
+
element, label: def?.label ?? element, type: def?.internalType ?? '', value,
|
|
768
|
+
isDefault: isDefaultValue(def, value),
|
|
769
|
+
};
|
|
770
|
+
const choice = def?.choices?.find(c => c.value === value);
|
|
771
|
+
const named = value.split(',').map(id => references[id.trim()]?.display).filter(Boolean);
|
|
772
|
+
if (choice)
|
|
773
|
+
exported.display = choice.label;
|
|
774
|
+
else if (named.length)
|
|
775
|
+
exported.display = named.join(', ');
|
|
776
|
+
variables.push(exported);
|
|
777
|
+
}
|
|
778
|
+
const result = {
|
|
779
|
+
sysId: activity.sysId,
|
|
780
|
+
name: activity.name,
|
|
781
|
+
typeSysId: activity.definitionSysId,
|
|
782
|
+
type: activity.definitionName,
|
|
783
|
+
x: activity.x,
|
|
784
|
+
y: activity.y,
|
|
785
|
+
stage: activity.stage,
|
|
786
|
+
parent: activity.parent,
|
|
787
|
+
variables,
|
|
788
|
+
exits: activity.conditions.map(c => ({
|
|
789
|
+
sysId: c.sysId, name: c.name, condition: c.condition, order: c.order, elseFlag: c.elseFlag, error: c.error,
|
|
790
|
+
})),
|
|
791
|
+
};
|
|
792
|
+
if (activity.input && activity.input !== '{}')
|
|
793
|
+
result.input = parseJsonObject(activity.input);
|
|
794
|
+
const subflow = subflows.get(activity.sysId);
|
|
795
|
+
if (subflow)
|
|
796
|
+
result.subflow = subflow;
|
|
797
|
+
return result;
|
|
798
|
+
});
|
|
799
|
+
const wf = wfRows[0] ?? {};
|
|
800
|
+
return {
|
|
801
|
+
format: 'now-sdk-ext/legacy-workflow@1',
|
|
802
|
+
exportedAt: new Date().toISOString(),
|
|
803
|
+
workflow: {
|
|
804
|
+
sysId: workflowSysId, name: str(wf.name), table: str(wf.table), description: str(wf.description),
|
|
805
|
+
scope: str(wf['sys_scope.scope']),
|
|
806
|
+
},
|
|
807
|
+
version: {
|
|
808
|
+
sysId: versionSysId,
|
|
809
|
+
name: str(version.name),
|
|
810
|
+
published: str(version.published) === 'true',
|
|
811
|
+
active: str(version.active) === 'true',
|
|
812
|
+
checkedOut: str(version.checked_out),
|
|
813
|
+
checkedOutBy: str(version.checked_out_by),
|
|
814
|
+
condition: str(version.condition),
|
|
815
|
+
conditionType: str(version.condition_type),
|
|
816
|
+
order: num(version.order),
|
|
817
|
+
runMultiple: str(version.run_multiple) === 'true',
|
|
818
|
+
afterBusinessRules: str(version.after_business_rules) === 'true',
|
|
819
|
+
stageField: str(version.stage_field),
|
|
820
|
+
onCancel: str(version.on_cancel),
|
|
821
|
+
start: str(version.start),
|
|
822
|
+
fullSequences: definition.fullSequences,
|
|
823
|
+
},
|
|
824
|
+
inputs: inputRows.map(r => ({
|
|
825
|
+
name: str(r.element), label: str(r.column_label), type: str(r.internal_type),
|
|
826
|
+
mandatory: str(r.mandatory) === 'true', defaultValue: str(r.default_value),
|
|
827
|
+
})),
|
|
828
|
+
stages: definition.stages,
|
|
829
|
+
activityTypes,
|
|
830
|
+
activities,
|
|
831
|
+
transitions: definition.transitions.map(t => ({ sysId: t.sysId, from: t.from, exit: t.condition, to: t.to })),
|
|
832
|
+
references,
|
|
833
|
+
usedBy: {
|
|
834
|
+
catalogItems: catalogRows.map(r => ({ sysId: str(r.sys_id), name: str(r.name), active: str(r.active) === 'true' })),
|
|
835
|
+
parentWorkflows: parentRows.map(r => ({
|
|
836
|
+
workflowSysId: str(r['workflow_version.workflow']), name: str(r['workflow_version.name']), versionSysId: str(r.workflow_version),
|
|
837
|
+
})),
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Plan the move of a workflow version to Flow Designer and write a Fluent (now-sdk)
|
|
843
|
+
* skeleton for it. Read-only: it exports the version and works from the export (see
|
|
844
|
+
* {@link planFlowConversion} and {@link generateFluentFlow} to work from a saved export).
|
|
845
|
+
*
|
|
846
|
+
* @param workflow Workflow name or sys_id, or a version sys_id
|
|
847
|
+
*/
|
|
848
|
+
async convertToFlow(workflow, options = {}) {
|
|
849
|
+
const data = await this.exportWorkflow(workflow, { version: options.version });
|
|
850
|
+
const plan = planFlowConversion(data);
|
|
851
|
+
return { export: data, plan, files: generateFluentFlow(plan, { directory: options.directory }) };
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Run the Workflow Editor's validation ("Validate Workflow") without publishing.
|
|
855
|
+
*/
|
|
856
|
+
async validateWorkflow(versionSysId) {
|
|
857
|
+
this.requireSysId(versionSysId, 'versionSysId');
|
|
858
|
+
// Rendering the report page is what runs the checks; the results are then
|
|
859
|
+
// readable as records.
|
|
860
|
+
const page = await this.request().get({
|
|
861
|
+
method: 'GET', path: '/validate_workflow.do', headers: null, body: null,
|
|
862
|
+
query: { sysparm_sys_id: versionSysId }, requires: READ_ONLY, responseFormat: 'text',
|
|
863
|
+
});
|
|
864
|
+
const html = typeof page.data === 'string' ? page.data : '';
|
|
865
|
+
if (!html.includes('id="wf_validation_summary_message"')) {
|
|
866
|
+
// Without the report page the checks did not run; the table may hold an earlier run.
|
|
867
|
+
throw new Error(`Could not run the validation for workflow version ${versionSysId} (status ${page.status ?? 'unknown'})`);
|
|
868
|
+
}
|
|
869
|
+
const summaryMatch = /id="wf_validation_summary_message"[^>]*>\s*<h4>([\s\S]*?)<\/h4>/.exec(html);
|
|
870
|
+
const summary = summaryMatch ? decodeEntities(summaryMatch[1]).replace(/\s+/g, ' ').trim() : '';
|
|
871
|
+
const rows = await this.read('v_wf_validation_report', `workflow_version=${versionSysId}`, 'type,level,message,details', 500);
|
|
872
|
+
const items = rows.map((row) => ({
|
|
873
|
+
type: str(row.type), level: str(row.level), message: str(row.message), details: str(row.details),
|
|
874
|
+
}));
|
|
875
|
+
if (!summary && !items.length) {
|
|
876
|
+
throw new Error(`Could not read the validation report for workflow version ${versionSysId}`);
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
versionSysId,
|
|
880
|
+
summary,
|
|
881
|
+
valid: !items.some(i => i.level === 'Warn' || i.level === 'Critical'),
|
|
882
|
+
items,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
// ============================================================
|
|
886
|
+
// Workflow lifecycle (Workflow Editor parity)
|
|
887
|
+
// ============================================================
|
|
888
|
+
/**
|
|
889
|
+
* Create a workflow the way the Workflow Editor's "New Workflow" does: a version with
|
|
890
|
+
* Begin and End activities joined by a transition, checked out to the current user.
|
|
891
|
+
*
|
|
892
|
+
* @throws InvalidParameterException when a workflow with this name already exists
|
|
893
|
+
*/
|
|
894
|
+
async newWorkflow(options) {
|
|
895
|
+
if (!options?.name || options.name.trim().length === 0)
|
|
896
|
+
throw new InvalidParameterException('Workflow name is required');
|
|
897
|
+
if (!options.table || options.table.trim().length === 0)
|
|
898
|
+
throw new InvalidParameterException('Table name is required');
|
|
899
|
+
const existing = await this.read(WorkflowManager.WF_WORKFLOW, `name=${queryValue(options.name, 'workflow name')}`, 'sys_id', 1);
|
|
900
|
+
if (existing.length)
|
|
901
|
+
throw new InvalidParameterException(`A workflow named '${options.name}' already exists (${str(existing[0].sys_id)})`);
|
|
902
|
+
// Inserting a version without a parent workflow is what the editor does; the
|
|
903
|
+
// instance's "Workflow initialize" rule then creates the workflow, Begin, End,
|
|
904
|
+
// the Begin exit and the Begin → End transition.
|
|
905
|
+
const body = { ...(options.fields ?? {}), name: options.name, table: options.table };
|
|
906
|
+
if (options.description !== undefined)
|
|
907
|
+
body.description = options.description;
|
|
908
|
+
if (options.condition !== undefined)
|
|
909
|
+
body.condition = options.condition;
|
|
910
|
+
if (options.conditionType !== undefined)
|
|
911
|
+
body.condition_type = options.conditionType;
|
|
912
|
+
this._logger.info(`Creating workflow '${options.name}' on ${options.table}`);
|
|
913
|
+
const response = await this._tableAPI.post(WorkflowManager.WF_WORKFLOW_VERSION, {}, body);
|
|
914
|
+
const versionSysId = response?.bodyObject?.result?.sys_id;
|
|
915
|
+
if (!versionSysId)
|
|
916
|
+
throw new Error(`Failed to create workflow '${options.name}'. Status: ${response?.status ?? 'unknown'}`);
|
|
917
|
+
const version = await this.readVersion(versionSysId);
|
|
918
|
+
const transitions = await this.read(WorkflowManager.WF_TRANSITION, `from.workflow_version=${versionSysId}`, 'sys_id,from,to,condition', 10);
|
|
919
|
+
const begin = str(version.start);
|
|
920
|
+
const transition = transitions.find(t => str(t.from) === begin);
|
|
921
|
+
if (!begin || !transition) {
|
|
922
|
+
throw new Error(`Workflow '${options.name}' was created (version ${versionSysId}) but its Begin/End activities were not`);
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
workflowSysId: str(version.workflow),
|
|
926
|
+
versionSysId,
|
|
927
|
+
beginActivitySysId: begin,
|
|
928
|
+
endActivitySysId: str(transition.to),
|
|
929
|
+
beginConditionSysId: str(transition.condition),
|
|
930
|
+
transitionSysId: str(transition.sys_id),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Check a workflow out for editing. Like the editor, this creates a draft version —
|
|
935
|
+
* a full copy of the published one — checked out to the current user. If the
|
|
936
|
+
* current user already has a draft, that draft is returned.
|
|
937
|
+
*
|
|
938
|
+
* @param workflow Workflow name, wf_workflow sys_id or a version sys_id
|
|
939
|
+
* @throws Error when another user has it checked out (unless `force`)
|
|
940
|
+
*/
|
|
941
|
+
async checkout(workflow, options = {}) {
|
|
942
|
+
const summary = await this.resolveWorkflow(workflow);
|
|
943
|
+
const draft = summary.checkedOutVersion;
|
|
944
|
+
if (draft) {
|
|
945
|
+
if (draft.byCurrentUser) {
|
|
946
|
+
return { workflowSysId: summary.sysId, versionSysId: draft.sysId, alreadyCheckedOut: true };
|
|
947
|
+
}
|
|
948
|
+
if (!options.force) {
|
|
949
|
+
throw new Error(`Workflow '${summary.name}' is checked out by ${draft.checkedOutByName || draft.checkedOutBy} `
|
|
950
|
+
+ `(since ${draft.checkedOutOn}). Pass force to take over the checkout.`);
|
|
951
|
+
}
|
|
952
|
+
const graph = await this._diagram.forceCheckout(summary.publishedVersionSysId ?? draft.sysId);
|
|
953
|
+
if (graph.id === summary.publishedVersionSysId || graph.readOnly) {
|
|
954
|
+
throw new Error(`Workflow '${summary.name}' could not be checked out (${graph.statusDisplay || 'no draft was returned'})`);
|
|
955
|
+
}
|
|
956
|
+
this._logger.info(`Force-checked out workflow '${summary.name}' as version ${graph.id}`);
|
|
957
|
+
return { workflowSysId: summary.sysId, versionSysId: graph.id, alreadyCheckedOut: false };
|
|
958
|
+
}
|
|
959
|
+
if (!summary.publishedVersionSysId) {
|
|
960
|
+
throw new Error(`Workflow '${summary.name}' has no published version to check out`);
|
|
961
|
+
}
|
|
962
|
+
const graph = await this._diagram.checkout(summary.publishedVersionSysId, summary.name);
|
|
963
|
+
if (graph.id === summary.publishedVersionSysId || graph.readOnly) {
|
|
964
|
+
throw new Error(`Workflow '${summary.name}' could not be checked out (${graph.statusDisplay || 'no draft was created'})`);
|
|
965
|
+
}
|
|
966
|
+
this._logger.info(`Checked out workflow '${summary.name}' as version ${graph.id}`);
|
|
967
|
+
return { workflowSysId: summary.sysId, versionSysId: graph.id, alreadyCheckedOut: false };
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* The draft version of a workflow that the current user has checked out.
|
|
971
|
+
*
|
|
972
|
+
* @param workflow Workflow name, wf_workflow sys_id or a version sys_id
|
|
973
|
+
* @throws Error when the workflow is not checked out, or is checked out by someone else
|
|
974
|
+
*/
|
|
975
|
+
async getDraftVersion(workflow) {
|
|
976
|
+
const summary = await this.resolveWorkflow(workflow);
|
|
977
|
+
const draft = summary.checkedOutVersion;
|
|
978
|
+
if (!draft)
|
|
979
|
+
throw new Error(`Workflow '${summary.name}' is not checked out. Check it out first.`);
|
|
980
|
+
if (!draft.byCurrentUser) {
|
|
981
|
+
throw new Error(`Workflow '${summary.name}' is checked out by ${draft.checkedOutByName || draft.checkedOutBy}, not the current user.`);
|
|
982
|
+
}
|
|
983
|
+
return draft.sysId;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Publish a checked-out draft, as the editor's Publish does: validate, publish, and
|
|
987
|
+
* retire the previously published version. The instance records the whole workflow
|
|
988
|
+
* in the current update set.
|
|
989
|
+
*
|
|
990
|
+
* @throws WorkflowValidationError when validation finds critical problems, or
|
|
991
|
+
* warnings without `allowWarnings`
|
|
992
|
+
*/
|
|
993
|
+
async publish(versionSysId, options = {}) {
|
|
994
|
+
await this.requireDraft(versionSysId);
|
|
995
|
+
let outcome = await this._diagram.publish(versionSysId, true);
|
|
996
|
+
let warnings = [];
|
|
997
|
+
if (outcome.messages.validationCritical !== undefined) {
|
|
998
|
+
const items = await this.findingsOrEmpty(versionSysId);
|
|
999
|
+
throw new WorkflowValidationError(versionSysId, 'critical', firstLine(outcome.messages.validationCritical, items), items);
|
|
1000
|
+
}
|
|
1001
|
+
if (outcome.messages.validationWarning !== undefined) {
|
|
1002
|
+
const items = await this.findingsOrEmpty(versionSysId);
|
|
1003
|
+
if (!options.allowWarnings) {
|
|
1004
|
+
throw new WorkflowValidationError(versionSysId, 'warning', firstLine(outcome.messages.validationWarning, items), items);
|
|
1005
|
+
}
|
|
1006
|
+
warnings = items;
|
|
1007
|
+
outcome = await this._diagram.publish(versionSysId, false);
|
|
1008
|
+
}
|
|
1009
|
+
if (!outcome.graph?.published) {
|
|
1010
|
+
throw new Error(`Workflow version ${versionSysId} was not published`);
|
|
1011
|
+
}
|
|
1012
|
+
this._logger.info(`Published workflow version ${versionSysId}`);
|
|
1013
|
+
return { versionSysId, warnings, fullSequences: outcome.graph.fullSequences };
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Throw away a checked-out draft, keeping the published version.
|
|
1017
|
+
*
|
|
1018
|
+
* @throws Error when the draft is the workflow's only version (use deleteWorkflow)
|
|
1019
|
+
*/
|
|
1020
|
+
async discardCheckout(versionSysId) {
|
|
1021
|
+
const version = await this.requireDraft(versionSysId);
|
|
1022
|
+
const others = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `workflow=${str(version.workflow)}^sys_id!=${versionSysId}`, 'sys_id', 1);
|
|
1023
|
+
if (!others.length) {
|
|
1024
|
+
throw new Error(`Version ${versionSysId} is the only version of workflow '${str(version.name)}'; `
|
|
1025
|
+
+ 'discarding it would delete the workflow. Use deleteWorkflow() to do that.');
|
|
1026
|
+
}
|
|
1027
|
+
await this._diagram.delete(versionSysId);
|
|
1028
|
+
if ((await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${versionSysId}`, 'sys_id', 1)).length) {
|
|
1029
|
+
throw new Error(`Draft version ${versionSysId} was not deleted`);
|
|
1030
|
+
}
|
|
1031
|
+
this._logger.info(`Discarded draft version ${versionSysId}`);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Delete a workflow and every version of it.
|
|
1035
|
+
*
|
|
1036
|
+
* @param workflow Workflow name, wf_workflow sys_id or a version sys_id
|
|
1037
|
+
*/
|
|
1038
|
+
async deleteWorkflow(workflow) {
|
|
1039
|
+
const summary = await this.resolveWorkflow(workflow);
|
|
1040
|
+
const versions = await this.getWorkflowVersions(summary.sysId);
|
|
1041
|
+
// Drafts first, the published version last: the parent workflow goes with its last version.
|
|
1042
|
+
const ordered = [...versions].sort((a, b) => Number(a.published) - Number(b.published));
|
|
1043
|
+
for (const version of ordered)
|
|
1044
|
+
await this._diagram.delete(version.sysId);
|
|
1045
|
+
if ((await this.read(WorkflowManager.WF_WORKFLOW, `sys_id=${summary.sysId}`, 'sys_id', 1)).length) {
|
|
1046
|
+
throw new Error(`Workflow '${summary.name}' was not fully deleted`);
|
|
1047
|
+
}
|
|
1048
|
+
this._logger.info(`Deleted workflow '${summary.name}' (${versions.length} versions)`);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Activate or deactivate a workflow (the editor's Set Active / Set Inactive).
|
|
1052
|
+
*
|
|
1053
|
+
* @param versionSysId The published version
|
|
1054
|
+
*/
|
|
1055
|
+
async setWorkflowActive(versionSysId, active) {
|
|
1056
|
+
this.requireSysId(versionSysId, 'versionSysId');
|
|
1057
|
+
await this._diagram.setActive(versionSysId, active);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Change a draft's workflow properties.
|
|
1061
|
+
*/
|
|
1062
|
+
async updateWorkflowProperties(versionSysId, options) {
|
|
1063
|
+
await this.requireDraft(versionSysId);
|
|
1064
|
+
const body = { ...(options.fields ?? {}) };
|
|
1065
|
+
if (options.name !== undefined)
|
|
1066
|
+
body.name = options.name;
|
|
1067
|
+
if (options.description !== undefined)
|
|
1068
|
+
body.description = options.description;
|
|
1069
|
+
if (options.condition !== undefined)
|
|
1070
|
+
body.condition = options.condition;
|
|
1071
|
+
if (options.conditionType !== undefined)
|
|
1072
|
+
body.condition_type = options.conditionType;
|
|
1073
|
+
if (!Object.keys(body).length)
|
|
1074
|
+
return;
|
|
1075
|
+
const response = await this._tableAPI.put(WorkflowManager.WF_WORKFLOW_VERSION, versionSysId, body);
|
|
1076
|
+
if (response?.status !== 200) {
|
|
1077
|
+
throw new Error(`Failed to update workflow version ${versionSysId}. Status: ${response?.status ?? 'unknown'}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// ============================================================
|
|
1081
|
+
// Editing a checked-out draft (Workflow Editor parity)
|
|
1082
|
+
// ============================================================
|
|
1083
|
+
/**
|
|
1084
|
+
* Add an activity to a checked-out draft, configured with variables, and optionally
|
|
1085
|
+
* wire it in. The activity is created through the editor's activity form, so its
|
|
1086
|
+
* variables are written and its default exits are created exactly as in the UI.
|
|
1087
|
+
*/
|
|
1088
|
+
async addActivity(versionSysId, options) {
|
|
1089
|
+
await this.requireDraft(versionSysId);
|
|
1090
|
+
if (!options?.name || options.name.trim().length === 0)
|
|
1091
|
+
throw new InvalidParameterException('Activity name is required');
|
|
1092
|
+
const definition = await this.resolveActivityDefinition(options.definition);
|
|
1093
|
+
const insertOn = options.insertOn ? await this.readTransition(versionSysId, options.insertOn) : undefined;
|
|
1094
|
+
const from = options.connectFrom ? await this.readActivity(options.connectFrom.activity, versionSysId) : undefined;
|
|
1095
|
+
if (options.connectTo)
|
|
1096
|
+
await this.readActivity(options.connectTo, versionSysId);
|
|
1097
|
+
let x = options.x;
|
|
1098
|
+
let y = options.y;
|
|
1099
|
+
if (x === undefined || y === undefined) {
|
|
1100
|
+
const position = this.defaultPosition(insertOn, from);
|
|
1101
|
+
x = x ?? position.x;
|
|
1102
|
+
y = y ?? position.y;
|
|
1103
|
+
}
|
|
1104
|
+
const fields = { name: options.name, x, y };
|
|
1105
|
+
if (options.stage !== undefined)
|
|
1106
|
+
fields.stage = await this.resolveStage(versionSysId, options.stage);
|
|
1107
|
+
if (options.input !== undefined)
|
|
1108
|
+
fields.input = typeof options.input === 'string' ? options.input : JSON.stringify(options.input);
|
|
1109
|
+
// Resolve everything that can be checked before anything is written.
|
|
1110
|
+
const fromExit = from
|
|
1111
|
+
? this.pickExit(await this.readConditions(from.sysId), options.connectFrom.condition, from.name, false)
|
|
1112
|
+
: undefined;
|
|
1113
|
+
this._logger.info(`Adding ${definition.name} activity '${options.name}' to version ${versionSysId}`);
|
|
1114
|
+
const written = await this._forms.insert(WorkflowManager.WF_ACTIVITY, {
|
|
1115
|
+
view: WorkflowManager.ACTIVITY_VIEW,
|
|
1116
|
+
initialQuery: `workflow_version=${versionSysId}^activity_definition=${definition.sysId}`,
|
|
1117
|
+
fields,
|
|
1118
|
+
variables: options.variables && Object.keys(options.variables).length ? options.variables : undefined,
|
|
1119
|
+
});
|
|
1120
|
+
const activitySysId = written.sysId;
|
|
1121
|
+
const conditions = await this.readConditions(activitySysId);
|
|
1122
|
+
// The new activity's exits exist only now (Switch builds them from its variables).
|
|
1123
|
+
// If the one asked for is not there, take the activity out again rather than
|
|
1124
|
+
// leave a half-wired step on the draft.
|
|
1125
|
+
let ownExit;
|
|
1126
|
+
try {
|
|
1127
|
+
if (insertOn || options.connectTo)
|
|
1128
|
+
ownExit = this.pickExit(conditions, options.exitCondition, options.name, true);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
await this.removeActivity(activitySysId).catch(cleanup => {
|
|
1132
|
+
this._logger.warn(`Could not remove activity ${activitySysId} after a failed add: ${cleanup.message}`);
|
|
1133
|
+
});
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
const transitionSysIds = [];
|
|
1137
|
+
try {
|
|
1138
|
+
if (insertOn) {
|
|
1139
|
+
const outgoing = { id: newSysId(), source: activitySysId, sourcePort: ownExit.sysId, target: insertOn.to };
|
|
1140
|
+
await this._diagram.newEdgeControlNode(versionSysId, { id: activitySysId, x, y }, [{ id: insertOn.sysId, source: insertOn.from, sourcePort: insertOn.condition, target: activitySysId }], [outgoing]);
|
|
1141
|
+
transitionSysIds.push(insertOn.sysId, outgoing.id);
|
|
1142
|
+
}
|
|
1143
|
+
if (from) {
|
|
1144
|
+
const edge = { id: newSysId(), source: from.sysId, sourcePort: fromExit.sysId, target: activitySysId };
|
|
1145
|
+
await this._diagram.newEdge(versionSysId, edge);
|
|
1146
|
+
transitionSysIds.push(edge.id);
|
|
1147
|
+
}
|
|
1148
|
+
if (options.connectTo) {
|
|
1149
|
+
const edge = { id: newSysId(), source: activitySysId, sourcePort: ownExit.sysId, target: options.connectTo };
|
|
1150
|
+
await this._diagram.newEdge(versionSysId, edge);
|
|
1151
|
+
transitionSysIds.push(edge.id);
|
|
1152
|
+
}
|
|
1153
|
+
await this._diagram.updateStages(versionSysId);
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
const e = error;
|
|
1157
|
+
e.message = `${e.message} (activity '${options.name}' ${activitySysId} was added to the draft; its wiring is incomplete)`;
|
|
1158
|
+
throw e;
|
|
1159
|
+
}
|
|
1160
|
+
return { activitySysId, name: options.name, conditions, transitionSysIds };
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Change an activity on a checked-out draft. Variables you do not pass keep their values.
|
|
1164
|
+
*/
|
|
1165
|
+
async updateActivity(activitySysId, options) {
|
|
1166
|
+
const activity = await this.readActivity(activitySysId);
|
|
1167
|
+
await this.requireDraft(activity.versionSysId);
|
|
1168
|
+
const fields = {};
|
|
1169
|
+
if (options.name !== undefined)
|
|
1170
|
+
fields.name = options.name;
|
|
1171
|
+
if (options.stage !== undefined)
|
|
1172
|
+
fields.stage = await this.resolveStage(activity.versionSysId, options.stage);
|
|
1173
|
+
if (options.input !== undefined)
|
|
1174
|
+
fields.input = typeof options.input === 'string' ? options.input : JSON.stringify(options.input);
|
|
1175
|
+
const variables = options.variables && Object.keys(options.variables).length ? options.variables : undefined;
|
|
1176
|
+
if (Object.keys(fields).length || variables) {
|
|
1177
|
+
await this._forms.update(WorkflowManager.WF_ACTIVITY, activitySysId, {
|
|
1178
|
+
view: WorkflowManager.ACTIVITY_VIEW, fields, variables,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
if (options.x !== undefined || options.y !== undefined) {
|
|
1182
|
+
await this._diagram.moveNodes(activity.versionSysId, [{
|
|
1183
|
+
id: activitySysId, x: options.x ?? activity.x ?? 0, y: options.y ?? activity.y ?? 0,
|
|
1184
|
+
}]);
|
|
1185
|
+
}
|
|
1186
|
+
if (fields.stage !== undefined)
|
|
1187
|
+
await this._diagram.updateStages(activity.versionSysId);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Remove an activity (with its exits and variables) from a checked-out draft.
|
|
1191
|
+
*/
|
|
1192
|
+
async removeActivity(activitySysId, options = {}) {
|
|
1193
|
+
const activity = await this.readActivity(activitySysId);
|
|
1194
|
+
await this.requireDraft(activity.versionSysId);
|
|
1195
|
+
if (/(^|,)\s*(begin|end)\s*=\s*true/i.test(activity.definitionAttributes)) {
|
|
1196
|
+
throw new InvalidParameterException(`'${activity.name}' is the workflow's ${activity.definitionName} activity and cannot be removed`);
|
|
1197
|
+
}
|
|
1198
|
+
const touching = (await this.read(WorkflowManager.WF_TRANSITION, `from=${activitySysId}^ORto=${activitySysId}`, 'sys_id,from,to,condition', 1000))
|
|
1199
|
+
.map(toEdge);
|
|
1200
|
+
const incoming = touching.filter(e => e.target === activitySysId && e.source !== activitySysId);
|
|
1201
|
+
const outgoing = touching.filter(e => e.source === activitySysId);
|
|
1202
|
+
if (options.reconnect) {
|
|
1203
|
+
if (incoming.length !== 1 || outgoing.length !== 1) {
|
|
1204
|
+
throw new InvalidParameterException(`Cannot reconnect around '${activity.name}': it has ${incoming.length} incoming `
|
|
1205
|
+
+ `and ${outgoing.length} outgoing transitions (exactly one of each is needed)`);
|
|
1206
|
+
}
|
|
1207
|
+
const joined = { ...incoming[0], target: outgoing[0].target };
|
|
1208
|
+
// The path may already exist; the editor never draws the same exit-to-target twice.
|
|
1209
|
+
const duplicate = await this.read(WorkflowManager.WF_TRANSITION, `condition=${joined.sourcePort}^to=${joined.target}^sys_id!=${joined.id}`, 'sys_id', 1);
|
|
1210
|
+
if (duplicate.length) {
|
|
1211
|
+
await this._diagram.deleteNode(activity.versionSysId, activitySysId, [...incoming, ...outgoing]);
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
await this._diagram.deleteNode(activity.versionSysId, activitySysId, outgoing, [joined]);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
await this._diagram.deleteNode(activity.versionSysId, activitySysId, touching);
|
|
1219
|
+
}
|
|
1220
|
+
if ((await this.read(WorkflowManager.WF_ACTIVITY, `sys_id=${activitySysId}`, 'sys_id', 1)).length) {
|
|
1221
|
+
throw new Error(`Activity '${activity.name}' was not removed`);
|
|
1222
|
+
}
|
|
1223
|
+
this._logger.info(`Removed activity '${activity.name}' from version ${activity.versionSysId}`);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Reposition activities on checked-out drafts.
|
|
1227
|
+
*/
|
|
1228
|
+
async moveActivities(positions) {
|
|
1229
|
+
const byVersion = new Map();
|
|
1230
|
+
for (const position of positions ?? []) {
|
|
1231
|
+
const activity = await this.readActivity(position.sysId);
|
|
1232
|
+
byVersion.set(activity.versionSysId, [...(byVersion.get(activity.versionSysId) ?? []), position]);
|
|
1233
|
+
}
|
|
1234
|
+
for (const [versionSysId, moves] of byVersion) {
|
|
1235
|
+
await this.requireDraft(versionSysId);
|
|
1236
|
+
await this._diagram.moveNodes(versionSysId, moves.map(m => ({ id: m.sysId, x: m.x, y: m.y })));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Add a transition on a checked-out draft.
|
|
1241
|
+
*
|
|
1242
|
+
* @returns The new transition's sys_id
|
|
1243
|
+
*/
|
|
1244
|
+
async addTransition(options) {
|
|
1245
|
+
const from = await this.readActivity(options.from);
|
|
1246
|
+
await this.requireDraft(from.versionSysId);
|
|
1247
|
+
await this.readActivity(options.to, from.versionSysId);
|
|
1248
|
+
const exit = this.pickExit(await this.readConditions(from.sysId), options.condition, from.name, false);
|
|
1249
|
+
const existing = await this.read(WorkflowManager.WF_TRANSITION, `condition=${exit.sysId}^to=${options.to}`, 'sys_id', 1);
|
|
1250
|
+
if (existing.length)
|
|
1251
|
+
throw new InvalidParameterException(`'${from.name}' (${exit.name}) already leads to that activity (${str(existing[0].sys_id)})`);
|
|
1252
|
+
const edge = { id: newSysId(), source: from.sysId, sourcePort: exit.sysId, target: options.to };
|
|
1253
|
+
await this._diagram.newEdge(from.versionSysId, edge);
|
|
1254
|
+
return edge.id;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Point an existing transition at a different activity on a checked-out draft.
|
|
1258
|
+
*/
|
|
1259
|
+
async retargetTransition(transitionSysId, toActivitySysId) {
|
|
1260
|
+
const transition = await this.readTransition(undefined, transitionSysId);
|
|
1261
|
+
await this.requireDraft(transition.versionSysId);
|
|
1262
|
+
await this.readActivity(toActivitySysId, transition.versionSysId);
|
|
1263
|
+
await this._diagram.changeEdge(transition.versionSysId, {
|
|
1264
|
+
id: transition.sysId, source: transition.from, sourcePort: transition.condition, target: toActivitySysId,
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Remove a transition from a checked-out draft.
|
|
1269
|
+
*/
|
|
1270
|
+
async removeTransition(transitionSysId) {
|
|
1271
|
+
const transition = await this.readTransition(undefined, transitionSysId);
|
|
1272
|
+
await this.requireDraft(transition.versionSysId);
|
|
1273
|
+
await this._diagram.deleteEdge(transition.versionSysId, {
|
|
1274
|
+
id: transition.sysId, source: transition.from, sourcePort: transition.condition, target: transition.to,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Add an exit to an activity on a checked-out draft.
|
|
1279
|
+
*
|
|
1280
|
+
* @returns The new condition's sys_id
|
|
1281
|
+
*/
|
|
1282
|
+
async addCondition(options) {
|
|
1283
|
+
const activity = await this.readActivity(options.activity);
|
|
1284
|
+
await this.requireDraft(activity.versionSysId);
|
|
1285
|
+
if (!options.name || !options.condition)
|
|
1286
|
+
throw new InvalidParameterException('Condition name and condition are required');
|
|
1287
|
+
const body = { activity: activity.sysId, name: options.name, condition: options.condition };
|
|
1288
|
+
if (options.order !== undefined)
|
|
1289
|
+
body.order = options.order;
|
|
1290
|
+
if (options.elseFlag !== undefined)
|
|
1291
|
+
body.else_flag = options.elseFlag;
|
|
1292
|
+
if (options.error !== undefined)
|
|
1293
|
+
body.error = options.error;
|
|
1294
|
+
if (options.shortDescription !== undefined)
|
|
1295
|
+
body.short_description = options.shortDescription;
|
|
1296
|
+
const response = await this._tableAPI.post(WorkflowManager.WF_CONDITION, {}, body);
|
|
1297
|
+
const sysId = response?.bodyObject?.result?.sys_id;
|
|
1298
|
+
if (!sysId)
|
|
1299
|
+
throw new Error(`Failed to add condition '${options.name}'. Status: ${response?.status ?? 'unknown'}`);
|
|
1300
|
+
return sysId;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Change an exit on a checked-out draft.
|
|
1304
|
+
*/
|
|
1305
|
+
async updateCondition(conditionSysId, options) {
|
|
1306
|
+
const condition = await this.readCondition(conditionSysId);
|
|
1307
|
+
await this.requireDraft(condition.versionSysId);
|
|
1308
|
+
const body = {};
|
|
1309
|
+
if (options.name !== undefined)
|
|
1310
|
+
body.name = options.name;
|
|
1311
|
+
if (options.condition !== undefined)
|
|
1312
|
+
body.condition = options.condition;
|
|
1313
|
+
if (options.order !== undefined)
|
|
1314
|
+
body.order = options.order;
|
|
1315
|
+
if (options.elseFlag !== undefined)
|
|
1316
|
+
body.else_flag = options.elseFlag;
|
|
1317
|
+
if (options.error !== undefined)
|
|
1318
|
+
body.error = options.error;
|
|
1319
|
+
if (options.shortDescription !== undefined)
|
|
1320
|
+
body.short_description = options.shortDescription;
|
|
1321
|
+
if (!Object.keys(body).length)
|
|
1322
|
+
return;
|
|
1323
|
+
const response = await this._tableAPI.put(WorkflowManager.WF_CONDITION, conditionSysId, body);
|
|
1324
|
+
if (response?.status !== 200)
|
|
1325
|
+
throw new Error(`Failed to update condition ${conditionSysId}. Status: ${response?.status ?? 'unknown'}`);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Remove an exit, and the transitions leaving from it, from a checked-out draft.
|
|
1329
|
+
*/
|
|
1330
|
+
async removeCondition(conditionSysId) {
|
|
1331
|
+
const condition = await this.readCondition(conditionSysId);
|
|
1332
|
+
await this.requireDraft(condition.versionSysId);
|
|
1333
|
+
const leaving = (await this.read(WorkflowManager.WF_TRANSITION, `condition=${conditionSysId}`, 'sys_id,from,to,condition', 1000)).map(toEdge);
|
|
1334
|
+
for (const edge of leaving)
|
|
1335
|
+
await this._diagram.deleteEdge(condition.versionSysId, edge);
|
|
1336
|
+
await this.request().delete({
|
|
1337
|
+
method: 'DELETE', path: `/api/now/table/${WorkflowManager.WF_CONDITION}/${conditionSysId}`,
|
|
1338
|
+
headers: { Accept: 'application/json' }, query: null, body: null,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
// ============================================================
|
|
1342
|
+
// Internals
|
|
1343
|
+
// ============================================================
|
|
1344
|
+
async createActivityThroughForm(options) {
|
|
1345
|
+
if (!options.activityDefinitionSysId) {
|
|
1346
|
+
throw new Error('activityDefinitionSysId is required when variables are given');
|
|
1347
|
+
}
|
|
1348
|
+
// Both go into the form's initial query, which the signed form state carries.
|
|
1349
|
+
this.requireSysId(options.workflowVersionSysId, 'workflowVersionSysId');
|
|
1350
|
+
this.requireSysId(options.activityDefinitionSysId, 'activityDefinitionSysId');
|
|
1351
|
+
const fields = { name: options.name };
|
|
1352
|
+
if (options.x !== undefined)
|
|
1353
|
+
fields.x = options.x;
|
|
1354
|
+
if (options.y !== undefined)
|
|
1355
|
+
fields.y = options.y;
|
|
1356
|
+
if (options.width !== undefined)
|
|
1357
|
+
fields.width = options.width;
|
|
1358
|
+
if (options.height !== undefined)
|
|
1359
|
+
fields.height = options.height;
|
|
1360
|
+
const written = await this._forms.insert(WorkflowManager.WF_ACTIVITY, {
|
|
1361
|
+
view: WorkflowManager.ACTIVITY_VIEW,
|
|
1362
|
+
initialQuery: `workflow_version=${options.workflowVersionSysId}^activity_definition=${options.activityDefinitionSysId}`,
|
|
1363
|
+
fields,
|
|
1364
|
+
variables: Object.keys(options.variables).length ? options.variables : undefined,
|
|
1365
|
+
});
|
|
1366
|
+
this._logger.info(`Created activity '${options.name}' with sys_id=${written.sysId}`);
|
|
1367
|
+
return { activitySysId: written.sysId, name: options.name };
|
|
1368
|
+
}
|
|
1369
|
+
async summarize(workflows) {
|
|
1370
|
+
if (!workflows.length)
|
|
1371
|
+
return [];
|
|
1372
|
+
const ids = workflows.map(w => str(w.sys_id));
|
|
1373
|
+
const versions = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `workflowIN${ids.join(',')}^published=true^ORchecked_outISNOTEMPTY`, VERSION_FIELDS, 10000);
|
|
1374
|
+
const me = versions.some(v => str(v.published) !== 'true' && str(v.checked_out_by)) ? await this.currentUserSysId() : '';
|
|
1375
|
+
return workflows.map(row => {
|
|
1376
|
+
const own = versions.filter(v => str(v.workflow) === str(row.sys_id));
|
|
1377
|
+
const published = own.find(v => str(v.published) === 'true');
|
|
1378
|
+
const draft = own.find(v => str(v.published) !== 'true' && str(v.checked_out_by));
|
|
1379
|
+
const summary = {
|
|
1380
|
+
sysId: str(row.sys_id),
|
|
1381
|
+
name: str(row.name),
|
|
1382
|
+
table: str(row.table),
|
|
1383
|
+
description: str(row.description),
|
|
1384
|
+
active: str((published ?? draft)?.active) === 'true',
|
|
1385
|
+
};
|
|
1386
|
+
if (published)
|
|
1387
|
+
summary.publishedVersionSysId = str(published.sys_id);
|
|
1388
|
+
if (draft) {
|
|
1389
|
+
summary.checkedOutVersion = {
|
|
1390
|
+
sysId: str(draft.sys_id),
|
|
1391
|
+
checkedOutBy: str(draft.checked_out_by),
|
|
1392
|
+
checkedOutByName: str(draft['checked_out_by.name']) || undefined,
|
|
1393
|
+
checkedOutOn: str(draft.checked_out),
|
|
1394
|
+
byCurrentUser: str(draft.checked_out_by) === me,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
return summary;
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
async readVersion(versionSysId) {
|
|
1401
|
+
const rows = await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${versionSysId}`, VERSION_FIELDS, 1);
|
|
1402
|
+
if (!rows.length)
|
|
1403
|
+
throw new InvalidParameterException(`Workflow version ${versionSysId} was not found`);
|
|
1404
|
+
return rows[0];
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Editing is only allowed on an unpublished draft checked out to the current user —
|
|
1408
|
+
* the same rule the editor enforces (and the wf_condition ACLs require a checkout).
|
|
1409
|
+
*/
|
|
1410
|
+
async requireDraft(versionSysId) {
|
|
1411
|
+
this.requireSysId(versionSysId, 'versionSysId');
|
|
1412
|
+
const version = await this.readVersion(versionSysId);
|
|
1413
|
+
if (str(version.published) === 'true') {
|
|
1414
|
+
throw new Error(`Workflow version ${versionSysId} ('${str(version.name)}') is published. Check the workflow out and edit the draft.`);
|
|
1415
|
+
}
|
|
1416
|
+
const holder = str(version.checked_out_by);
|
|
1417
|
+
if (!holder) {
|
|
1418
|
+
throw new Error(`Workflow version ${versionSysId} ('${str(version.name)}') is not checked out; it is a retired version.`);
|
|
1419
|
+
}
|
|
1420
|
+
if (holder !== await this.currentUserSysId()) {
|
|
1421
|
+
throw new Error(`Workflow version ${versionSysId} ('${str(version.name)}') is checked out by ${str(version['checked_out_by.name']) || holder}.`);
|
|
1422
|
+
}
|
|
1423
|
+
return version;
|
|
1424
|
+
}
|
|
1425
|
+
async readActivity(activitySysId, versionSysId) {
|
|
1426
|
+
this.requireSysId(activitySysId, 'activity sys_id');
|
|
1427
|
+
const rows = await this.read(WorkflowManager.WF_ACTIVITY, `sys_id=${activitySysId}`, 'sys_id,name,workflow_version,activity_definition.name,activity_definition.attributes,x,y', 1);
|
|
1428
|
+
if (!rows.length)
|
|
1429
|
+
throw new InvalidParameterException(`Activity ${activitySysId} was not found`);
|
|
1430
|
+
const row = rows[0];
|
|
1431
|
+
if (versionSysId && str(row.workflow_version) !== versionSysId) {
|
|
1432
|
+
throw new InvalidParameterException(`Activity ${activitySysId} ('${str(row.name)}') belongs to a different workflow version`);
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
sysId: activitySysId,
|
|
1436
|
+
name: str(row.name),
|
|
1437
|
+
versionSysId: str(row.workflow_version),
|
|
1438
|
+
definitionName: str(row['activity_definition.name']),
|
|
1439
|
+
definitionAttributes: str(row['activity_definition.attributes']),
|
|
1440
|
+
x: optNum(row.x),
|
|
1441
|
+
y: optNum(row.y),
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
async readTransition(versionSysId, transitionSysId) {
|
|
1445
|
+
this.requireSysId(transitionSysId, 'transition sys_id');
|
|
1446
|
+
const rows = await this.read(WorkflowManager.WF_TRANSITION, `sys_id=${transitionSysId}`, 'sys_id,from,to,condition,from.workflow_version,from.x,from.y,to.x,to.y', 1);
|
|
1447
|
+
if (!rows.length)
|
|
1448
|
+
throw new InvalidParameterException(`Transition ${transitionSysId} was not found`);
|
|
1449
|
+
const row = rows[0];
|
|
1450
|
+
const owner = str(row['from.workflow_version']);
|
|
1451
|
+
if (versionSysId && owner !== versionSysId) {
|
|
1452
|
+
throw new InvalidParameterException(`Transition ${transitionSysId} belongs to a different workflow version`);
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
sysId: transitionSysId, from: str(row.from), to: str(row.to), condition: str(row.condition), versionSysId: owner,
|
|
1456
|
+
fromX: optNum(row['from.x']), fromY: optNum(row['from.y']), toX: optNum(row['to.x']), toY: optNum(row['to.y']),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
async readCondition(conditionSysId) {
|
|
1460
|
+
this.requireSysId(conditionSysId, 'condition sys_id');
|
|
1461
|
+
const rows = await this.read(WorkflowManager.WF_CONDITION, `sys_id=${conditionSysId}`, `${CONDITION_FIELDS},activity.workflow_version`, 1);
|
|
1462
|
+
if (!rows.length)
|
|
1463
|
+
throw new InvalidParameterException(`Condition ${conditionSysId} was not found`);
|
|
1464
|
+
return { ...toCondition(rows[0]), versionSysId: str(rows[0]['activity.workflow_version']) };
|
|
1465
|
+
}
|
|
1466
|
+
async readConditions(activitySysId) {
|
|
1467
|
+
return (await this.read(WorkflowManager.WF_CONDITION, `activity=${activitySysId}^ORDERBYorder`, CONDITION_FIELDS, 500)).map(toCondition);
|
|
1468
|
+
}
|
|
1469
|
+
async readVariableValues(activitySysIds) {
|
|
1470
|
+
const values = new Map();
|
|
1471
|
+
for (let i = 0; i < activitySysIds.length; i += 100) {
|
|
1472
|
+
const chunk = activitySysIds.slice(i, i + 100);
|
|
1473
|
+
const rows = await this.read('sys_variable_value', `document=wf_activity^document_keyIN${chunk.join(',')}^ORDERBYorder`, 'document_key,variable.element,value', 10000);
|
|
1474
|
+
for (const row of rows) {
|
|
1475
|
+
const key = str(row.document_key);
|
|
1476
|
+
const element = str(row['variable.element']);
|
|
1477
|
+
if (!element)
|
|
1478
|
+
continue;
|
|
1479
|
+
values.set(key, { ...(values.get(key) ?? {}), [element]: str(row.value) });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return values;
|
|
1483
|
+
}
|
|
1484
|
+
async resolveActivityDefinition(nameOrSysId) {
|
|
1485
|
+
const ref = (nameOrSysId ?? '').trim();
|
|
1486
|
+
if (!ref)
|
|
1487
|
+
throw new InvalidParameterException('An activity definition name or sys_id is required');
|
|
1488
|
+
const fields = 'sys_id,name,category,sys_class_name';
|
|
1489
|
+
let rows = SYS_ID.test(ref) ? await this.read('wf_element_definition', `sys_id=${ref}`, fields, 1) : [];
|
|
1490
|
+
if (!rows.length)
|
|
1491
|
+
rows = await this.read('wf_element_definition', `name=${queryValue(ref, 'activity definition name')}`, fields, 20);
|
|
1492
|
+
if (!rows.length)
|
|
1493
|
+
throw new InvalidParameterException(`No activity definition found for '${ref}'`);
|
|
1494
|
+
if (rows.length > 1) {
|
|
1495
|
+
// A core activity wins over same-named Activity Designer ones.
|
|
1496
|
+
const core = rows.filter(r => str(r.sys_class_name) === 'wf_activity_definition');
|
|
1497
|
+
if (core.length === 1)
|
|
1498
|
+
rows = core;
|
|
1499
|
+
else {
|
|
1500
|
+
throw new InvalidParameterException(`'${ref}' matches ${rows.length} activity definitions; use a sys_id: `
|
|
1501
|
+
+ rows.map(r => `${str(r.sys_id)} (${str(r.sys_class_name)}${str(r.category) ? `, ${str(r.category)}` : ''})`).join(', '));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return toDefinitionSummary(rows[0]);
|
|
1505
|
+
}
|
|
1506
|
+
async resolveStage(versionSysId, stage) {
|
|
1507
|
+
if (stage === '')
|
|
1508
|
+
return '';
|
|
1509
|
+
const stages = await this.read('wf_stage', `workflow_version=${versionSysId}`, 'sys_id,name,value', 1000);
|
|
1510
|
+
const byId = stages.find(s => str(s.sys_id) === stage);
|
|
1511
|
+
const byName = stages.filter(s => str(s.name).toLowerCase() === stage.toLowerCase());
|
|
1512
|
+
const byValue = stages.filter(s => str(s.value).toLowerCase() === stage.toLowerCase());
|
|
1513
|
+
const candidates = byId ? [byId] : byName.length ? byName : byValue;
|
|
1514
|
+
if (candidates.length > 1) {
|
|
1515
|
+
throw new InvalidParameterException(`${candidates.length} stages match '${stage}' on workflow version ${versionSysId}; `
|
|
1516
|
+
+ `use a sys_id: ${candidates.map(c => str(c.sys_id)).join(', ')}`);
|
|
1517
|
+
}
|
|
1518
|
+
const match = candidates[0];
|
|
1519
|
+
if (!match) {
|
|
1520
|
+
const known = stages.map(s => str(s.name)).join(', ') || '(this version has no stages)';
|
|
1521
|
+
throw new InvalidParameterException(`No stage '${stage}' on workflow version ${versionSysId}. Available: ${known}`);
|
|
1522
|
+
}
|
|
1523
|
+
return str(match.sys_id);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Pick an exit by name or sys_id. With no reference, a single exit is unambiguous;
|
|
1527
|
+
* for several, `firstWhenAmbiguous` takes the first in order (what the editor does
|
|
1528
|
+
* when you drop onto a line), otherwise the caller must choose.
|
|
1529
|
+
*/
|
|
1530
|
+
pickExit(conditions, ref, activityName, firstWhenAmbiguous) {
|
|
1531
|
+
if (!conditions.length)
|
|
1532
|
+
throw new InvalidParameterException(`'${activityName}' has no exits to connect from`);
|
|
1533
|
+
const names = conditions.map(c => c.name).join(', ');
|
|
1534
|
+
if (ref) {
|
|
1535
|
+
const byId = conditions.find(c => c.sysId === ref);
|
|
1536
|
+
if (byId)
|
|
1537
|
+
return byId;
|
|
1538
|
+
const byName = conditions.filter(c => c.name.toLowerCase() === ref.toLowerCase());
|
|
1539
|
+
if (byName.length > 1) {
|
|
1540
|
+
throw new InvalidParameterException(`'${activityName}' has ${byName.length} exits named '${ref}'; use a sys_id: `
|
|
1541
|
+
+ byName.map(c => c.sysId).join(', '));
|
|
1542
|
+
}
|
|
1543
|
+
if (!byName.length)
|
|
1544
|
+
throw new InvalidParameterException(`'${activityName}' has no exit '${ref}'. Exits: ${names}`);
|
|
1545
|
+
return byName[0];
|
|
1546
|
+
}
|
|
1547
|
+
if (conditions.length === 1 || firstWhenAmbiguous)
|
|
1548
|
+
return conditions[0];
|
|
1549
|
+
throw new InvalidParameterException(`'${activityName}' has several exits; specify one of: ${names}`);
|
|
1550
|
+
}
|
|
1551
|
+
defaultPosition(insertOn, from) {
|
|
1552
|
+
if (insertOn) {
|
|
1553
|
+
return {
|
|
1554
|
+
x: Math.round(((insertOn.fromX ?? 0) + (insertOn.toX ?? 0)) / 2),
|
|
1555
|
+
y: Math.round(((insertOn.fromY ?? 0) + (insertOn.toY ?? 0)) / 2),
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
if (from)
|
|
1559
|
+
return { x: (from.x ?? 0) + 180, y: from.y ?? 0 };
|
|
1560
|
+
return { x: 200, y: 200 };
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* The version to read for a workflow reference: a version sys_id as is; otherwise the
|
|
1564
|
+
* current user's draft and/or the published version, as asked.
|
|
1565
|
+
*/
|
|
1566
|
+
async resolveVersionToRead(ref, which) {
|
|
1567
|
+
if (SYS_ID.test(ref ?? '') && (await this.read(WorkflowManager.WF_WORKFLOW_VERSION, `sys_id=${ref}`, 'sys_id', 1)).length) {
|
|
1568
|
+
return ref;
|
|
1569
|
+
}
|
|
1570
|
+
const summary = await this.resolveWorkflow(ref);
|
|
1571
|
+
const mine = summary.checkedOutVersion?.byCurrentUser ? summary.checkedOutVersion.sysId : undefined;
|
|
1572
|
+
const chosen = which === 'draft' ? mine
|
|
1573
|
+
: which === 'published' ? summary.publishedVersionSysId
|
|
1574
|
+
: mine ?? summary.publishedVersionSysId ?? summary.checkedOutVersion?.sysId;
|
|
1575
|
+
if (!chosen)
|
|
1576
|
+
throw new Error(`Workflow '${summary.name}' has no ${which === 'current' ? 'readable' : which} version`);
|
|
1577
|
+
return chosen;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Whether a core activity type can pause the workflow, read from its handler script:
|
|
1581
|
+
* it sets `activity.state` / `executing.state` to `'waiting'`, itself or in a handler it
|
|
1582
|
+
* extends. Some only wait conditionally (tasks honour `wait_for_completion`).
|
|
1583
|
+
*/
|
|
1584
|
+
async typeWaits(detail) {
|
|
1585
|
+
if (detail.sysClassName !== 'wf_activity_definition')
|
|
1586
|
+
return undefined;
|
|
1587
|
+
let query = `sys_id=${detail.sysId}`;
|
|
1588
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
1589
|
+
const rows = await this.read('wf_activity_definition', query, 'script', 1);
|
|
1590
|
+
if (!rows.length)
|
|
1591
|
+
return undefined;
|
|
1592
|
+
const script = str(rows[0].script).replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
1593
|
+
if (/\b(activity|executing)\.state\s*=\s*['"]waiting['"]/.test(script))
|
|
1594
|
+
return true;
|
|
1595
|
+
const parent = /extendsObject\(\s*(\w+)ActivityHandler\b/.exec(script)?.[1];
|
|
1596
|
+
if (!parent || parent === 'WF')
|
|
1597
|
+
return false;
|
|
1598
|
+
query = `js_class_name=${parent}`;
|
|
1599
|
+
}
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
/** Display names for sys_ids, grouped by table. Unknown tables or records are skipped. */
|
|
1603
|
+
async displayNames(wanted) {
|
|
1604
|
+
const names = {};
|
|
1605
|
+
await mapLimit([...wanted.entries()], 2, async ([table, ids]) => {
|
|
1606
|
+
if (!/^[a-z0-9_]+$/i.test(table))
|
|
1607
|
+
return;
|
|
1608
|
+
const list = [...ids];
|
|
1609
|
+
for (let i = 0; i < list.length; i += 100) {
|
|
1610
|
+
let rows = [];
|
|
1611
|
+
try {
|
|
1612
|
+
rows = await this.read(table, `sys_idIN${list.slice(i, i + 100).join(',')}`, 'sys_id,name,label,question_text,user_name,number,short_description,title', 100);
|
|
1613
|
+
}
|
|
1614
|
+
catch {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
for (const row of rows) {
|
|
1618
|
+
const display = ['name', 'question_text', 'label', 'title', 'user_name', 'number', 'short_description']
|
|
1619
|
+
.map(f => str(row[f])).find(Boolean);
|
|
1620
|
+
if (display)
|
|
1621
|
+
names[str(row.sys_id)] = { table, display };
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
return names;
|
|
1626
|
+
}
|
|
1627
|
+
async findingsOrEmpty(versionSysId) {
|
|
1628
|
+
try {
|
|
1629
|
+
const report = await this.validateWorkflow(versionSysId);
|
|
1630
|
+
return report.items.filter(i => i.level === 'Warn' || i.level === 'Critical');
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
this._logger.warn(`Could not read validation details for ${versionSysId}: ${error.message}`);
|
|
1634
|
+
return [];
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async currentUserSysId() {
|
|
1638
|
+
if (this._currentUser === undefined) {
|
|
1639
|
+
this._currentUser = this.read('sys_user', 'sys_id=javascript:gs.getUserID()', 'sys_id', 2).then(rows => {
|
|
1640
|
+
// Anything but exactly one row means the query was not evaluated as written.
|
|
1641
|
+
const id = rows.length === 1 ? str(rows[0].sys_id) : '';
|
|
1642
|
+
if (!id)
|
|
1643
|
+
throw new Error('Could not determine the current user');
|
|
1644
|
+
return id;
|
|
1645
|
+
});
|
|
1646
|
+
this._currentUser.catch(() => { this._currentUser = undefined; });
|
|
1647
|
+
}
|
|
1648
|
+
return this._currentUser;
|
|
1649
|
+
}
|
|
1650
|
+
async read(table, query, fields, limit) {
|
|
1651
|
+
const params = { sysparm_query: query, sysparm_fields: fields, sysparm_limit: String(limit), sysparm_exclude_reference_link: 'true' };
|
|
1652
|
+
let response = await this._tableAPI.get(table, params);
|
|
1653
|
+
// 202: the instance queued the request behind others in this session; ask again.
|
|
1654
|
+
for (let attempt = 1; response?.status === 202 && attempt <= 3; attempt++) {
|
|
1655
|
+
await new Promise(resolve => setTimeout(resolve, 500 * attempt));
|
|
1656
|
+
response = await this._tableAPI.get(table, params);
|
|
1657
|
+
}
|
|
1658
|
+
if (response?.status !== 200 || !Array.isArray(response.bodyObject?.result)) {
|
|
1659
|
+
throw new Error(`Failed to read ${table}. Status: ${response?.status ?? 'unknown'}`);
|
|
1660
|
+
}
|
|
1661
|
+
return response.bodyObject.result;
|
|
1662
|
+
}
|
|
1663
|
+
async count(table, query) {
|
|
1664
|
+
const get = () => this.request().get({
|
|
1665
|
+
method: 'GET', path: `/api/now/stats/${table}`, headers: { Accept: 'application/json' }, body: null,
|
|
1666
|
+
query: { sysparm_query: query, sysparm_count: 'true' }, requires: READ_ONLY,
|
|
1667
|
+
});
|
|
1668
|
+
let response = await get();
|
|
1669
|
+
for (let attempt = 1; response?.status === 202 && attempt <= 3; attempt++) {
|
|
1670
|
+
await new Promise(resolve => setTimeout(resolve, 500 * attempt));
|
|
1671
|
+
response = await get();
|
|
1672
|
+
}
|
|
1673
|
+
const count = response?.bodyObject?.result?.stats?.count;
|
|
1674
|
+
if (response?.status !== 200 || count === undefined) {
|
|
1675
|
+
throw new Error(`Failed to count ${table}. Status: ${response?.status ?? 'unknown'}`);
|
|
1676
|
+
}
|
|
1677
|
+
return Number(count) || 0;
|
|
1678
|
+
}
|
|
1679
|
+
request() {
|
|
1680
|
+
return SessionManager.getInstance().getRequest(this._instance);
|
|
1681
|
+
}
|
|
1682
|
+
requireSysId(value, label) {
|
|
1683
|
+
if (!SYS_ID.test(value ?? ''))
|
|
1684
|
+
throw new InvalidParameterException(`${label} must be a 32-character sys_id`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function str(value) {
|
|
1688
|
+
if (typeof value === 'string')
|
|
1689
|
+
return value;
|
|
1690
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
1691
|
+
return String(value);
|
|
1692
|
+
if (value && typeof value === 'object' && 'value' in value)
|
|
1693
|
+
return str(value.value);
|
|
1694
|
+
return '';
|
|
1695
|
+
}
|
|
1696
|
+
function num(value) {
|
|
1697
|
+
return Number(str(value)) || 0;
|
|
1698
|
+
}
|
|
1699
|
+
function optNum(value) {
|
|
1700
|
+
const text = str(value);
|
|
1701
|
+
if (text === '')
|
|
1702
|
+
return undefined;
|
|
1703
|
+
const n = Number(text);
|
|
1704
|
+
return Number.isFinite(n) ? n : undefined;
|
|
1705
|
+
}
|
|
1706
|
+
/** A value embedded in an encoded query must not be able to add conditions of its own. */
|
|
1707
|
+
function queryValue(value, label) {
|
|
1708
|
+
if (/[\^\r\n]/.test(value))
|
|
1709
|
+
throw new InvalidParameterException(`The ${label} cannot contain '^' or line breaks`);
|
|
1710
|
+
return value;
|
|
1711
|
+
}
|
|
1712
|
+
function normalizeSpace(value) {
|
|
1713
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Whether a stored variable value is the variable's default. Booleans are stored as `1`/`0`
|
|
1717
|
+
* but defaulted as `true`/`false` (or empty).
|
|
1718
|
+
*/
|
|
1719
|
+
function isDefaultValue(def, value) {
|
|
1720
|
+
if (!def)
|
|
1721
|
+
return false;
|
|
1722
|
+
if (def.internalType === 'boolean') {
|
|
1723
|
+
const truth = (v) => ['1', 'true'].includes(String(v ?? '').trim().toLowerCase());
|
|
1724
|
+
return truth(def.defaultValue) === truth(value);
|
|
1725
|
+
}
|
|
1726
|
+
return normalizeSpace(def.defaultValue ?? '') === normalizeSpace(value);
|
|
1727
|
+
}
|
|
1728
|
+
function parseJsonObject(text) {
|
|
1729
|
+
try {
|
|
1730
|
+
const parsed = JSON.parse(text);
|
|
1731
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1732
|
+
return parsed;
|
|
1733
|
+
}
|
|
1734
|
+
catch {
|
|
1735
|
+
// fall through: keep the raw text
|
|
1736
|
+
}
|
|
1737
|
+
return { raw: text };
|
|
1738
|
+
}
|
|
1739
|
+
/** Map with at most `limit` calls in flight. */
|
|
1740
|
+
async function mapLimit(items, limit, fn) {
|
|
1741
|
+
const results = new Array(items.length);
|
|
1742
|
+
let next = 0;
|
|
1743
|
+
const worker = async () => {
|
|
1744
|
+
while (next < items.length) {
|
|
1745
|
+
const index = next++;
|
|
1746
|
+
results[index] = await fn(items[index]);
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1750
|
+
return results;
|
|
1751
|
+
}
|
|
1752
|
+
function newSysId() {
|
|
1753
|
+
return randomBytes(16).toString('hex');
|
|
1754
|
+
}
|
|
1755
|
+
function decodeEntities(text) {
|
|
1756
|
+
return text
|
|
1757
|
+
.replace(/ /g, ' ')
|
|
1758
|
+
.replace(/"/g, '"')
|
|
1759
|
+
.replace(/'/g, "'")
|
|
1760
|
+
.replace(/</g, '<')
|
|
1761
|
+
.replace(/>/g, '>')
|
|
1762
|
+
.replace(/&/g, '&');
|
|
1763
|
+
}
|
|
1764
|
+
/** The processor's validation text is a canned prompt; the report summary line says more. */
|
|
1765
|
+
function firstLine(message, items) {
|
|
1766
|
+
const summary = /Validate Summary[^\n]*/.exec(message)?.[0]?.replace(/\s+/g, ' ').trim();
|
|
1767
|
+
return summary || message.split('\n')[0] || `${items.length} findings`;
|
|
1768
|
+
}
|
|
1769
|
+
/** Parse Activity Designer `input_meta` / `output_meta` (a DATA_OBJECT with properties). */
|
|
1770
|
+
function designerFields(meta) {
|
|
1771
|
+
if (!meta)
|
|
1772
|
+
return [];
|
|
1773
|
+
try {
|
|
1774
|
+
const parsed = JSON.parse(meta);
|
|
1775
|
+
return (parsed.properties ?? []).map(p => ({ name: String(p.name ?? ''), type: String(p.type ?? ''), mandatory: p.mandatory === true }));
|
|
1776
|
+
}
|
|
1777
|
+
catch {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function toEdge(row) {
|
|
1782
|
+
return { id: str(row.sys_id), source: str(row.from), sourcePort: str(row.condition), target: str(row.to) };
|
|
1783
|
+
}
|
|
1784
|
+
function toCondition(row) {
|
|
1785
|
+
return {
|
|
1786
|
+
sysId: str(row.sys_id),
|
|
1787
|
+
activitySysId: str(row.activity),
|
|
1788
|
+
name: str(row.name),
|
|
1789
|
+
condition: str(row.condition),
|
|
1790
|
+
order: num(row.order),
|
|
1791
|
+
elseFlag: str(row.else_flag) === 'true',
|
|
1792
|
+
error: str(row.error) === 'true',
|
|
1793
|
+
event: str(row.event) === 'true',
|
|
1794
|
+
eventName: str(row.event_name),
|
|
1795
|
+
shortDescription: str(row.short_description),
|
|
1796
|
+
skipDuringGenerate: str(row.skip_during_generate) === 'true',
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function toDefinitionSummary(row) {
|
|
1800
|
+
return { sysId: str(row.sys_id), name: str(row.name), category: str(row.category), sysClassName: str(row.sys_class_name) };
|
|
1801
|
+
}
|
|
1802
|
+
function toVersionSummary(row) {
|
|
1803
|
+
return {
|
|
1804
|
+
sysId: str(row.sys_id),
|
|
1805
|
+
workflowSysId: str(row.workflow),
|
|
1806
|
+
name: str(row.name),
|
|
1807
|
+
table: str(row.table),
|
|
1808
|
+
published: str(row.published) === 'true',
|
|
1809
|
+
active: str(row.active) === 'true',
|
|
1810
|
+
checkedOut: str(row.checked_out),
|
|
1811
|
+
checkedOutBy: str(row.checked_out_by),
|
|
1812
|
+
createdOn: str(row.sys_created_on),
|
|
1813
|
+
updatedOn: str(row.sys_updated_on),
|
|
1814
|
+
};
|
|
387
1815
|
}
|
|
388
1816
|
//# sourceMappingURL=WorkflowManager.js.map
|