@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,448 @@
|
|
|
1
|
+
// Trigger contract for Flow Designer creation.
|
|
2
|
+
//
|
|
3
|
+
// Flow Designer does NOT give a newly POSTed ProcessFlow a default trigger, so
|
|
4
|
+
// the trigger instance must be INSERTED through the same snFlowDesigner
|
|
5
|
+
// GraphQL `flowPatch` that inserts actions. Everything the insert needs is read
|
|
6
|
+
// from the instance's own schema tables — no trigger input is hardcoded:
|
|
7
|
+
//
|
|
8
|
+
// sys_hub_trigger_definition one row per trigger ("Created", "Daily", …)
|
|
9
|
+
// sys_hub_trigger_category the picker's first level
|
|
10
|
+
// sys_hub_trigger_input the inputs a definition declares. The owning
|
|
11
|
+
// definition is carried by `name`, which is
|
|
12
|
+
// `var__m_sys_hub_trigger_input_<definitionId>`;
|
|
13
|
+
// the `model` reference is EMPTY on this
|
|
14
|
+
// instance for most definitions, and a
|
|
15
|
+
// `trigger_definition=` query is silently
|
|
16
|
+
// ignored, so `name` is the only reliable
|
|
17
|
+
// relation.
|
|
18
|
+
// sys_db_object / sys_dictionary the record trigger's Table label and display
|
|
19
|
+
// column (the ProcessFlow
|
|
20
|
+
// /trigger/record/table descriptor endpoint
|
|
21
|
+
// does not exist: HTTP 400 "Requested URI does
|
|
22
|
+
// not represent any resource").
|
|
23
|
+
//
|
|
24
|
+
// `internal_type` on sys_hub_trigger_input holds the parameter's real type
|
|
25
|
+
// ("glide_time", "table_name", "conditions", …) as its raw value; its display
|
|
26
|
+
// value is the human label ("Time", "Table Name", …).
|
|
27
|
+
|
|
28
|
+
export const TRIGGER_DEFINITION_TABLE = 'sys_hub_trigger_definition';
|
|
29
|
+
export const TRIGGER_CATEGORY_TABLE = 'sys_hub_trigger_category';
|
|
30
|
+
export const TRIGGER_INPUT_TABLE = 'sys_hub_trigger_input';
|
|
31
|
+
|
|
32
|
+
export const TRIGGER_INPUT_NAME_PREFIX = 'var__m_sys_hub_trigger_input_';
|
|
33
|
+
|
|
34
|
+
// Families this CLI can actually create, each verified against a live instance
|
|
35
|
+
// with a disposable draft flow (see the PR body for the probe sys_ids). The
|
|
36
|
+
// picker intersects this set with the instance's own active definitions, so an
|
|
37
|
+
// unimplemented family can never be offered.
|
|
38
|
+
export const SUPPORTED_TRIGGER_TYPES = new Set([
|
|
39
|
+
'record_create',
|
|
40
|
+
'record_update',
|
|
41
|
+
'record_create_or_update',
|
|
42
|
+
'daily',
|
|
43
|
+
'weekly',
|
|
44
|
+
'monthly',
|
|
45
|
+
'repeat',
|
|
46
|
+
'run_once',
|
|
47
|
+
'email',
|
|
48
|
+
'knowledge_management',
|
|
49
|
+
'service_catalog',
|
|
50
|
+
'sla_task',
|
|
51
|
+
'remote_table_query',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// Definitions that exist on the instance but are deliberately not offered, with
|
|
55
|
+
// the reason. Kept explicit so the picker can explain the omission instead of
|
|
56
|
+
// silently hiding a family.
|
|
57
|
+
export const EXCLUDED_TRIGGER_TYPES = new Map([
|
|
58
|
+
['analytics', 'the Proactive Analytics trigger definition is inactive on this instance'],
|
|
59
|
+
['rest', 'the Trigger Rest definition has no resolvable trigger category'],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Families whose declared inputs name a table. The manifest's `table` shortcut
|
|
63
|
+
// maps onto whichever of these the chosen definition declares.
|
|
64
|
+
const TABLE_INPUT_NAMES = ['table', 'u_table', 'target_table'];
|
|
65
|
+
// Same idea for the encoded-query condition.
|
|
66
|
+
const CONDITION_INPUT_NAMES = ['condition', 'email_conditions', 'business_calendar_condition'];
|
|
67
|
+
|
|
68
|
+
// Trigger input types this CLI can prompt for. Anything else must be supplied
|
|
69
|
+
// through --data-file so its complete schema and value are preserved.
|
|
70
|
+
export const PROMPTABLE_TRIGGER_TYPES = new Set([
|
|
71
|
+
'string', 'table_name', 'field_name', 'conditions', 'choice', 'reference',
|
|
72
|
+
'glide_time', 'glide_date_time', 'glide_duration', 'glide_list',
|
|
73
|
+
'integer', 'decimal', 'float', 'boolean', 'day_of_week', 'month_of_year',
|
|
74
|
+
'script', 'html', 'url', 'email', 'document_id', 'date', 'datetime',
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
export const NUMERIC_TRIGGER_TYPES = new Set(['integer', 'decimal', 'float']);
|
|
78
|
+
|
|
79
|
+
function rawValue(value) {
|
|
80
|
+
if (value && typeof value === 'object') return value.value ?? value.sys_id ?? value.name ?? '';
|
|
81
|
+
return value == null ? '' : value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function displayValue(value) {
|
|
85
|
+
if (value && typeof value === 'object') return value.display_value ?? value.value ?? '';
|
|
86
|
+
return value == null ? '' : value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function text(value) {
|
|
90
|
+
const result = rawValue(value);
|
|
91
|
+
return result == null ? '' : String(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isSysId(value) {
|
|
95
|
+
return typeof value === 'string' && /^[0-9a-f]{32}$/i.test(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function boolValue(value) {
|
|
99
|
+
return String(rawValue(value)).toLowerCase() === 'true';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function numeric(value, fallback = 0) {
|
|
103
|
+
const parsed = Number(rawValue(value));
|
|
104
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The category tree the picker shows, built from the instance's own
|
|
109
|
+
* sys_hub_trigger_category rows and the category reference each active
|
|
110
|
+
* definition carries.
|
|
111
|
+
*
|
|
112
|
+
* Definitions whose category does not resolve to a readable category record
|
|
113
|
+
* are returned separately in `ungrouped` — they are never selectable, but the
|
|
114
|
+
* caller can report them.
|
|
115
|
+
*/
|
|
116
|
+
export async function readTriggerCatalog(sdk) {
|
|
117
|
+
const [definitions, categories] = await Promise.all([
|
|
118
|
+
sdk.list(TRIGGER_DEFINITION_TABLE, new URLSearchParams({
|
|
119
|
+
sysparm_query: 'active=true^ORDERBYname',
|
|
120
|
+
sysparm_limit: '200',
|
|
121
|
+
sysparm_display_value: 'all',
|
|
122
|
+
sysparm_fields: 'sys_id,name,type,active,category,description',
|
|
123
|
+
})),
|
|
124
|
+
sdk.list(TRIGGER_CATEGORY_TABLE, new URLSearchParams({
|
|
125
|
+
sysparm_limit: '200',
|
|
126
|
+
sysparm_display_value: 'all',
|
|
127
|
+
sysparm_fields: 'sys_id,name',
|
|
128
|
+
})),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const categoryNames = new Map(categories.map(row => [text(row.sys_id), displayValue(row.name) || text(row.name)]));
|
|
132
|
+
const grouped = new Map();
|
|
133
|
+
const ungrouped = [];
|
|
134
|
+
|
|
135
|
+
for (const row of definitions) {
|
|
136
|
+
const categoryId = text(row.category);
|
|
137
|
+
const categoryName = categoryNames.get(categoryId) || '';
|
|
138
|
+
const definition = {
|
|
139
|
+
sysId: text(row.sys_id),
|
|
140
|
+
type: text(row.type),
|
|
141
|
+
name: displayValue(row.name) || text(row.name),
|
|
142
|
+
description: displayValue(row.description) || '',
|
|
143
|
+
supported: SUPPORTED_TRIGGER_TYPES.has(text(row.type)),
|
|
144
|
+
excludedReason: EXCLUDED_TRIGGER_TYPES.get(text(row.type)) || (SUPPORTED_TRIGGER_TYPES.has(text(row.type)) ? '' : 'this CLI has no verified creation contract for this trigger'),
|
|
145
|
+
};
|
|
146
|
+
if (!categoryId || !categoryName) {
|
|
147
|
+
ungrouped.push(definition);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!grouped.has(categoryId)) grouped.set(categoryId, { id: categoryId, name: categoryName, triggers: [] });
|
|
151
|
+
grouped.get(categoryId).triggers.push(definition);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const categoriesOut = [...grouped.values()]
|
|
155
|
+
.map(category => ({ ...category, triggers: category.triggers.sort((a, b) => a.name.localeCompare(b.name)) }))
|
|
156
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
157
|
+
|
|
158
|
+
return { categories: categoriesOut, ungrouped };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The pickable definitions for one category, in the shape the picker needs. */
|
|
162
|
+
export function selectableTriggers(category) {
|
|
163
|
+
return (category?.triggers || []).filter(trigger => trigger.supported);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Definition descriptions are authored HTML; strip it and keep the first
|
|
167
|
+
// sentence so a picker row stays readable.
|
|
168
|
+
export function triggerChoiceLabel(trigger) {
|
|
169
|
+
const text = String(trigger?.description || '')
|
|
170
|
+
.replace(/<[^>]*>/g, ' ')
|
|
171
|
+
.replace(/&[a-z]+;/gi, ' ')
|
|
172
|
+
.replace(/\s+/g, ' ')
|
|
173
|
+
.trim();
|
|
174
|
+
if (!text) return trigger?.name || '';
|
|
175
|
+
const sentence = text.split(/\.\s/)[0];
|
|
176
|
+
return `${trigger.name} — ${sentence.length > 90 ? `${sentence.slice(0, 87)}…` : sentence}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The inputs a trigger definition declares, straight from
|
|
181
|
+
* sys_hub_trigger_input. Answers "what must I supply for this family?" from the
|
|
182
|
+
* instance rather than from a hand-written table.
|
|
183
|
+
*/
|
|
184
|
+
export async function readTriggerInputDefinitions(sdk, definitionId) {
|
|
185
|
+
if (!isSysId(definitionId)) throw new Error(`Trigger definition id must be an exact sys_id, got "${definitionId}"`);
|
|
186
|
+
const rows = await sdk.list(TRIGGER_INPUT_TABLE, new URLSearchParams({
|
|
187
|
+
sysparm_query: `name=${TRIGGER_INPUT_NAME_PREFIX}${definitionId}^ORDERBYorder`,
|
|
188
|
+
sysparm_limit: '200',
|
|
189
|
+
sysparm_display_value: 'all',
|
|
190
|
+
sysparm_fields: 'sys_id,element,label,column_label,internal_type,mandatory,order,max_length,default_value,attributes,choice,reference,use_dependent_field,dependent_on_field,read_only,hint',
|
|
191
|
+
}));
|
|
192
|
+
return rows
|
|
193
|
+
.map(normalizeTriggerInput)
|
|
194
|
+
.filter(input => input.name);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeTriggerInput(row) {
|
|
198
|
+
const parameter = {
|
|
199
|
+
id: text(row.sys_id),
|
|
200
|
+
label: displayValue(row.label) || displayValue(row.column_label) || text(row.element),
|
|
201
|
+
name: text(row.element),
|
|
202
|
+
type: text(row.internal_type),
|
|
203
|
+
hint: displayValue(row.hint) || '',
|
|
204
|
+
order: numeric(row.order),
|
|
205
|
+
extended: false,
|
|
206
|
+
mandatory: boolValue(row.mandatory),
|
|
207
|
+
readonly: boolValue(row.read_only),
|
|
208
|
+
maxsize: numeric(row.max_length),
|
|
209
|
+
children: [],
|
|
210
|
+
reference: text(row.reference) ? displayValue(row.reference) : '',
|
|
211
|
+
reference_display: '',
|
|
212
|
+
ref_qual: '',
|
|
213
|
+
choiceOption: text(row.choice),
|
|
214
|
+
table: '',
|
|
215
|
+
columnName: '',
|
|
216
|
+
defaultValue: rawValue(row.default_value) ?? '',
|
|
217
|
+
use_dependent: boolValue(row.use_dependent_field),
|
|
218
|
+
dependent_on: text(row.dependent_on_field),
|
|
219
|
+
show_ref_finder: false,
|
|
220
|
+
local: false,
|
|
221
|
+
// On INPUT the GraphQL type is String; the read path renders it as an object.
|
|
222
|
+
attributes: text(row.attributes),
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
id: parameter.id,
|
|
226
|
+
name: parameter.name,
|
|
227
|
+
label: parameter.label,
|
|
228
|
+
type: parameter.type,
|
|
229
|
+
mandatory: parameter.mandatory,
|
|
230
|
+
order: parameter.order,
|
|
231
|
+
defaultValue: parameter.defaultValue === '' ? '' : String(parameter.defaultValue),
|
|
232
|
+
reference: parameter.reference,
|
|
233
|
+
parameter,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The record trigger's table metadata. The documented ProcessFlow descriptor
|
|
239
|
+
* route (/api/now/processflow/trigger/record/table) does not exist on this
|
|
240
|
+
* instance, so the label comes from sys_db_object and the display column from
|
|
241
|
+
* the nearest sys_dictionary `display=true` element walking up the
|
|
242
|
+
* super_class chain (a child table such as `ticket` inherits `number` from
|
|
243
|
+
* `task`).
|
|
244
|
+
*/
|
|
245
|
+
export async function readTableDescriptor(sdk, table) {
|
|
246
|
+
const name = String(table || '').trim();
|
|
247
|
+
if (!name) throw new Error('Record trigger requires a table name');
|
|
248
|
+
const rows = await sdk.list('sys_db_object', new URLSearchParams({
|
|
249
|
+
sysparm_query: `name=${name}`,
|
|
250
|
+
sysparm_limit: '2',
|
|
251
|
+
sysparm_display_value: 'all',
|
|
252
|
+
sysparm_fields: 'name,label,super_class',
|
|
253
|
+
}));
|
|
254
|
+
if (rows.length !== 1) throw new Error(rows.length === 0 ? `Table not found: ${name}` : `Table name is ambiguous: ${name}`);
|
|
255
|
+
const label = displayValue(rows[0].label) || text(rows[0].label);
|
|
256
|
+
if (!label) throw new Error(`Table ${name} has no label`);
|
|
257
|
+
return { table: name, label, displayField: await readDisplayField(sdk, name, text(rows[0].super_class)) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function readDisplayField(sdk, table, superId) {
|
|
261
|
+
let current = table;
|
|
262
|
+
let parentId = superId;
|
|
263
|
+
for (let depth = 0; depth < 16 && current; depth += 1) {
|
|
264
|
+
const field = await sdk.list('sys_dictionary', new URLSearchParams({
|
|
265
|
+
sysparm_query: `name=${current}^display=true`,
|
|
266
|
+
sysparm_limit: '5',
|
|
267
|
+
sysparm_display_value: 'all',
|
|
268
|
+
sysparm_fields: 'element',
|
|
269
|
+
}));
|
|
270
|
+
const element = field.map(row => text(row.element)).find(Boolean);
|
|
271
|
+
if (element) return element;
|
|
272
|
+
if (!parentId) return '';
|
|
273
|
+
const parent = await sdk.list('sys_db_object', new URLSearchParams({
|
|
274
|
+
sysparm_query: `sys_id=${parentId}`,
|
|
275
|
+
sysparm_limit: '2',
|
|
276
|
+
sysparm_display_value: 'all',
|
|
277
|
+
sysparm_fields: 'name,super_class',
|
|
278
|
+
}));
|
|
279
|
+
if (parent.length !== 1) return '';
|
|
280
|
+
current = text(parent[0].name);
|
|
281
|
+
parentId = text(parent[0].super_class);
|
|
282
|
+
}
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The declared input a manifest shortcut (`table`, `condition`) should drive. */
|
|
287
|
+
export function tableInputFor(declared) {
|
|
288
|
+
return declared.find(input => TABLE_INPUT_NAMES.includes(input.name)) || null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function conditionInputFor(declared) {
|
|
292
|
+
return declared.find(input => CONDITION_INPUT_NAMES.includes(input.name)) || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function wrapped(value) {
|
|
296
|
+
const scalar = value === '' || value === null || value === undefined ? '' : String(value);
|
|
297
|
+
return { schemaless: false, schemalessValue: '', value: scalar };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function declaredDefault(input) {
|
|
301
|
+
return input.defaultValue === '' || input.defaultValue === undefined ? '' : input.defaultValue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Turn a definition's declared inputs plus the caller's values into the exact
|
|
306
|
+
* `inputs` array the snFlowDesigner insert expects.
|
|
307
|
+
*
|
|
308
|
+
* Every declared input is written, using the instance's own default when the
|
|
309
|
+
* caller supplies nothing, because Flow Designer stores the full input set
|
|
310
|
+
* (a real record trigger carries run_on_extended / run_flow_in /
|
|
311
|
+
* run_when_setting / … alongside table and condition).
|
|
312
|
+
*/
|
|
313
|
+
export function buildTriggerInputs(declared, supplied = {}, tableDescriptor = null) {
|
|
314
|
+
const byName = new Map(declared.map(input => [input.name, input]));
|
|
315
|
+
for (const name of Object.keys(supplied)) {
|
|
316
|
+
if (!byName.has(name)) throw new Error(`Trigger input "${name}" is not declared by this trigger definition`);
|
|
317
|
+
}
|
|
318
|
+
for (const input of declared) {
|
|
319
|
+
const value = Object.prototype.hasOwnProperty.call(supplied, input.name) ? supplied[input.name] : declaredDefault(input);
|
|
320
|
+
const resolved = value === '' || value === null || value === undefined ? '' : String(value);
|
|
321
|
+
if (resolved === '' && input.mandatory) throw new Error(`Trigger input "${input.name}" (${input.label || input.type}) is required`);
|
|
322
|
+
const tableValue = tableDescriptor && TABLE_INPUT_NAMES.includes(input.name);
|
|
323
|
+
const renderedValue = tableValue ? tableDescriptor.table : resolved;
|
|
324
|
+
const renderedDisplay = tableValue ? tableDescriptor.label : resolved;
|
|
325
|
+
if (!input.parameter?.id) throw new Error(`Trigger input "${input.name}" is missing its provider identity; refusing to invent one`);
|
|
326
|
+
byName.get(input.name).value = renderedValue;
|
|
327
|
+
byName.get(input.name).displayValue = renderedDisplay;
|
|
328
|
+
byName.get(input.name).displayField = tableValue ? tableDescriptor.displayField || '' : '';
|
|
329
|
+
}
|
|
330
|
+
return declared.map(input => ({
|
|
331
|
+
name: input.name,
|
|
332
|
+
value: wrapped(input.value),
|
|
333
|
+
displayValue: wrapped(input.displayValue),
|
|
334
|
+
displayField: input.displayField || '',
|
|
335
|
+
scriptActive: false,
|
|
336
|
+
label: input.parameter.label,
|
|
337
|
+
internalType: input.parameter.type,
|
|
338
|
+
fromTemplate: false,
|
|
339
|
+
order: input.parameter.order,
|
|
340
|
+
valueSysId: '',
|
|
341
|
+
mandatory: input.parameter.mandatory === true,
|
|
342
|
+
parameter: input.parameter,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The triggerInstances.insert payload for the snFlowDesigner flow patch. */
|
|
347
|
+
export function buildTriggerInstance({ flowId, definition, triggerType = '', inputs, metadata = '{}' }) {
|
|
348
|
+
if (!flowId) throw new Error('Trigger insert requires the created flow id');
|
|
349
|
+
if (!definition?.sysId || !definition?.type) throw new Error('Trigger insert requires a resolved trigger definition');
|
|
350
|
+
return {
|
|
351
|
+
flowSysId: flowId,
|
|
352
|
+
name: definition.name,
|
|
353
|
+
type: definition.type,
|
|
354
|
+
triggerType: triggerType || '',
|
|
355
|
+
triggerDefinitionId: definition.sysId,
|
|
356
|
+
metadata,
|
|
357
|
+
inputs,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const RECORD_TRIGGER_FAMILY = 'record_create_or_update';
|
|
362
|
+
|
|
363
|
+
// A trigger instance carries the FAMILY and the picker CATEGORY in two different
|
|
364
|
+
// fields: `type` is the family Flow Designer executes ("record_create_or_update",
|
|
365
|
+
// "daily", …) and `triggerType` is the category display name ("Record",
|
|
366
|
+
// "Scheduled", …). Creation writes the family to `type` (resolveTriggerDefinition
|
|
367
|
+
// reads it from sys_hub_trigger_definition), and the live ProcessFlow read-back
|
|
368
|
+
// returns exactly that shape, so `type` is authoritative.
|
|
369
|
+
//
|
|
370
|
+
// Instances and fixtures that predate that split stored the family in
|
|
371
|
+
// `triggerType` (and sometimes inside the raw metadata block), so those stay as
|
|
372
|
+
// deliberate fallbacks — otherwise a real saved trigger reads as the category
|
|
373
|
+
// "Record" and every consumer rejects it.
|
|
374
|
+
export function triggerFamily(source) {
|
|
375
|
+
const metadata = parsedObject(source?.metadata);
|
|
376
|
+
const candidates = [
|
|
377
|
+
source?.type,
|
|
378
|
+
source?.trigger_type,
|
|
379
|
+
metadata?.type,
|
|
380
|
+
metadata?.trigger_type,
|
|
381
|
+
source?.triggerType,
|
|
382
|
+
metadata?.triggerType,
|
|
383
|
+
];
|
|
384
|
+
for (const candidate of candidates) {
|
|
385
|
+
const family = familyValue(candidate);
|
|
386
|
+
if (family) return family;
|
|
387
|
+
}
|
|
388
|
+
return '';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function parsedObject(value) {
|
|
392
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
393
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
394
|
+
try {
|
|
395
|
+
const parsed = JSON.parse(value);
|
|
396
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function familyValue(value) {
|
|
403
|
+
if (value === null || value === undefined) return '';
|
|
404
|
+
if (typeof value === 'object') {
|
|
405
|
+
for (const key of ['value', 'display_value', 'sys_id']) {
|
|
406
|
+
const nested = familyValue(value[key]);
|
|
407
|
+
if (nested) return nested;
|
|
408
|
+
}
|
|
409
|
+
return '';
|
|
410
|
+
}
|
|
411
|
+
return String(value).trim();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The snFlowDesigner GraphQL input types take `attributes` as a String
|
|
415
|
+
// ("key=value,key=value") even though the ProcessFlow read path renders it as an
|
|
416
|
+
// object. Sending the object fails validation: "not a valid 'String'".
|
|
417
|
+
export function attributesToString(value) {
|
|
418
|
+
if (value === null || value === undefined) return '';
|
|
419
|
+
if (typeof value === 'string') return value;
|
|
420
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new Error('Provider attributes must be a string or a flat object');
|
|
421
|
+
return Object.entries(value).map(([key, item]) => `${key}=${item}`).join(',');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Accepted `parameter` keys on global_snFlowDesigner_FlowDesignParameter.
|
|
425
|
+
// GraphQL introspection is disabled on this instance, so this list was
|
|
426
|
+
// established empirically by probing each candidate field against the live
|
|
427
|
+
// schema (unknown keys answer "contains a field not in
|
|
428
|
+
// 'global_snFlowDesigner_FlowDesignParameter'"). Anything the ProcessFlow
|
|
429
|
+
// action definition returns that is NOT in this list — notably the synthetic
|
|
430
|
+
// `default` this CLI adds, plus `scriptActive`/`value`/`displayValue` — must be
|
|
431
|
+
// carried elsewhere or dropped, or the mutation fails validation.
|
|
432
|
+
const FLOW_DESIGN_PARAMETER_FIELDS = [
|
|
433
|
+
'id', 'label', 'name', 'type', 'type_label', 'hint', 'order', 'extended',
|
|
434
|
+
'mandatory', 'readonly', 'maxsize', 'children', 'data_structure', 'reference',
|
|
435
|
+
'reference_display', 'ref_qual', 'choiceOption', 'table', 'columnName',
|
|
436
|
+
'defaultValue', 'defaultDisplayValue', 'use_dependent', 'dependent_on',
|
|
437
|
+
'show_ref_finder', 'local', 'attributes', 'sys_class_name', 'choices',
|
|
438
|
+
'defaultChoices', 'uiDisplayType', 'uiDisplayTypeLabel', 'placeholder',
|
|
439
|
+
'comments', 'internal_link', 'dynamic', 'searchField',
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
/** A provider schema entry narrowed to the GraphQL parameter input type. */
|
|
443
|
+
export function flowDesignParameter(entry) {
|
|
444
|
+
const out = {};
|
|
445
|
+
for (const key of FLOW_DESIGN_PARAMETER_FIELDS) if (entry?.[key] !== undefined) out[key] = entry[key];
|
|
446
|
+
out.attributes = attributesToString(entry?.attributes);
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const VERSION_ENDPOINT = '/api/now/processflow/versioning/create_version';
|
|
2
|
+
const VERSION_TABLE = 'sys_hub_flow_version';
|
|
3
|
+
|
|
4
|
+
function stringValue(value) {
|
|
5
|
+
if (value && typeof value === 'object') return String(value.value ?? value.sys_id ?? value.display_value ?? '');
|
|
6
|
+
return value == null ? '' : String(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function versionId(record) {
|
|
10
|
+
return stringValue(record?.sys_id ?? record?.sysId ?? record?.id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function responseVersionId(response) {
|
|
14
|
+
const result = response?.result?.result ?? response?.result ?? response?.data?.result ?? response?.data ?? response;
|
|
15
|
+
return versionId(result);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function versionType(record) {
|
|
19
|
+
const value = record?.type;
|
|
20
|
+
if (value && typeof value === 'object') return String(value.display_value ?? value.value ?? '');
|
|
21
|
+
return stringValue(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function versionQuery(flowId) {
|
|
25
|
+
return new URLSearchParams({
|
|
26
|
+
sysparm_query: `flow=${flowId}^ORDERBYDESCsys_updated_on`,
|
|
27
|
+
sysparm_limit: '20',
|
|
28
|
+
sysparm_display_value: 'all',
|
|
29
|
+
sysparm_fields: 'sys_id,flow,type,sys_created_on,sys_updated_on',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function createVerifiedFlowVersion(sdk, { instance, flowId, scope, type, annotation }) {
|
|
34
|
+
if (!instance || !flowId || !scope || !type) throw new Error('Flow version creation requires instance, flow ID, scope, and type');
|
|
35
|
+
const before = await sdk.list(VERSION_TABLE, versionQuery(flowId));
|
|
36
|
+
const beforeIds = new Set(before.map(versionId).filter(Boolean));
|
|
37
|
+
const response = await sdk.request(`${instance}${VERSION_ENDPOINT}?sysparm_transaction_scope=${encodeURIComponent(scope)}`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: JSON.stringify({ item_sys_id: flowId, type, annotation, favorite: false }),
|
|
40
|
+
});
|
|
41
|
+
const responseId = responseVersionId(response);
|
|
42
|
+
const after = await sdk.list(VERSION_TABLE, versionQuery(flowId));
|
|
43
|
+
const matching = after.filter(record => stringValue(record.flow) === flowId && versionType(record) === type);
|
|
44
|
+
const newVersions = matching.filter(record => versionId(record) && !beforeIds.has(versionId(record)));
|
|
45
|
+
const verified = responseId
|
|
46
|
+
? newVersions.find(record => versionId(record) === responseId)
|
|
47
|
+
: newVersions.length === 1 ? newVersions[0] : null;
|
|
48
|
+
if (!verified) {
|
|
49
|
+
if (newVersions.length > 1 && !responseId) throw new Error(`Flow ${flowId} has ambiguous new ${type} versions; refusing to guess`);
|
|
50
|
+
throw new Error(`New ${type} version for flow ${flowId} was not read back with an exact ID`);
|
|
51
|
+
}
|
|
52
|
+
return { version: verified, response };
|
|
53
|
+
}
|