@jacebenson/jsn 4.3.7 → 4.3.8-dev.47
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 +266 -1
- package/package.json +1 -1
- package/skills/servicenow/SKILL.md +16 -0
- package/src/app.js +8 -1
- package/src/auth.js +14 -12
- package/src/cli-adapter.js +162 -11
- package/src/cli.js +121 -13
- package/src/commands/_generic.js +3 -2
- package/src/commands/_simple.js +1 -1
- package/src/commands/actions.js +156 -0
- package/src/commands/auth.js +21 -11
- package/src/commands/cmdb.js +18 -0
- package/src/commands/diff.js +205 -0
- package/src/commands/docs/docs.js +18 -16
- package/src/commands/flows.js +149 -47
- package/src/commands/graphql.js +95 -0
- package/src/commands/groupmembers.js +19 -4
- package/src/commands/grouproles.js +15 -4
- package/src/commands/midservers.js +708 -0
- package/src/commands/perf.js +36 -9
- package/src/commands/platform.js +202 -0
- package/src/commands/records.js +19 -15
- package/src/commands/skill.js +5 -2
- package/src/commands/transactions.js +78 -0
- package/src/commands/updatesets.js +77 -11
- package/src/errors.js +71 -6
- package/src/flow-create.js +884 -0
- package/src/flow-designer-interactive.js +417 -0
- package/src/flow-inspection.js +15 -5
- package/src/flow-publish.js +32 -0
- package/src/flow-test.js +122 -0
- package/src/flow-triggers.js +448 -0
- package/src/flow-versioning.js +53 -0
- package/src/help.js +93 -28
- package/src/helpers.js +124 -9
- package/src/midserver-direct.js +312 -0
- package/src/midserver-install.js +170 -0
- package/src/midserver-zip.js +107 -0
- package/src/output.js +7 -0
- package/src/processflow-actions.js +366 -0
- package/src/sdk.js +8 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// Terminal orchestration only. Persistence and lifecycle remain in the SDK.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
|
+
import { input, select, confirm, editor } from '@inquirer/prompts';
|
|
5
|
+
import { canPrompt, interactiveList, getStringField } from './helpers.js';
|
|
6
|
+
import { FormatAuto } from './output.js';
|
|
7
|
+
import { errUsage, errConfirmationRequired } from './errors.js';
|
|
8
|
+
import { getCurrentUser, getCurrentApplication } from './context.js';
|
|
9
|
+
import { validateDefinition, validateScope, ACTION_TABLE } from './processflow-actions.js';
|
|
10
|
+
import { validateFlowManifest, validateFlowUpdateManifest, readProcessFlow, readActionDefinition } from './flow-create.js';
|
|
11
|
+
import {
|
|
12
|
+
NUMERIC_TRIGGER_TYPES,
|
|
13
|
+
PROMPTABLE_TRIGGER_TYPES,
|
|
14
|
+
conditionInputFor,
|
|
15
|
+
readTableDescriptor,
|
|
16
|
+
readTriggerCatalog,
|
|
17
|
+
readTriggerInputDefinitions,
|
|
18
|
+
selectableTriggers,
|
|
19
|
+
tableInputFor,
|
|
20
|
+
triggerChoiceLabel,
|
|
21
|
+
} from './flow-triggers.js';
|
|
22
|
+
import { deriveRecordTrigger } from './flow-test.js';
|
|
23
|
+
import { getActiveProfile } from './config.js';
|
|
24
|
+
|
|
25
|
+
const CANCEL = Symbol('cancel');
|
|
26
|
+
const clone = value => structuredClone(value);
|
|
27
|
+
export const rawField = value => value && typeof value === 'object' && 'value' in value ? value.value : value;
|
|
28
|
+
const prompts = app => app.flowDesignerPrompts || { input, select, confirm, editor };
|
|
29
|
+
export function hasDefinitionData(argv) {
|
|
30
|
+
return argv.data !== undefined || argv['data-file'] !== undefined || argv.dataFile !== undefined;
|
|
31
|
+
}
|
|
32
|
+
export function interactiveDesigner(argv, app) {
|
|
33
|
+
return !hasDefinitionData(argv) && argv['output-map'] === undefined && !argv.get && app.output?.getFormat() === FormatAuto && canPrompt();
|
|
34
|
+
}
|
|
35
|
+
export async function cancelled(operation) {
|
|
36
|
+
try { return await operation(); }
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error === CANCEL || ['ExitPromptError', 'AbortPromptError'].includes(error.name)) return undefined;
|
|
39
|
+
// Keep direct handler errors throwable; opt into the shared JSON renderer at the CLI boundary.
|
|
40
|
+
if (error.code === 'usage_error') error.jsonEnvelope = true;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function pickDesignerRecord(argv, app, table, singular, query = '', labelField = 'name') {
|
|
45
|
+
if (argv.identifier !== undefined) {
|
|
46
|
+
if (!String(argv.identifier).trim()) throw errUsage(`${singular} identifier must not be empty`);
|
|
47
|
+
return argv.identifier;
|
|
48
|
+
}
|
|
49
|
+
if (!interactiveDesigner(argv, app)) throw errUsage(`Provide an explicit ${singular} identifier in noninteractive mode`);
|
|
50
|
+
const picked = await interactiveList({ app, table, singular, query, labelField,
|
|
51
|
+
columns: [labelField], promptFn: app.promptFn });
|
|
52
|
+
if (picked === null) throw errUsage(`No readable ${singular} records are available for selection`);
|
|
53
|
+
if (picked === undefined) return undefined;
|
|
54
|
+
const id = rawField(picked.sys_id);
|
|
55
|
+
if (!id) throw errUsage(`Selected ${singular} has no raw sys_id`);
|
|
56
|
+
return id;
|
|
57
|
+
}
|
|
58
|
+
export async function designerScope(argv, app) {
|
|
59
|
+
if (argv.scope !== undefined) return validateScope(app.sdk, argv.scope);
|
|
60
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --scope <exact sys_id or global> in noninteractive mode');
|
|
61
|
+
const user = await getCurrentUser(app.sdk);
|
|
62
|
+
const current = user ? await getCurrentApplication(app.sdk, user.sys_id) : null;
|
|
63
|
+
if (current?.appSysId && await prompts(app).confirm({ message: `Use current application scope ${current.appSysId}?`, default: true })) {
|
|
64
|
+
return validateScope(app.sdk, current.appSysId);
|
|
65
|
+
}
|
|
66
|
+
const scope = await pickDesignerRecord(argv, app, 'sys_scope', 'scope');
|
|
67
|
+
if (scope === undefined) throw CANCEL;
|
|
68
|
+
return validateScope(app.sdk, scope);
|
|
69
|
+
}
|
|
70
|
+
export async function confirmDesignerSave(app, message) {
|
|
71
|
+
if (!await prompts(app).confirm({ message, default: false })) throw CANCEL;
|
|
72
|
+
}
|
|
73
|
+
// Explicit JSON/data, pipes and disabled prompts never enter a TTY confirmation.
|
|
74
|
+
export async function confirmDesignerMutation(argv, app, message) {
|
|
75
|
+
if (argv.force) return;
|
|
76
|
+
if (interactiveDesigner(argv, app)) return confirmDesignerSave(app, message);
|
|
77
|
+
if (getActiveProfile(app.config)?.skip_confirmations === true) return;
|
|
78
|
+
throw errConfirmationRequired(message, ['jsn', ...(argv._ || [])].join(' '));
|
|
79
|
+
}
|
|
80
|
+
function required(value) { return String(value).trim() ? true : 'A value is required'; }
|
|
81
|
+
function scalarDefault(value) { return value == null ? '' : typeof value === 'string' ? value : JSON.stringify(value); }
|
|
82
|
+
function schemaObjectValue(field, value) {
|
|
83
|
+
return ['object', 'complex'].includes(field.type) && value && typeof value === 'object'
|
|
84
|
+
&& !('schemaless' in value) && field.children?.some(child => Object.hasOwn(value, child.name));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Preserve schema objects. Change only a requested value, never regenerate metadata.
|
|
88
|
+
export async function promptSchemaValue(app, field, existing, prefix = '') {
|
|
89
|
+
if (!field?.name || !field.type) throw errUsage('Provider returned an incomplete named, typed input schema');
|
|
90
|
+
const p = prompts(app);
|
|
91
|
+
const message = `${prefix}${field.label || field.name} [${field.type}]`;
|
|
92
|
+
const supplied = existing ?? field.defaultValue ?? field.default;
|
|
93
|
+
const value = schemaObjectValue(field, supplied) ? supplied : rawField(supplied);
|
|
94
|
+
if (field.type === 'script') {
|
|
95
|
+
const method = await p.select({ message: `${message}: script source`, choices: [
|
|
96
|
+
{ name: 'Script file', value: 'file' }, { name: 'Open $VISUAL/$EDITOR', value: 'editor' },
|
|
97
|
+
] });
|
|
98
|
+
const script = method === 'file'
|
|
99
|
+
? fs.readFileSync(await p.input({ message: 'Script file path', validate: required }), 'utf8')
|
|
100
|
+
: await p.editor({ message, default: scalarDefault(value), waitForUse: true });
|
|
101
|
+
if ((field.mandatory === true || field.required === true) && !script.trim()) throw errUsage(`Script ${field.name} is required; nothing was saved`);
|
|
102
|
+
return script;
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === 'string' && value.startsWith('{{')) {
|
|
105
|
+
return p.input({ message: `${message} (raw pill)`, default: value, validate: required });
|
|
106
|
+
}
|
|
107
|
+
if (field.children?.length && ['object', 'complex'].includes(field.type)) {
|
|
108
|
+
const result = value && typeof value === 'object' && !Array.isArray(value) ? clone(value) : {};
|
|
109
|
+
for (const child of field.children) result[child.name] = await promptSchemaValue(app, child, result[child.name], `${message}.`);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
if (['boolean', 'bool'].includes(field.type)) {
|
|
113
|
+
return p.confirm({ message, default: value === true || value === 'true' });
|
|
114
|
+
}
|
|
115
|
+
if (field.type === 'choice' && Array.isArray(field.choices) && field.choices.length) {
|
|
116
|
+
if (field.choices.some(choice => typeof choice !== 'string' && (!choice || !Object.hasOwn(choice, 'value')))) {
|
|
117
|
+
throw errUsage(`Choice input ${field.name} has incomplete provider choices; nothing was saved`);
|
|
118
|
+
}
|
|
119
|
+
return p.select({ message, default: value, choices: field.choices.map(choice => ({
|
|
120
|
+
name: typeof choice === 'string' ? choice : String(choice.label ?? choice.display_value ?? choice.value),
|
|
121
|
+
value: typeof choice === 'string' ? choice : choice.value,
|
|
122
|
+
})) });
|
|
123
|
+
}
|
|
124
|
+
const textual = ['string', 'reference', 'table_name', 'field_name', 'conditions', 'condition', 'html', 'url', 'email', 'choice', 'document_id'];
|
|
125
|
+
const numeric = ['integer', 'number', 'decimal', 'float'];
|
|
126
|
+
if (![...textual, ...numeric].includes(field.type)) {
|
|
127
|
+
throw errUsage(`Guided input ${field.name} has unsupported type ${field.type}; use --data-file to preserve its complete schema and value`);
|
|
128
|
+
}
|
|
129
|
+
const answer = await p.input({ message: `${message} (raw pills accepted)`, default: scalarDefault(value), validate: answer => {
|
|
130
|
+
if ((field.mandatory === true || field.required === true) && answer === '') return 'A value is required';
|
|
131
|
+
if (numeric.includes(field.type) && answer !== '' && !answer.startsWith('{{') && (!Number.isFinite(Number(answer)) || field.type === 'integer' && !Number.isInteger(Number(answer)))) return 'Enter a valid number or raw pill';
|
|
132
|
+
return true;
|
|
133
|
+
} });
|
|
134
|
+
// Pills are opaque provider expressions, not numbers to be coerced.
|
|
135
|
+
return numeric.includes(field.type) && answer !== '' && !answer.startsWith('{{') ? Number(answer) : answer;
|
|
136
|
+
}
|
|
137
|
+
async function editVariables(app, variables, message, valueKey = 'value') {
|
|
138
|
+
for (const field of variables.filter(field => !field.name.startsWith('__'))) {
|
|
139
|
+
if (await prompts(app).confirm({ message: `${message}: change ${field.name}?`, default: false })) {
|
|
140
|
+
const value = await promptSchemaValue(app, field, field[valueKey], `${message}: `);
|
|
141
|
+
const wrapped = field[valueKey] && typeof field[valueKey] === 'object' && 'value' in field[valueKey] && !schemaObjectValue(field, field[valueKey]);
|
|
142
|
+
field[valueKey] = wrapped ? { ...field[valueKey], value } : value;
|
|
143
|
+
if (valueKey === 'defaultValue') field.mandatory = await prompts(app).confirm({ message: `${field.name}: required?`, default: field.mandatory === true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function schemaTemplates(types) {
|
|
148
|
+
const templates = [];
|
|
149
|
+
const visit = fields => {
|
|
150
|
+
if (fields !== undefined && !Array.isArray(fields)) throw errUsage('Provider variable schema must be an array; no parent was allocated');
|
|
151
|
+
for (const field of fields || []) {
|
|
152
|
+
if (!field || typeof field !== 'object') throw errUsage('Provider variable schema requires objects; no parent was allocated');
|
|
153
|
+
if (field.name && field.type && !field.name.startsWith('__')) templates.push(field);
|
|
154
|
+
visit(field.children);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
for (const type of types) { visit(type.inputs); visit(type.outputs); visit(type.extended_inputs); visit(type.extended_outputs); }
|
|
158
|
+
return templates;
|
|
159
|
+
}
|
|
160
|
+
function freshVariable(template) {
|
|
161
|
+
const field = clone(template);
|
|
162
|
+
const renew = variable => {
|
|
163
|
+
variable.id = randomBytes(16).toString('hex');
|
|
164
|
+
if (variable.attributes?.uiUniqueId) variable.attributes.uiUniqueId = randomUUID();
|
|
165
|
+
for (const child of variable.children || []) renew(child);
|
|
166
|
+
};
|
|
167
|
+
renew(field);
|
|
168
|
+
return field;
|
|
169
|
+
}
|
|
170
|
+
async function newVariable(app, templates, message) {
|
|
171
|
+
const candidates = templates.filter(field => ['string', 'integer', 'boolean', 'reference', 'object'].includes(field.type));
|
|
172
|
+
if (!candidates.length) throw errUsage('Step schemas contain no supported variable templates; use --data-file');
|
|
173
|
+
const template = await prompts(app).select({ message: `${message}: select provider schema template`, choices: candidates.map(field => ({ name: `${field.name} [${field.type}]`, value: field })) });
|
|
174
|
+
const field = freshVariable(template);
|
|
175
|
+
field.name = await prompts(app).input({ message: `${message}: variable name`, validate: value => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) || 'Use a JavaScript variable name' });
|
|
176
|
+
field.label = field.name;
|
|
177
|
+
field.mandatory = await prompts(app).confirm({ message: `${field.name}: required?`, default: false });
|
|
178
|
+
delete field.value;
|
|
179
|
+
delete field.display_value;
|
|
180
|
+
return field;
|
|
181
|
+
}
|
|
182
|
+
export function stepTypeSchemas(response) {
|
|
183
|
+
const types = Array.isArray(response) ? response : response?.step_types ?? response?.stepTypes;
|
|
184
|
+
if (!Array.isArray(types) || !types.length) throw errUsage('Provider returned no supported step_types schema array; no parent was allocated');
|
|
185
|
+
return types;
|
|
186
|
+
}
|
|
187
|
+
export async function guidedActionCreate(argv, app) {
|
|
188
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for a full action definition in noninteractive mode');
|
|
189
|
+
const scope = await designerScope(argv, app);
|
|
190
|
+
const types = stepTypeSchemas(await app.sdk.getProcessFlowStepTypes(scope));
|
|
191
|
+
const definition = { scope, name: await prompts(app).input({ message: 'Action name', validate: required }),
|
|
192
|
+
description: await prompts(app).input({ message: 'Description' }), inputs: [], outputs: [], steps: [] };
|
|
193
|
+
const templates = schemaTemplates(types);
|
|
194
|
+
while (await prompts(app).confirm({ message: 'Add an action input?', default: false })) {
|
|
195
|
+
definition.inputs.push(await newVariable(app, templates, 'Action input'));
|
|
196
|
+
}
|
|
197
|
+
do {
|
|
198
|
+
const schema = await prompts(app).select({ message: 'Select executable step schema', choices: types.map(type => ({ name: type.name || type.step_type_name || type.type || type.step_type, value: type })) });
|
|
199
|
+
if (!(schema.type || schema.step_type) || !(schema.id || schema.step_type_id) || !Array.isArray(schema.inputs) || !Array.isArray(schema.outputs)) {
|
|
200
|
+
throw errUsage('Selected provider step schema is incomplete; no parent was allocated');
|
|
201
|
+
}
|
|
202
|
+
const step = { ...clone(schema), cid: randomUUID(), step_type: schema.step_type || schema.type,
|
|
203
|
+
step_type_id: schema.step_type_id || schema.id, order: definition.steps.length + 1,
|
|
204
|
+
inputs: clone(schema.inputs), outputs: clone(schema.outputs),
|
|
205
|
+
extended_inputs: clone(schema.extended_inputs || []), extended_outputs: clone(schema.extended_outputs || []) };
|
|
206
|
+
delete step.id;
|
|
207
|
+
delete step.step_id;
|
|
208
|
+
delete step.action;
|
|
209
|
+
for (const field of step.inputs) field.value = await promptSchemaValue(app, field, field.value);
|
|
210
|
+
if (step.step_type === 'SCRIPT') {
|
|
211
|
+
for (const field of definition.inputs) step.extended_inputs.push({ ...freshVariable(field), extended: true, value: `{{inputs.${field.name}}}` });
|
|
212
|
+
while (await prompts(app).confirm({ message: 'Declare a script step output?', default: false })) {
|
|
213
|
+
step.extended_outputs.push({ ...await newVariable(app, templates, 'Script output'), extended: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
definition.steps.push(step);
|
|
217
|
+
} while (await prompts(app).confirm({ message: 'Add another step?', default: false }));
|
|
218
|
+
const outputs = definition.steps.flatMap(step => [...step.outputs, ...step.extended_outputs]
|
|
219
|
+
.filter(field => !field.name.startsWith('__')).map(field => ({ step, field })));
|
|
220
|
+
while (await prompts(app).confirm({ message: 'Map a step output to an action output?', default: false })) {
|
|
221
|
+
if (!outputs.length) throw errUsage('No provider-declared step outputs available');
|
|
222
|
+
const { step, field } = await prompts(app).select({ message: 'Select output', choices: outputs.map(candidate => ({ name: `Step ${candidate.step.order}: ${candidate.field.name}`, value: candidate })) });
|
|
223
|
+
const output = { ...freshVariable(field), extended: false };
|
|
224
|
+
output.name = await prompts(app).input({ message: 'Action output name', default: field.name, validate: required });
|
|
225
|
+
output.value = `{{step[${step.cid}].${field.name}}}`;
|
|
226
|
+
definition.outputs.push(output);
|
|
227
|
+
}
|
|
228
|
+
validateDefinition(definition, undefined, scope);
|
|
229
|
+
await confirmDesignerSave(app, `Create inactive draft action '${definition.name}' with ${definition.steps.length} step(s)? No publish or execution.`);
|
|
230
|
+
return { scope, definition };
|
|
231
|
+
}
|
|
232
|
+
export async function guidedActionEdit(argv, app, definition) {
|
|
233
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for the full action definition in noninteractive mode');
|
|
234
|
+
validateDefinition(definition, definition.id, definition.scope);
|
|
235
|
+
const edited = clone(definition);
|
|
236
|
+
const fields = await prompts(app).select({ message: 'Edit action', choices: [
|
|
237
|
+
{ name: 'Name and description', value: 'metadata' }, { name: 'Action inputs', value: 'inputs' },
|
|
238
|
+
{ name: 'Action output mappings', value: 'outputs' }, { name: 'Step inputs, scripts and mappings', value: 'steps' },
|
|
239
|
+
] });
|
|
240
|
+
if (fields === 'metadata') {
|
|
241
|
+
edited.name = await prompts(app).input({ message: 'Action name', default: edited.name, validate: required });
|
|
242
|
+
if (Object.hasOwn(edited, 'displayName')) edited.displayName = edited.name;
|
|
243
|
+
edited.description = await prompts(app).input({ message: 'Description', default: edited.description || '' });
|
|
244
|
+
} else if (fields === 'steps') {
|
|
245
|
+
const step = await prompts(app).select({ message: 'Select saved step', choices: edited.steps.map(step => ({ name: `${step.order}: ${step.step_type}`, value: step })) });
|
|
246
|
+
await editVariables(app, step.inputs, 'Step input');
|
|
247
|
+
await editVariables(app, step.extended_inputs, 'Step input mapping');
|
|
248
|
+
await editVariables(app, step.extended_outputs, 'Step output');
|
|
249
|
+
} else await editVariables(app, edited[fields], fields, fields === 'inputs' ? 'defaultValue' : 'value');
|
|
250
|
+
validateDefinition(edited, definition.id, definition.scope);
|
|
251
|
+
await confirmDesignerSave(app, `Save action '${edited.name}'? Unspecified schema and lifecycle identities are preserved. No publish or execution.`);
|
|
252
|
+
return edited;
|
|
253
|
+
}
|
|
254
|
+
export async function guidedActionTestInputs(argv, app, definition) {
|
|
255
|
+
const values = {};
|
|
256
|
+
for (const field of definition.inputs) values[field.name] = await promptSchemaValue(app, field, field.defaultValue, 'Test input: ');
|
|
257
|
+
return values;
|
|
258
|
+
}
|
|
259
|
+
async function tableName(argv, app) {
|
|
260
|
+
const id = await pickDesignerRecord(argv, app, 'sys_db_object', 'trigger table');
|
|
261
|
+
if (id === undefined) throw CANCEL;
|
|
262
|
+
const table = await app.sdk.get('sys_db_object', id);
|
|
263
|
+
const name = rawField(table?.name);
|
|
264
|
+
if (!name) throw errUsage('Selected trigger table has no raw name');
|
|
265
|
+
return name;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Prompt for one declared trigger input. Choice lists are not exposed by
|
|
269
|
+
// sys_hub_trigger_input on this instance, and every supported family declares
|
|
270
|
+
// its choice/boolean inputs as optional with a default, so those keep the
|
|
271
|
+
// instance default instead of inventing options.
|
|
272
|
+
async function promptTriggerInput(app, input) {
|
|
273
|
+
if (!input?.name || !input.type) throw errUsage('Provider returned an incomplete named, typed trigger input');
|
|
274
|
+
if (input.type === 'choice') {
|
|
275
|
+
if (input.mandatory) throw errUsage(`Trigger input ${input.name} is a mandatory choice whose options this instance does not expose; supply it through --data-file`);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (input.type === 'boolean') return null;
|
|
279
|
+
if (!PROMPTABLE_TRIGGER_TYPES.has(input.type)) {
|
|
280
|
+
throw errUsage(`Trigger input ${input.name} has unsupported type ${input.type}; use --data-file to preserve its complete schema and value`);
|
|
281
|
+
}
|
|
282
|
+
const p = prompts(app);
|
|
283
|
+
const message = `Trigger input: ${input.label || input.name} [${input.type}]`;
|
|
284
|
+
const fallback = input.defaultValue === '' || input.defaultValue === undefined ? '' : String(input.defaultValue);
|
|
285
|
+
if (NUMERIC_TRIGGER_TYPES.has(input.type)) {
|
|
286
|
+
const answer = await p.input({ message, default: scalarDefault(fallback), validate: value => {
|
|
287
|
+
if (value === '' && input.mandatory) return 'A value is required';
|
|
288
|
+
if (value === '') return true;
|
|
289
|
+
return Number.isFinite(Number(value)) ? true : 'Enter a number';
|
|
290
|
+
} });
|
|
291
|
+
return answer === '' ? '' : Number(answer);
|
|
292
|
+
}
|
|
293
|
+
return p.input({ message, default: scalarDefault(fallback), validate: value => (input.mandatory && value === '' ? 'A value is required' : true) });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function guidedFlowCreate(argv, app) {
|
|
297
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for the flow manifest in noninteractive mode');
|
|
298
|
+
const name = await prompts(app).input({ message: 'Flow name', validate: required });
|
|
299
|
+
const description = await prompts(app).input({ message: 'Description' });
|
|
300
|
+
const scope = await designerScope(argv, app);
|
|
301
|
+
const manifest = { name, description, scope, trigger: { type: '' }, actions: [] };
|
|
302
|
+
|
|
303
|
+
// Category first, then the triggers that category actually contains, both
|
|
304
|
+
// read from the instance (sys_hub_trigger_category + the definition's own
|
|
305
|
+
// `category` reference). Definitions this CLI cannot create are never offered.
|
|
306
|
+
const catalog = await readTriggerCatalog(app.sdk);
|
|
307
|
+
const pickable = catalog.categories.filter(category => selectableTriggers(category).length);
|
|
308
|
+
if (!pickable.length) {
|
|
309
|
+
throw errUsage('No supported trigger categories are available on this instance; use --data-file to supply the manifest directly');
|
|
310
|
+
}
|
|
311
|
+
const chosenCategory = await prompts(app).select({
|
|
312
|
+
message: 'Select a trigger category',
|
|
313
|
+
choices: pickable.map(category => ({ name: `${category.name} (${selectableTriggers(category).length})`, value: category })),
|
|
314
|
+
});
|
|
315
|
+
const triggers = selectableTriggers(chosenCategory);
|
|
316
|
+
const definition = await prompts(app).select({
|
|
317
|
+
message: `Select a ${chosenCategory.name} trigger`,
|
|
318
|
+
choices: triggers.map(trigger => ({ name: triggerChoiceLabel(trigger), value: trigger })),
|
|
319
|
+
});
|
|
320
|
+
manifest.trigger.type = definition.type;
|
|
321
|
+
|
|
322
|
+
const declared = await readTriggerInputDefinitions(app.sdk, definition.sysId);
|
|
323
|
+
const tableInput = tableInputFor(declared);
|
|
324
|
+
if (tableInput) {
|
|
325
|
+
manifest.trigger.table = await tableName(argv, app);
|
|
326
|
+
}
|
|
327
|
+
const conditionInput = conditionInputFor(declared);
|
|
328
|
+
if (conditionInput) {
|
|
329
|
+
manifest.trigger.condition = await prompts(app).input({ message: 'Trigger condition (single supported encoded predicate, or empty)' });
|
|
330
|
+
}
|
|
331
|
+
const optional = declared.filter(input => !tableInput || input.name !== tableInput.name)
|
|
332
|
+
.filter(input => !conditionInput || input.name !== conditionInput.name);
|
|
333
|
+
if (optional.some(input => input.mandatory)) {
|
|
334
|
+
manifest.trigger.inputs = {};
|
|
335
|
+
for (const input of optional.filter(candidate => candidate.mandatory)) {
|
|
336
|
+
const value = await promptTriggerInput(app, input);
|
|
337
|
+
if (value !== null) manifest.trigger.inputs[input.name] = value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (optional.some(input => !input.mandatory && input.type !== 'choice' && input.type !== 'boolean')
|
|
341
|
+
&& await prompts(app).confirm({ message: 'Change optional trigger inputs (otherwise the instance defaults are used)?', default: false })) {
|
|
342
|
+
manifest.trigger.inputs = manifest.trigger.inputs || {};
|
|
343
|
+
for (const input of optional.filter(candidate => !candidate.mandatory)) {
|
|
344
|
+
if (input.type === 'choice' || input.type === 'boolean') continue;
|
|
345
|
+
if (!await prompts(app).confirm({ message: `Change ${input.label || input.name} [${input.type}]?`, default: false })) continue;
|
|
346
|
+
const value = await promptTriggerInput(app, input);
|
|
347
|
+
if (value !== null) manifest.trigger.inputs[input.name] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (manifest.trigger.inputs && !Object.keys(manifest.trigger.inputs).length) delete manifest.trigger.inputs;
|
|
351
|
+
|
|
352
|
+
while (await prompts(app).confirm({ message: 'Add a published action?', default: true })) {
|
|
353
|
+
const id = await pickDesignerRecord(argv, app, ACTION_TABLE, 'published action', 'active=true^published=true');
|
|
354
|
+
if (id === undefined) throw CANCEL;
|
|
355
|
+
const { schema } = await readActionDefinition(app.sdk, app.getEffectiveInstance(), id, manifest.scope, { requireTypes: true });
|
|
356
|
+
const inputs = {};
|
|
357
|
+
for (const field of schema) {
|
|
358
|
+
if (!field.id || !field.name) {
|
|
359
|
+
throw errUsage(`Published action input ${field.name || 'unknown'} lacks the provider identity needed to bind a value; no flow was allocated`);
|
|
360
|
+
}
|
|
361
|
+
inputs[field.name] = await promptSchemaValue(app, field, field.default, 'Action input: ');
|
|
362
|
+
}
|
|
363
|
+
manifest.actions.push({ type: id, inputs });
|
|
364
|
+
}
|
|
365
|
+
validateFlowManifest(manifest);
|
|
366
|
+
await confirmDesignerSave(app, `Create inactive draft flow '${manifest.name}' with trigger ${definition.name} and ${manifest.actions.length} action(s)? No publish or execution.`);
|
|
367
|
+
return manifest;
|
|
368
|
+
}
|
|
369
|
+
export async function savedFlowDefinition(app, identifier) {
|
|
370
|
+
const { resolveRecord, unwrapSysId } = await import('./resolve-record.js');
|
|
371
|
+
const record = await resolveRecord(app.sdk, { table: 'sys_hub_flow', identifier, matchField: 'name', resource: 'flow' });
|
|
372
|
+
const id = unwrapSysId(record);
|
|
373
|
+
const scope = rawField(record.scope);
|
|
374
|
+
if (!scope) throw errUsage('Saved flow has no raw transaction scope');
|
|
375
|
+
return { record, definition: await readProcessFlow(app.sdk, app.getEffectiveInstance(), id, scope) };
|
|
376
|
+
}
|
|
377
|
+
export async function guidedFlowEdit(argv, app) {
|
|
378
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for the flow update manifest in noninteractive mode');
|
|
379
|
+
const { record, definition } = await savedFlowDefinition(app, argv.identifier);
|
|
380
|
+
if (rawField(record.active) === true || rawField(record.active) === 'true' || rawField(record.status) !== 'draft') throw errUsage('Guided flow edit requires an inactive Draft');
|
|
381
|
+
const manifest = {};
|
|
382
|
+
const choice = await prompts(app).select({ message: 'Edit supported draft flow fields', choices: [
|
|
383
|
+
{ name: 'Record trigger condition', value: 'trigger' }, { name: 'Existing action input values', value: 'actions' },
|
|
384
|
+
] });
|
|
385
|
+
if (choice === 'trigger') {
|
|
386
|
+
const trigger = deriveRecordTrigger(definition).trigger;
|
|
387
|
+
const condition = (trigger.inputs || []).find(field => field.name === 'condition');
|
|
388
|
+
manifest.trigger = { condition: await prompts(app).input({ message: 'Trigger condition (single supported encoded predicate, or empty)', default: rawField(condition?.value) || '' }) };
|
|
389
|
+
} else {
|
|
390
|
+
const actions = definition.actionInstances || definition.actions || [];
|
|
391
|
+
if (!actions.length) throw errUsage('Saved flow contains no supported action instances');
|
|
392
|
+
const action = await prompts(app).select({ message: 'Select saved action instance', choices: actions.map(action => ({ name: `${action.order}: ${getStringField(action, 'name') || action.actionTypeSysId}`, value: action })) });
|
|
393
|
+
const fields = action.inputs || action.inputValues || [];
|
|
394
|
+
if (!fields.length) throw errUsage('Saved action instance contains no input schema');
|
|
395
|
+
const field = await prompts(app).select({ message: 'Select input to change', choices: fields.map(field => ({ name: `${field.name} [${field.type || field.parameter?.type || 'unknown'}]`, value: field })) });
|
|
396
|
+
const actionType = rawField(action.actionTypeSysId || action.action_type_sys_id || action.actionType?.sys_id || action.actionType?.sysId);
|
|
397
|
+
let inputSchema = { ...field, type: field.type || field.parameter?.type };
|
|
398
|
+
if (!inputSchema.type) {
|
|
399
|
+
const { schema } = await readActionDefinition(app.sdk, app.getEffectiveInstance(), actionType, rawField(record.scope), { requireTypes: true });
|
|
400
|
+
inputSchema = schema.find(candidate => candidate.name === field.name);
|
|
401
|
+
if (!inputSchema) throw errUsage(`Saved action input ${field.name} is absent from its provider schema`);
|
|
402
|
+
}
|
|
403
|
+
const value = await promptSchemaValue(app, inputSchema, rawField(field.value));
|
|
404
|
+
manifest.actions = [{ order: Number(action.order), type: actionType, inputs: { [field.name]: value } }];
|
|
405
|
+
}
|
|
406
|
+
validateFlowUpdateManifest(manifest);
|
|
407
|
+
return manifest;
|
|
408
|
+
}
|
|
409
|
+
export async function guidedFlowTestRecord(argv, app) {
|
|
410
|
+
if (argv.record !== undefined) return argv.record;
|
|
411
|
+
if (!interactiveDesigner(argv, app)) throw errUsage('Provide --record <sys_id> in noninteractive mode');
|
|
412
|
+
const { definition } = await savedFlowDefinition(app, argv.identifier);
|
|
413
|
+
const { table } = deriveRecordTrigger(definition);
|
|
414
|
+
const descriptor = await readTableDescriptor(app.sdk, table);
|
|
415
|
+
const { identifier: _identifier, ...pickerArgs } = argv;
|
|
416
|
+
return pickDesignerRecord(pickerArgs, app, table, `test record in ${table}`, '', descriptor.displayField);
|
|
417
|
+
}
|
package/src/flow-inspection.js
CHANGED
|
@@ -298,11 +298,7 @@ function buildLabelCache(payload) {
|
|
|
298
298
|
if (!payload || typeof payload !== 'object') return cache;
|
|
299
299
|
let entries = payload.label_cache;
|
|
300
300
|
if (!Array.isArray(entries)) {
|
|
301
|
-
|
|
302
|
-
entries = JSON.parse(payload.labelCacheAsJsonString || '[]');
|
|
303
|
-
} catch {
|
|
304
|
-
entries = [];
|
|
305
|
-
}
|
|
301
|
+
entries = parseLabelCacheJson(payload.labelCacheAsJsonString);
|
|
306
302
|
}
|
|
307
303
|
for (const e of entries) {
|
|
308
304
|
if (!e || typeof e !== 'object') continue;
|
|
@@ -313,6 +309,20 @@ function buildLabelCache(payload) {
|
|
|
313
309
|
return cache;
|
|
314
310
|
}
|
|
315
311
|
|
|
312
|
+
// labelCacheAsJsonString is a JSON document, and JSON `null` or `{}` parses
|
|
313
|
+
// successfully into something that is not iterable — that threw
|
|
314
|
+
// "entries is not iterable" and broke `flows show` on real drafts whose cache is
|
|
315
|
+
// empty. An empty cache simply has no labels.
|
|
316
|
+
function parseLabelCacheJson(value) {
|
|
317
|
+
if (typeof value !== 'string' || !value.trim()) return [];
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(value);
|
|
320
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
321
|
+
} catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
316
326
|
const GUID_PILL_RE = /\{\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.([^}]+)\}\}/g;
|
|
317
327
|
|
|
318
328
|
/**
|
package/src/flow-publish.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { decodeGzipJson } from './sdk.js';
|
|
17
|
+
import { createVerifiedFlowVersion } from './flow-versioning.js';
|
|
17
18
|
|
|
18
19
|
const ACTIVATE_PATH = '/api/now/wfa_fluent/activate_flows';
|
|
20
|
+
const PROCESSFLOW_PATH = '/api/now/processflow/flow';
|
|
19
21
|
|
|
20
22
|
// The endpoint returns 200 (all published), 207 (partial) or 422 (all failed).
|
|
21
23
|
// 422 is NOT an error case -- it carries a per-flow result body that is the
|
|
@@ -75,6 +77,36 @@ export async function publishFlows(sdk, flowIDs = [], actionIDs = []) {
|
|
|
75
77
|
throw new Error(`Publish failed (HTTP ${resp.status}): ${message}`);
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/** Publish one flow through the ProcessFlow snapshot/version lifecycle. */
|
|
81
|
+
export async function publishFlowLifecycle(sdk, instance, flowId) {
|
|
82
|
+
const flowRecord = sdk.get ? await sdk.get('sys_hub_flow', flowId) : null;
|
|
83
|
+
const scope = str(flowRecord, 'scope');
|
|
84
|
+
if (!scope) throw new Error(`Flow ${flowId} has no transaction scope; refusing to publish`);
|
|
85
|
+
const definitionResponse = await sdk.request(`${instance}${PROCESSFLOW_PATH}/${encodeURIComponent(flowId)}?sysparm_transaction_scope=${encodeURIComponent(scope)}`, { method: 'GET' });
|
|
86
|
+
const definition = definitionResponse?.result?.data ?? definitionResponse?.data?.result?.data ?? definitionResponse?.data ?? definitionResponse?.result ?? definitionResponse;
|
|
87
|
+
if (!definition || typeof definition !== 'object') throw new Error(`Flow definition could not be read for ${flowId}`);
|
|
88
|
+
const definitionScope = str(definition, 'scope');
|
|
89
|
+
if (definitionScope && definitionScope !== scope) throw new Error(`Flow definition scope did not match saved flow ${flowId}`);
|
|
90
|
+
const snapshot = await sdk.request(`${instance}${PROCESSFLOW_PATH}/${encodeURIComponent(flowId)}/snapshot`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: JSON.stringify(definition),
|
|
93
|
+
});
|
|
94
|
+
if (snapshot?.errors?.length || snapshot?.result?.errorMessage) throw new Error(`Flow snapshot failed for ${flowId}`);
|
|
95
|
+
const { version } = await createVerifiedFlowVersion(sdk, {
|
|
96
|
+
instance, flowId, scope, type: 'Activate/Publish', annotation: 'JSN flow publish',
|
|
97
|
+
});
|
|
98
|
+
const activeFlow = sdk.get ? await sdk.get('sys_hub_flow', flowId) : null;
|
|
99
|
+
const active = String(activeFlow?.active?.value ?? activeFlow?.active ?? '').toLowerCase() === 'true';
|
|
100
|
+
if (!active) throw new Error(`Flow ${flowId} publish did not persist active=true`);
|
|
101
|
+
return {
|
|
102
|
+
flow_id: flowId,
|
|
103
|
+
active,
|
|
104
|
+
snapshot: snapshot?.result ?? snapshot,
|
|
105
|
+
version,
|
|
106
|
+
lifecycle: 'Activate/Publish',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
/**
|
|
79
111
|
* Fallback: call FlowDesignerUtils.publishAll() through a background script.
|
|
80
112
|
* Same code path the REST endpoint uses internally.
|
package/src/flow-test.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { assertSafeExactMatch } from './helpers.js';
|
|
2
|
+
import { RECORD_TRIGGER_FAMILY, triggerFamily } from './flow-triggers.js';
|
|
3
|
+
|
|
4
|
+
const FLOW_ENDPOINT = '/api/now/processflow/flow';
|
|
5
|
+
|
|
6
|
+
function valueOf(value) {
|
|
7
|
+
if (value && typeof value === 'object') return String(value.value ?? value.sys_id ?? value.display_value ?? '');
|
|
8
|
+
return value == null ? '' : String(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findArray(value, key) {
|
|
12
|
+
if (!value || typeof value !== 'object') return [];
|
|
13
|
+
if (Array.isArray(value[key])) return value[key];
|
|
14
|
+
for (const item of Array.isArray(value) ? value : Object.values(value)) {
|
|
15
|
+
const found = findArray(item, key);
|
|
16
|
+
if (found.length) return found;
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function flowData(response) {
|
|
22
|
+
return response?.result?.data ?? response?.data?.result?.data ?? response?.data ?? response?.result ?? response;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findTrigger(definition) {
|
|
26
|
+
return findArray(definition, 'triggerInstances')[0] || findArray(definition, 'trigger_instances')[0] || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inputValue(trigger, name) {
|
|
30
|
+
const input = (trigger?.inputs || trigger?.triggerInputs || []).find(item => item?.name === name);
|
|
31
|
+
return valueOf(input?.value ?? input?.displayValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function deriveRecordTrigger(definition) {
|
|
35
|
+
const trigger = findTrigger(definition);
|
|
36
|
+
if (!trigger) throw new Error('Flow definition has no trigger instance');
|
|
37
|
+
// `type` is the family; `triggerType` is only the picker category ("Record").
|
|
38
|
+
// triggerFamily keeps a deliberate legacy fallback for saved instances that
|
|
39
|
+
// predate the split.
|
|
40
|
+
const type = triggerFamily(trigger);
|
|
41
|
+
if (type !== RECORD_TRIGGER_FAMILY) throw new Error(`Flow test supports only ${RECORD_TRIGGER_FAMILY} triggers; found ${type || 'unknown'}`);
|
|
42
|
+
const table = inputValue(trigger, 'table');
|
|
43
|
+
if (!table) throw new Error('Flow definition record trigger has no table');
|
|
44
|
+
return { trigger, type, table };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function booleanFlag(value) {
|
|
48
|
+
if (value === true || value === 'true') return true;
|
|
49
|
+
if (value === false || value === 'false') return false;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function classifyResponse(response) {
|
|
54
|
+
const data = flowData(response);
|
|
55
|
+
const contextId = data?.contextId || data?.context_id || data?.sys_id || data?.execution_id || '';
|
|
56
|
+
const state = valueOf(data?.state ?? data?.status).toLowerCase();
|
|
57
|
+
const succeeded = ['success', 'succeeded'].includes(state);
|
|
58
|
+
const failed = ['failed', 'failure', 'error', 'cancelled', 'canceled'].includes(state);
|
|
59
|
+
const completed = booleanFlag(data?.completed) === true
|
|
60
|
+
|| booleanFlag(data?.finished) === true
|
|
61
|
+
|| succeeded
|
|
62
|
+
|| failed
|
|
63
|
+
|| ['complete', 'completed'].includes(state);
|
|
64
|
+
const explicitSuccess = booleanFlag(data?.success ?? data?.successful);
|
|
65
|
+
const success = failed || explicitSuccess === false
|
|
66
|
+
? false
|
|
67
|
+
: completed && (succeeded || explicitSuccess === true) ? true : undefined;
|
|
68
|
+
return {
|
|
69
|
+
accepted: true,
|
|
70
|
+
status: completed ? 'completed' : 'accepted',
|
|
71
|
+
success,
|
|
72
|
+
context_id: contextId || undefined,
|
|
73
|
+
response: data,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function testFlow(sdk, instance, identifier, recordId) {
|
|
78
|
+
if (!recordId || !String(recordId).trim()) throw new Error('Flow test requires --record <sys_id>');
|
|
79
|
+
if (!/^[0-9a-f]{32}$/i.test(String(recordId))) throw new Error(`--record must be a 32-character sys_id; flow was not dispatched`);
|
|
80
|
+
const flowIdentifier = String(identifier);
|
|
81
|
+
assertSafeExactMatch(flowIdentifier);
|
|
82
|
+
const isId = /^[0-9a-f]{32}$/i.test(flowIdentifier);
|
|
83
|
+
const flowRows = await sdk.list('sys_hub_flow', new URLSearchParams({
|
|
84
|
+
sysparm_query: `${isId ? 'sys_id' : 'name'}=${flowIdentifier}`,
|
|
85
|
+
sysparm_limit: '2',
|
|
86
|
+
sysparm_display_value: 'all',
|
|
87
|
+
sysparm_fields: 'sys_id,name,scope',
|
|
88
|
+
}));
|
|
89
|
+
if (!flowRows.length) throw new Error(`Flow not found: ${identifier}`);
|
|
90
|
+
if (flowRows.length > 1) throw new Error(`Flow identifier is ambiguous: ${identifier}`);
|
|
91
|
+
const flow = flowRows[0];
|
|
92
|
+
const flowId = valueOf(flow.sys_id);
|
|
93
|
+
const scope = valueOf(flow.scope);
|
|
94
|
+
if (!scope) throw new Error(`Flow ${flowId} has no transaction scope; flow was not dispatched`);
|
|
95
|
+
const response = await sdk.request(`${instance}${FLOW_ENDPOINT}/${encodeURIComponent(flowId)}?sysparm_transaction_scope=${encodeURIComponent(scope)}`, { method: 'GET' });
|
|
96
|
+
const definition = flowData(response);
|
|
97
|
+
const trigger = deriveRecordTrigger(definition);
|
|
98
|
+
const records = await sdk.list(trigger.table, new URLSearchParams({
|
|
99
|
+
sysparm_query: `sys_id=${recordId}`,
|
|
100
|
+
sysparm_limit: '1',
|
|
101
|
+
sysparm_fields: 'sys_id',
|
|
102
|
+
}));
|
|
103
|
+
if (!records.length) throw new Error(`Record ${recordId} was not found in trigger table ${trigger.table}; flow was not dispatched`);
|
|
104
|
+
const definitionScope = valueOf(definition.scope);
|
|
105
|
+
if (definitionScope && definitionScope !== scope) throw new Error(`Flow ${flowId} definition scope did not match the saved record; flow was not dispatched`);
|
|
106
|
+
const body = {
|
|
107
|
+
...definition,
|
|
108
|
+
outputMap: { current: recordId, table_name: trigger.table },
|
|
109
|
+
runOnThread: true,
|
|
110
|
+
tracingEnabled: false,
|
|
111
|
+
};
|
|
112
|
+
let dispatch;
|
|
113
|
+
try {
|
|
114
|
+
dispatch = await sdk.request(`${instance}${FLOW_ENDPOINT}/${encodeURIComponent(flowId)}/test?sysparm_transaction_scope=${encodeURIComponent(scope)}`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
body: JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Flow test dispatch failed for ${flowId}: ${error.message}`, { cause: error });
|
|
120
|
+
}
|
|
121
|
+
return { flow: { id: flowId, name: valueOf(flow.name) }, trigger: { type: trigger.type, table: trigger.table }, record: recordId, ...classifyResponse(dispatch) };
|
|
122
|
+
}
|