@jacebenson/jsn 4.3.7 → 4.3.8-dev.46
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,366 @@
|
|
|
1
|
+
// Resource-specific Flow Designer contract. No Table API definition updates.
|
|
2
|
+
import { AppError, errUsage } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export const ACTION_TABLE = 'sys_hub_action_type_definition';
|
|
5
|
+
const ROOT = '/api/now/processflow/action';
|
|
6
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const raw = value => object(value) ? value.value : value;
|
|
8
|
+
const successCode = value => value === 0 || value === '0';
|
|
9
|
+
function token(value, label, global = false) {
|
|
10
|
+
if (typeof value !== 'string' || (!/^[a-fA-F0-9]{32}$/.test(value) && !(global && value === 'global'))) {
|
|
11
|
+
throw errUsage(`${label} must be an exact sys_id${global ? ' or global' : ''}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function failure(code, message, details = {}) {
|
|
16
|
+
const error = new AppError(code, message);
|
|
17
|
+
error.details = details;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
async function request(sdk, id, scope, suffix = '', method = 'GET', body, opts = {}) {
|
|
21
|
+
if (id !== null) token(id, 'Action ID');
|
|
22
|
+
token(scope, 'Transaction scope', true);
|
|
23
|
+
const resource = id === null ? '/step_types' : `/action_types/${encodeURIComponent(id)}${suffix}`;
|
|
24
|
+
const query = new URLSearchParams({ sysparm_transaction_scope: scope });
|
|
25
|
+
const response = await sdk.request(`${sdk.baseURL}${ROOT}${resource}?${query}`, {
|
|
26
|
+
...opts, method, ...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
27
|
+
});
|
|
28
|
+
const result = response?.result;
|
|
29
|
+
if (response?.error || result?.error || (result?.errorCode !== undefined && !successCode(result.errorCode))) {
|
|
30
|
+
throw failure('action_api_error', result?.errorMessage || response?.error?.message || 'Process Flow request failed');
|
|
31
|
+
}
|
|
32
|
+
if (result === undefined || result === null) throw failure('action_api_error', 'Process Flow returned no result');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
async function metadata(sdk, id, scope, opts) {
|
|
36
|
+
const result = await request(sdk, id, scope, '', 'GET', undefined, opts);
|
|
37
|
+
if (!object(result) || result.id !== id || result.scope !== scope) {
|
|
38
|
+
throw failure('action_identity_mismatch', 'Process Flow definition ID/scope does not match the target', { sys_id: id, scope });
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
export async function getAction(sdk, id, scope, opts = {}) {
|
|
43
|
+
const definition = await metadata(sdk, id, scope, opts);
|
|
44
|
+
const result = await request(sdk, id, scope, '/step_instances', 'GET', undefined, opts);
|
|
45
|
+
const steps = Array.isArray(result) ? result : result.steps;
|
|
46
|
+
if (!Array.isArray(steps)) throw failure('action_definition_incomplete', 'step_instances returned no steps array');
|
|
47
|
+
return { ...definition, steps };
|
|
48
|
+
}
|
|
49
|
+
export function getStepTypes(sdk, scope) {
|
|
50
|
+
return request(sdk, null, scope);
|
|
51
|
+
}
|
|
52
|
+
export async function validateScope(sdk, scope) {
|
|
53
|
+
token(scope, 'Transaction scope', true);
|
|
54
|
+
const record = await sdk.get('sys_scope', scope);
|
|
55
|
+
if (!record || raw(record.sys_id) !== scope) throw errUsage(`Scope does not exist or is not readable: ${scope}`);
|
|
56
|
+
return scope;
|
|
57
|
+
}
|
|
58
|
+
export async function actionScope(sdk, id, providedScope) {
|
|
59
|
+
token(id, 'Action ID');
|
|
60
|
+
if (providedScope !== undefined) token(providedScope, 'Transaction scope', true);
|
|
61
|
+
const record = await sdk.get(ACTION_TABLE, id);
|
|
62
|
+
if (!record || raw(record.sys_id) !== id) throw errUsage(`Action does not exist or is not readable: ${id}`);
|
|
63
|
+
const scope = raw(record.sys_scope);
|
|
64
|
+
token(scope, 'Action record scope', true);
|
|
65
|
+
if (providedScope !== undefined && providedScope !== scope) {
|
|
66
|
+
throw errUsage(`--scope ${providedScope} does not match action scope ${scope}`);
|
|
67
|
+
}
|
|
68
|
+
await validateScope(sdk, scope);
|
|
69
|
+
return scope;
|
|
70
|
+
}
|
|
71
|
+
function validateVariables(list, label) {
|
|
72
|
+
if (list.some(v => !object(v) || typeof v.name !== 'string' || !v.name || typeof v.type !== 'string' || !v.type)) {
|
|
73
|
+
throw errUsage(`${label} requires named, typed variable objects`);
|
|
74
|
+
}
|
|
75
|
+
if (new Set(list.map(v => v.name)).size !== list.length) throw errUsage(`${label} has duplicate variable names`);
|
|
76
|
+
}
|
|
77
|
+
export function validateDefinition(definition, id, scope) {
|
|
78
|
+
if (!object(definition)) throw errUsage('Expected a full action definition JSON object');
|
|
79
|
+
for (const key of ['inputs', 'outputs', 'steps']) {
|
|
80
|
+
if (!Array.isArray(definition[key])) throw errUsage(`Full definition requires an explicit ${key} array`);
|
|
81
|
+
}
|
|
82
|
+
validateVariables(definition.inputs, 'Action inputs');
|
|
83
|
+
validateVariables(definition.outputs, 'Action outputs');
|
|
84
|
+
if (!definition.steps.length) throw errUsage('Full definition requires at least one executable step');
|
|
85
|
+
if (id !== undefined && definition.id !== id) throw errUsage('Definition ID must match the target action ID');
|
|
86
|
+
if (scope !== undefined && definition.scope !== undefined && definition.scope !== scope) {
|
|
87
|
+
throw errUsage('Definition scope must match the target transaction scope');
|
|
88
|
+
}
|
|
89
|
+
for (const step of definition.steps) {
|
|
90
|
+
if (!object(step) || typeof step.cid !== 'string' || !step.cid || !step.step_type) {
|
|
91
|
+
throw errUsage('Each step requires cid and step_type from its Process Flow schema');
|
|
92
|
+
}
|
|
93
|
+
for (const key of ['inputs', 'outputs', 'extended_inputs', 'extended_outputs']) {
|
|
94
|
+
if (!Array.isArray(step[key])) throw errUsage(`Each step requires an explicit ${key} array`);
|
|
95
|
+
validateVariables(step[key], `Step ${key}`);
|
|
96
|
+
}
|
|
97
|
+
if (id !== undefined && step.action && step.action !== id) throw errUsage('Step action ID must match the target');
|
|
98
|
+
}
|
|
99
|
+
if (new Set(definition.steps.map(s => s.cid)).size !== definition.steps.length) throw errUsage('Step CIDs must be unique');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check executable intent, not server-assigned record/UI IDs or display caches.
|
|
103
|
+
const VARIABLE_FIELDS = ['name', 'type', 'value', 'defaultValue', 'mandatory', 'reference', 'data_structure', 'scriptActive', 'script', 'scriptAsJsonString', 'children', 'complexObjectValue', 'maxsize', 'ref_qual', 'choices', 'defaultChoices', 'choiceOption', 'table', 'dependent_on', 'sys_class_name', 'extended', 'local', 'dynamic'];
|
|
104
|
+
function subset(expected, actual, path, mismatches) {
|
|
105
|
+
if (Array.isArray(expected)) {
|
|
106
|
+
if (!Array.isArray(actual) || expected.length !== actual.length) { mismatches.push(path); return; }
|
|
107
|
+
expected.forEach((v, i) => subset(v, actual[i], `${path}[${i}]`, mismatches));
|
|
108
|
+
} else if (object(expected)) {
|
|
109
|
+
if (!object(actual)) { mismatches.push(path); return; }
|
|
110
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
111
|
+
if (key === 'uiUniqueId' || (typeof expected.name === 'string' && ['id', 'displayValue', 'display_value'].includes(key))) continue;
|
|
112
|
+
subset(value, actual[key], `${path}.${key}`, mismatches);
|
|
113
|
+
}
|
|
114
|
+
} else if (expected !== actual) mismatches.push(path);
|
|
115
|
+
}
|
|
116
|
+
// Only these attribute keys are UI/generated metadata, not schema constraints.
|
|
117
|
+
const UI_ATTRIBUTES = new Set(['uiUniqueId', 'uiTypeLabel', 'uiType', 'element_mapping_provider']);
|
|
118
|
+
function semantic(value) {
|
|
119
|
+
if (Array.isArray(value)) return value.map(semantic);
|
|
120
|
+
if (object(value)) return Object.fromEntries(Object.keys(value).sort().map(k => [k, semantic(value[k])]));
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function variableField(variable, field) {
|
|
124
|
+
const value = variable[field];
|
|
125
|
+
if (field === 'children') return (value || []).map(variableIntent);
|
|
126
|
+
if (field === 'mandatory' || field === 'scriptActive') return value ?? false;
|
|
127
|
+
// Process Flow materializes empty defaults. Nonempty mappings/scripts never disappear.
|
|
128
|
+
if (value === undefined || value === null || value === '' || (object(value) && !Object.keys(value).length)) return null;
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function variableIntent(variable) {
|
|
132
|
+
return { ...Object.fromEntries(VARIABLE_FIELDS.map(f => [f, variableField(variable, f)])),
|
|
133
|
+
attributes: Object.fromEntries(Object.entries(variable.attributes || {}).filter(([k]) => !UI_ATTRIBUTES.has(k))) };
|
|
134
|
+
}
|
|
135
|
+
function equalIntent(expected, actual) {
|
|
136
|
+
return JSON.stringify(semantic(expected)) === JSON.stringify(semantic(actual));
|
|
137
|
+
}
|
|
138
|
+
function lifecycleDefault(variable, found) {
|
|
139
|
+
const normalized = { ...variable };
|
|
140
|
+
if (!variable.scriptActive && variable.script === null && equalIntent(found.script, {})) normalized.script = {};
|
|
141
|
+
if (variable.name === '__dont_treat_as_error__' && variable.type === 'boolean' && variable.value === false && !variable.scriptActive && variable.maxsize === 0 && found.maxsize === 40) normalized.maxsize = 40;
|
|
142
|
+
if (variable.name !== '__action_status__' || variable.type !== 'object' || variable.value !== '' || variable.complexObjectValue !== null || variable.scriptActive) return normalized;
|
|
143
|
+
// Captured fresh status becomes a serialized EMPTY code/message object on PUT.
|
|
144
|
+
try {
|
|
145
|
+
const value = JSON.parse(found.value);
|
|
146
|
+
const empty = { code: { $cv: { $c: 'java.lang.String', $v: '' } }, message: { $cv: { $c: 'java.lang.String', $v: '' } } };
|
|
147
|
+
const schema = value.complexObjectSchema?.['FlowDesigner:FDACTIONSTATUS'];
|
|
148
|
+
const facetKeys = ['uiTypeLabel', 'read_only', 'hint', 'uiType', 'default_value', 'label', 'action_error_output', 'mandatory', 'order', 'max_length', 'uiUniqueId', 'co_type_name', 'element_mapping_provider'];
|
|
149
|
+
const emptyFacet = (facet, size, type) => {
|
|
150
|
+
if (!object(facet) || !equalIntent(Object.keys(facet), ['SimpleMapFacet'])) return false;
|
|
151
|
+
const fields = JSON.parse(facet.SimpleMapFacet);
|
|
152
|
+
return Object.keys(fields).every(k => facetKeys.includes(k)) && fields.mandatory === 'false' && fields.default_value === '' && fields.max_length === String(size) && fields.uiType === type && fields.action_error_output === 'true';
|
|
153
|
+
};
|
|
154
|
+
const defaultSchema = equalIntent(Object.keys(value.complexObjectSchema || {}).sort(), ['FlowDesigner:FDACTIONSTATUS', 'FlowDesigner:FDACTIONSTATUS.$type_facets'])
|
|
155
|
+
&& equalIntent(Object.keys(schema || {}).sort(), ['code', 'code.$field_facets', 'message', 'message.$field_facets'])
|
|
156
|
+
&& emptyFacet(schema['code.$field_facets'], 40, 'integer') && emptyFacet(schema['message.$field_facets'], 4000, 'string')
|
|
157
|
+
&& emptyFacet(value.complexObjectSchema['FlowDesigner:FDACTIONSTATUS.$type_facets'], 65000, 'object');
|
|
158
|
+
if (equalIntent(Object.keys(value).sort(), ['complexObject', 'complexObjectSchema', 'serializationFormat', 'version']) && value.version === '1.0' && value.serializationFormat === 'JSON' && defaultSchema && schema?.code === 'Integer' && schema?.message === 'String' && equalIntent(value.complexObject, empty)) normalized.value = found.value;
|
|
159
|
+
} catch { /* Non-default values remain mismatches. */ }
|
|
160
|
+
if (normalized.value === found.value && variable.maxsize === 0 && found.maxsize === 65000) normalized.maxsize = 65000;
|
|
161
|
+
const emptyObject = { name: variable.name, value: null, scriptActive: false, script: null, scriptAsJsonString: null, parameter: null,
|
|
162
|
+
children: (variable.children || []).map(child => ({ name: child.name, value: null, scriptActive: false, script: null, scriptAsJsonString: null, children: null, parameter: child })) };
|
|
163
|
+
const statusIntent = node => {
|
|
164
|
+
if (!object(node)) return node;
|
|
165
|
+
return Object.fromEntries(Object.entries(node).filter(([key]) => !['id', 'displayValue', 'displayField'].includes(key)).map(([key, value]) => [key,
|
|
166
|
+
key === 'parameter' && value ? variableIntent(value) : key === 'children' && Array.isArray(value) ? value.map(statusIntent) : value]));
|
|
167
|
+
};
|
|
168
|
+
if (equalIntent(statusIntent(emptyObject), statusIntent(found.complexObjectValue))) normalized.complexObjectValue = found.complexObjectValue;
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
171
|
+
function variables(expected, actual, path, mismatches, inherited = new Set()) {
|
|
172
|
+
if (!Array.isArray(actual) || expected.length !== actual.length) { mismatches.push(`${path}.length`); return; }
|
|
173
|
+
for (const variable of expected) {
|
|
174
|
+
const found = actual.find(v => v.name === variable.name);
|
|
175
|
+
if (!found) { mismatches.push(`${path}.${variable.name}`); continue; }
|
|
176
|
+
const intent = variableIntent(inherited.has(variable.name) ? lifecycleDefault(variable, found) : variable);
|
|
177
|
+
const persisted = variableIntent(found);
|
|
178
|
+
for (const field of [...VARIABLE_FIELDS, 'attributes']) {
|
|
179
|
+
if (!equalIntent(intent[field], persisted[field])) mismatches.push(`${path}.${variable.name}.${field}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function verify(expected, actual, inherited) {
|
|
184
|
+
const mismatches = [];
|
|
185
|
+
for (const key of ['id', 'scope', 'name', 'description', 'internal_name', 'state', 'master_snapshot', 'latest_snapshot']) {
|
|
186
|
+
if (Object.hasOwn(expected, key)) subset(expected[key], actual[key], key, mismatches);
|
|
187
|
+
}
|
|
188
|
+
for (const key of ['sysId', 'actionTypeId']) {
|
|
189
|
+
const before = expected.action_status_metadata?.[key];
|
|
190
|
+
const after = actual.action_status_metadata?.[key];
|
|
191
|
+
// The first create PUT allocates the status row; later updates must retain it.
|
|
192
|
+
const allocated = key === 'sysId' && inherited !== undefined && (before === '' || before === null || before === undefined)
|
|
193
|
+
&& typeof after === 'string' && /^[a-fA-F0-9]{32}$/.test(after);
|
|
194
|
+
if (!allocated && (before !== after || (key === 'sysId' && inherited !== undefined && !before))) mismatches.push(`action_status_metadata.${key}`);
|
|
195
|
+
}
|
|
196
|
+
variables(expected.inputs, actual.inputs, 'inputs', mismatches);
|
|
197
|
+
variables(expected.outputs, actual.outputs, 'outputs', mismatches, inherited);
|
|
198
|
+
if (inherited === undefined) {
|
|
199
|
+
for (const output of expected.outputs.filter(o => o.name.startsWith('__'))) {
|
|
200
|
+
if (actual.outputs.find(o => o.name === output.name)?.id !== output.id) mismatches.push(`outputs.${output.name}.id`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (actual.steps.length !== expected.steps.length) mismatches.push('steps.length');
|
|
204
|
+
for (const step of expected.steps) {
|
|
205
|
+
const found = actual.steps.find(s => s.cid === step.cid);
|
|
206
|
+
if (!found) { mismatches.push(`steps.${step.cid}`); continue; }
|
|
207
|
+
for (const key of ['cid', 'action', 'step_type', 'step_type_id', 'order', 'error_handling_type', 'quiescence']) {
|
|
208
|
+
if (!equalIntent(step[key] ?? null, found[key] ?? null)) mismatches.push(`steps.${step.cid}.${key}`);
|
|
209
|
+
}
|
|
210
|
+
for (const key of ['inputs', 'outputs', 'extended_inputs', 'extended_outputs']) variables(step[key], found[key], `steps.${step.cid}.${key}`, mismatches);
|
|
211
|
+
}
|
|
212
|
+
if (mismatches.length) throw failure('action_verification_failed', `Persisted definition differs at: ${mismatches.join(', ')}`, { mismatches });
|
|
213
|
+
}
|
|
214
|
+
async function save(sdk, id, scope, definition, inherited) {
|
|
215
|
+
let writeError;
|
|
216
|
+
let returned;
|
|
217
|
+
try {
|
|
218
|
+
returned = await request(sdk, id, scope, '', 'PUT', definition);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// A timeout is ambiguous: reconcile once via reads, NEVER retry the PUT.
|
|
221
|
+
if (!/timed?\s*out|timeout/i.test(error.message)) throw error;
|
|
222
|
+
writeError = error;
|
|
223
|
+
}
|
|
224
|
+
const persisted = await getAction(sdk, id, scope);
|
|
225
|
+
verify(definition, persisted, inherited);
|
|
226
|
+
if (returned?.id !== undefined && returned.id !== id) throw failure('action_identity_mismatch', 'PUT returned a different action ID');
|
|
227
|
+
if (returned?.scope !== undefined && returned.scope !== scope) throw failure('action_identity_mismatch', 'PUT returned a different action scope');
|
|
228
|
+
if (Array.isArray(returned?.inputs) && Array.isArray(returned?.outputs) && Array.isArray(returned?.steps)) verify(definition, returned, inherited);
|
|
229
|
+
return { sys_id: id, scope, status: writeError ? 'persisted_after_timeout' : 'verified',
|
|
230
|
+
http_write_confirmed: !writeError, definition: persisted };
|
|
231
|
+
}
|
|
232
|
+
export async function updateAction(sdk, id, providedScope, definition) {
|
|
233
|
+
validateDefinition(definition, id, providedScope);
|
|
234
|
+
const scope = await actionScope(sdk, id, providedScope);
|
|
235
|
+
validateDefinition(definition, id, scope);
|
|
236
|
+
// A full read document is required, so table fields cannot erase lifecycle defaults.
|
|
237
|
+
for (const key of ['scope', 'name', 'state', 'master_snapshot', 'latest_snapshot', 'action_status_metadata']) {
|
|
238
|
+
if (!Object.hasOwn(definition, key)) throw errUsage(`Full update definition requires ${key}; use actions definition first`);
|
|
239
|
+
}
|
|
240
|
+
const current = await getAction(sdk, id, scope);
|
|
241
|
+
const omitted = Object.keys(current).filter(key => !Object.hasOwn(definition, key));
|
|
242
|
+
if (omitted.length) throw errUsage(`Full update definition is missing ${omitted.join(', ')}; use actions definition first`);
|
|
243
|
+
for (const key of ['state', 'master_snapshot', 'latest_snapshot']) {
|
|
244
|
+
if (definition[key] !== current[key]) throw errUsage(`Do not replace target-owned ${key}`);
|
|
245
|
+
}
|
|
246
|
+
for (const key of ['sysId', 'actionTypeId']) {
|
|
247
|
+
if (definition.action_status_metadata?.[key] !== current.action_status_metadata?.[key]) {
|
|
248
|
+
throw errUsage(`Do not replace target-owned action_status_metadata.${key}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const currentReserved = current.outputs.filter(o => o.name.startsWith('__'));
|
|
252
|
+
const suppliedReserved = definition.outputs.filter(o => o.name.startsWith('__'));
|
|
253
|
+
if (currentReserved.length !== suppliedReserved.length || currentReserved.some(o => {
|
|
254
|
+
const supplied = suppliedReserved.find(v => v.name === o.name);
|
|
255
|
+
return !supplied || supplied.id !== o.id;
|
|
256
|
+
})) {
|
|
257
|
+
throw errUsage('Do not replace or remove target-owned reserved output IDs');
|
|
258
|
+
}
|
|
259
|
+
try { return await save(sdk, id, scope, definition); }
|
|
260
|
+
catch (error) {
|
|
261
|
+
throw failure(error.code?.startsWith('action_') ? error.code : 'action_update_failed', `Action ${id} update is unverified: ${error.message}`, {
|
|
262
|
+
...error.details, sys_id: id, scope, status: 'unverified', cleanup: 'explicit',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export async function createAction(sdk, scope, source) {
|
|
267
|
+
validateDefinition(source, undefined, scope);
|
|
268
|
+
for (const step of source.steps) {
|
|
269
|
+
if (['step_id', 'action'].some(key => step[key] !== undefined && step[key] !== null && step[key] !== '')) throw errUsage('Create requires new steps with blank step_id and action; do not reuse persisted Step IDs');
|
|
270
|
+
}
|
|
271
|
+
const name = source.name || source.displayName;
|
|
272
|
+
if (typeof name !== 'string' || !name.trim()) throw errUsage('Create requires a definition name');
|
|
273
|
+
await validateScope(sdk, scope);
|
|
274
|
+
const parentData = { name, description: source.description || '', sys_scope: scope };
|
|
275
|
+
if (source.internal_name !== undefined) parentData.internal_name = source.internal_name;
|
|
276
|
+
let parent;
|
|
277
|
+
try { parent = await sdk.create(ACTION_TABLE, parentData); }
|
|
278
|
+
catch (error) {
|
|
279
|
+
throw failure('action_create_partial', `Parent creation is unverified: ${error.message}. Inspect actions named '${name}' before retrying.`, {
|
|
280
|
+
scope, name, status: 'unknown_parent', cleanup: 'explicit',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const id = raw(parent?.sys_id);
|
|
284
|
+
if (!id) throw failure('action_create_partial', 'Parent creation returned no action ID; inspect the action table before retrying', { status: 'unknown_parent', cleanup: 'explicit' });
|
|
285
|
+
try {
|
|
286
|
+
token(id, 'Created action ID');
|
|
287
|
+
await actionScope(sdk, id, scope);
|
|
288
|
+
const fresh = await metadata(sdk, id, scope);
|
|
289
|
+
// Keep this parent's lifecycle/status objects and reserved output IDs.
|
|
290
|
+
const reserved = (fresh.outputs || []).filter(o => o.name?.startsWith('__'));
|
|
291
|
+
const definition = { ...fresh, name, displayName: name, description: parentData.description,
|
|
292
|
+
inputs: source.inputs, outputs: [...reserved, ...source.outputs.filter(o => !o.name?.startsWith('__'))],
|
|
293
|
+
steps: source.steps.map(step => ({ ...step, action: id })) };
|
|
294
|
+
if (source.internal_name !== undefined) definition.internal_name = source.internal_name;
|
|
295
|
+
validateDefinition(definition, id, scope);
|
|
296
|
+
return await save(sdk, id, scope, definition, new Set(reserved.filter(o => ['__action_status__', '__dont_treat_as_error__'].includes(o.name)).map(o => o.name)));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
throw failure('action_create_partial', `Action parent ${id} exists; full definition is unverified: ${error.message}. No automatic cleanup was performed.`, {
|
|
299
|
+
...error.details, sys_id: id, scope, status: 'parent_created_definition_unverified', cleanup: 'explicit',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function waitTimeout(context, state) {
|
|
305
|
+
return failure('action_test_timeout', `Action test ${context} exceeded --timeout; state=${state}. Execution was not cancelled.`, { context, state, status: 'timeout' });
|
|
306
|
+
}
|
|
307
|
+
export async function testAction(sdk, id, providedScope, suppliedDefinition, outputMap = {}, opts = {}) {
|
|
308
|
+
if (!object(outputMap)) throw errUsage('--output-map must be a JSON object of input names to values');
|
|
309
|
+
const timeout = opts.timeout ?? 60;
|
|
310
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 3600) throw errUsage('--timeout must be between 0 and 3600 seconds, exclusive of 0');
|
|
311
|
+
if (suppliedDefinition !== undefined) validateDefinition(suppliedDefinition, id, providedScope);
|
|
312
|
+
const scope = await actionScope(sdk, id, providedScope);
|
|
313
|
+
const definition = suppliedDefinition ?? await getAction(sdk, id, scope);
|
|
314
|
+
validateDefinition(definition, id, scope);
|
|
315
|
+
const dispatched = await request(sdk, id, scope, '/test', 'POST', {
|
|
316
|
+
action: definition, outputMap, runOnThread: opts.runOnThread ?? true, tracingEnabled: opts.tracingEnabled ?? false,
|
|
317
|
+
});
|
|
318
|
+
if (!successCode(dispatched.errorCode) || dispatched.errorMessage) throw failure('action_test_dispatch_failed', dispatched.errorMessage || 'Test dispatch did not return errorCode 0');
|
|
319
|
+
const context = token(dispatched.data, 'Test context ID');
|
|
320
|
+
if (!opts.wait) return { context, state: 'DISPATCHED', status: 'dispatched', outputs: null };
|
|
321
|
+
const deadline = Date.now() + timeout * 1000;
|
|
322
|
+
let state = 'UNKNOWN';
|
|
323
|
+
const expected = definition.outputs.filter(o => !o.name.startsWith('__')).map(o => o.name);
|
|
324
|
+
const bounded = async (url) => {
|
|
325
|
+
const remaining = deadline - Date.now();
|
|
326
|
+
if (remaining <= 0) throw waitTimeout(context, state);
|
|
327
|
+
try { return await sdk.request(url, { method: 'GET', timeout: Math.min(sdk.timeout || 30000, remaining) }); }
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (Date.now() >= deadline || /timed?\s*out|timeout/i.test(error.message)) throw waitTimeout(context, state);
|
|
330
|
+
throw failure('action_test_read_failed', `Cannot read test ${context} ${state}: ${error.message}`, { context, state, status: 'read_failed' });
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
while (Date.now() < deadline) {
|
|
334
|
+
const response = await bounded(`${sdk.baseURL}/api/now/table/sys_flow_context/${context}`);
|
|
335
|
+
const record = response?.result;
|
|
336
|
+
if (!record || raw(record.sys_id) !== context) throw failure('action_test_read_failed', `Test context ${context} is missing or unreadable`, { context, state, status: 'read_failed' });
|
|
337
|
+
state = String(raw(record.state) || 'UNKNOWN').toUpperCase();
|
|
338
|
+
const message = raw(record.error_message);
|
|
339
|
+
if (message || ['ERROR', 'FAILED', 'CANCELLED', 'CANCELED', 'TERMINATED'].includes(state)) {
|
|
340
|
+
throw failure('action_test_failed', `Test ${context} ended ${state}: ${message || 'terminal failure'}`, { context, state, status: 'failed' });
|
|
341
|
+
}
|
|
342
|
+
if (state === 'COMPLETE') {
|
|
343
|
+
if (!Object.hasOwn(record, 'error_message')) throw failure('action_test_read_failed', `Test ${context} COMPLETE but error_message is unreadable`, { context, state, status: 'read_failed' });
|
|
344
|
+
const query = new URLSearchParams({ sysparm_query: `context=${context}^type=output`, sysparm_fields: 'context,type,value', sysparm_display_value: 'false' });
|
|
345
|
+
const runtime = await bounded(`${sdk.baseURL}/api/now/table/sys_flow_runtime_value?${query}`);
|
|
346
|
+
const values = Object.create(null);
|
|
347
|
+
if (!Array.isArray(runtime?.result)) throw failure('action_test_outputs_missing', `Test ${context} output records are missing`, { context, state, status: 'outputs_missing' });
|
|
348
|
+
try {
|
|
349
|
+
for (const row of runtime.result) {
|
|
350
|
+
if (raw(row.context) !== context || raw(row.type) !== 'output') throw new Error('Runtime output belongs to a different context/type');
|
|
351
|
+
const map = JSON.parse(raw(row.value));
|
|
352
|
+
if (!object(map)) throw new Error('Runtime value is not a map');
|
|
353
|
+
Object.assign(values, map);
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
throw failure('action_test_outputs_invalid', `Test ${context} has invalid runtime outputs: ${error.message}`, { context, state, status: 'outputs_invalid' });
|
|
357
|
+
}
|
|
358
|
+
const missing = expected.filter(name => values[name]?.hasValue !== true || !Object.hasOwn(values[name], 'value'));
|
|
359
|
+
if (missing.length) throw failure('action_test_outputs_missing', `Test ${context} COMPLETE but missing actual outputs: ${missing.join(', ')}`, { context, state, status: 'outputs_missing', missing });
|
|
360
|
+
const outputs = Object.fromEntries(Object.entries(values).filter(([, v]) => v?.hasValue === true && Object.hasOwn(v, 'value')).map(([name, v]) => [name, { value: v.value, displayValue: v.displayValue, hasValue: v.hasValue }]));
|
|
361
|
+
return { context, state, status: 'complete', outputs };
|
|
362
|
+
}
|
|
363
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(500, Math.max(0, deadline - Date.now()))));
|
|
364
|
+
}
|
|
365
|
+
throw waitTimeout(context, state);
|
|
366
|
+
}
|
package/src/sdk.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { gunzipSync } from 'node:zlib';
|
|
4
4
|
import { errAuth, errAPI, errNetwork } from './errors.js';
|
|
5
5
|
import { getStringField } from './helpers.js';
|
|
6
|
+
import { getAction, getStepTypes, createAction, updateAction, testAction } from './processflow-actions.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_TIMEOUT = 30000;
|
|
8
9
|
|
|
@@ -232,6 +233,13 @@ export class SDKClient {
|
|
|
232
233
|
await this.request(endpoint, { method: 'DELETE' });
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// Flow Designer actions use full Process Flow documents, not table CRUD.
|
|
237
|
+
getProcessFlowAction(id, scope, opts) { return getAction(this, id, scope, opts); }
|
|
238
|
+
getProcessFlowStepTypes(scope) { return getStepTypes(this, scope); }
|
|
239
|
+
createProcessFlowAction(scope, definition) { return createAction(this, scope, definition); }
|
|
240
|
+
updateProcessFlowAction(id, scope, definition) { return updateAction(this, id, scope, definition); }
|
|
241
|
+
testProcessFlowAction(id, scope, definition, outputMap, opts) { return testAction(this, id, scope, definition, outputMap, opts); }
|
|
242
|
+
|
|
235
243
|
// ─── Attachments ───
|
|
236
244
|
|
|
237
245
|
/**
|