@jacebenson/jsn 4.3.7-dev.17 → 4.3.7-dev.19
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 +143 -0
- package/package.json +1 -1
- package/src/commands/_simple.js +1 -1
- package/src/commands/actions.js +122 -0
- package/src/commands/flows.js +78 -18
- package/src/flow-create.js +774 -0
- package/src/flow-publish.js +32 -0
- package/src/flow-test.js +122 -0
- package/src/flow-versioning.js +53 -0
- package/src/processflow-actions.js +366 -0
- package/src/sdk.js +8 -0
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ jsn flows executions --since "2026-08-25 00:00:00" --until "2026-08-26 00:00:00"
|
|
|
59
59
|
jsn flows executions --summary # Server totals + sampled duration metrics by flow
|
|
60
60
|
jsn flows executions --limit 100 # Inspect more rows from the matching population
|
|
61
61
|
jsn flows executions --record <sys_id> # Executions for one source record
|
|
62
|
+
jsn flows create --data-file flow.manifest.json # Create a draft record-triggered flow
|
|
62
63
|
jsn rules list --query "collection=incident"
|
|
63
64
|
jsn updatesets set "My Feature"
|
|
64
65
|
|
|
@@ -93,8 +94,150 @@ jsn snippets run open-inc
|
|
|
93
94
|
jsn logs follow --level error --tail 10
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
### Flow Designer actions
|
|
98
|
+
|
|
99
|
+
`actions` and `action` are the same command. `list`, `show`, and explicit
|
|
100
|
+
`delete` remain available. Create and edit use full Process Flow JSON, not
|
|
101
|
+
ordinary table-field updates.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
jsn actions step-types --scope <scope_sys_id> --get data > step-types.json
|
|
105
|
+
jsn actions create --scope <scope_sys_id> --data-file new-action.json --json
|
|
106
|
+
jsn actions definition <action_sys_id> --get data > action.json
|
|
107
|
+
# Edit action.json, retaining its ID, scope, lifecycle fields and all arrays.
|
|
108
|
+
jsn actions update <action_sys_id> --data-file action.json --json
|
|
109
|
+
jsn action edit <action_sys_id> --data-file action.json --json # same operation
|
|
110
|
+
jsn actions test <action_sys_id> --force --output-map '{"response":"hello"}' --wait --timeout 60 --json
|
|
111
|
+
jsn actions test <action_sys_id> --force --data-file action.json --output-map '{"response":"hello"}' --wait --get data.outputs.response.value
|
|
112
|
+
jsn actions delete <action_sys_id> --force # explicit cleanup, never automatic
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Existing targets infer their exact transaction scope from the action record.
|
|
116
|
+
An explicit `--scope` must match that record byte-for-byte and exist in
|
|
117
|
+
`sys_scope`. For Global, use the literal sys_id `global`.
|
|
118
|
+
|
|
119
|
+
This **illustrative create document** shows an input/script/output mapping.
|
|
120
|
+
Replace `<SCRIPT_STEP_TYPE_SYS_ID>` with the target's `step-types` result.
|
|
121
|
+
Start from that step type's full input/output schemas, preserving runtime
|
|
122
|
+
settings, status outputs and any other required fields. The shortened example
|
|
123
|
+
is not a captured or instance-verified template. New steps/variables need their
|
|
124
|
+
own identifiers; do not clone another action's record IDs. Leave new steps'
|
|
125
|
+
`step_id` and `action` blank or omitted. Create binds their `action` to the newly
|
|
126
|
+
allocated parent; persisted step IDs or parent bindings are rejected before creation.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"name": "Echo response",
|
|
131
|
+
"description": "Return the supplied response",
|
|
132
|
+
"internal_name": "echo_response",
|
|
133
|
+
"inputs": [{"name": "response", "label": "Response", "type": "string", "mandatory": true}],
|
|
134
|
+
"outputs": [{"name": "response", "label": "Response", "type": "string", "value": "{{step[11111111-2222-4333-8444-555555555555].response}}"}],
|
|
135
|
+
"steps": [{
|
|
136
|
+
"cid": "11111111-2222-4333-8444-555555555555",
|
|
137
|
+
"step_type": "SCRIPT",
|
|
138
|
+
"step_type_id": "<SCRIPT_STEP_TYPE_SYS_ID>",
|
|
139
|
+
"label": "Echo script",
|
|
140
|
+
"order": 1,
|
|
141
|
+
"inputs": [
|
|
142
|
+
{"name": "required_run_time", "type": "choice", "value": "instance"},
|
|
143
|
+
{"name": "script", "type": "script", "value": "(function execute(inputs, outputs) { outputs.response = inputs.response; })(inputs, outputs);"}
|
|
144
|
+
],
|
|
145
|
+
"outputs": [],
|
|
146
|
+
"extended_inputs": [{"name": "response", "type": "string", "value": "{{inputs.response}}", "extended": true}],
|
|
147
|
+
"extended_outputs": [{"name": "response", "type": "string", "extended": true}]
|
|
148
|
+
}]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Create first inserts the parent in `sys_hub_action_type_definition`, reads that
|
|
153
|
+
parent's own Process Flow defaults, then saves the inputs, outputs and nonempty
|
|
154
|
+
steps with the internal `/api/now/processflow/action/action_types/{id}` PUT.
|
|
155
|
+
It retains fresh lifecycle/status metadata instead of copying source snapshot
|
|
156
|
+
or status IDs. A subsequent definition GET plus `/step_instances` GET verifies
|
|
157
|
+
scripts, mappings and schemas. Update requires a complete document from
|
|
158
|
+
`definition`, including target-owned lifecycle fields. Omitted fields are
|
|
159
|
+
rejected before mutation. A PUT timeout triggers readback, not another PUT;
|
|
160
|
+
`persisted_after_timeout` means persistence was verified, not that HTTP success
|
|
161
|
+
was received. Failed creation reports the parent ID and leaves cleanup to you.
|
|
162
|
+
|
|
163
|
+
Test sends `action`, `outputMap`, `runOnThread` and `tracingEnabled` to `/test`.
|
|
164
|
+
Despite its wire name, `--output-map` supplies **action inputs**. Without
|
|
165
|
+
`--wait`, success means dispatch only, with `status: dispatched` and no outputs.
|
|
166
|
+
With `--wait`, JSN requires a `COMPLETE` context, blank `error_message`, and each
|
|
167
|
+
declared user output's actual value from `sys_flow_runtime_value`. Output entries
|
|
168
|
+
have `value`, `displayValue` and `hasValue`; false, zero and empty strings count.
|
|
169
|
+
Failures, denied/missing outputs and timeouts exit nonzero. The wait budget
|
|
170
|
+
starts after dispatch, defaults to 60 seconds, and is capped at 3600 seconds.
|
|
171
|
+
Timeout does not cancel the server execution. Draft actions can be tested without
|
|
172
|
+
publication. Testing runs their scripts and can have side effects. It requires
|
|
173
|
+
interactive confirmation or `--force`, unless the profile explicitly skips
|
|
174
|
+
confirmations. Read-only profiles block create/update/delete/test. No publish
|
|
175
|
+
or snapshot command is added.
|
|
176
|
+
These internal endpoints and runtime-table permissions can vary by instance.
|
|
177
|
+
|
|
96
178
|
### Flow execution fields
|
|
97
179
|
|
|
180
|
+
### Flow creation manifest
|
|
181
|
+
|
|
182
|
+
`jsn flows create` accepts `--data-file` or inline `--data`. The first slice
|
|
183
|
+
supports one `record_create_or_update` trigger and published Action types whose
|
|
184
|
+
input schemas are available from the instance. JSN resolves action metadata,
|
|
185
|
+
defaults, and typed inputs from ServiceNow; callers do not provide GraphQL or
|
|
186
|
+
editor-generated identifiers.
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"name": "JSN-STUDY-ticket-log",
|
|
191
|
+
"description": "Log JSN study tickets",
|
|
192
|
+
"scope": "<SCOPE_SYS_ID>",
|
|
193
|
+
"trigger": {
|
|
194
|
+
"type": "record_create_or_update",
|
|
195
|
+
"table": "ticket",
|
|
196
|
+
"condition": "short_descriptionSTARTSWITHJSN-STUDY-"
|
|
197
|
+
},
|
|
198
|
+
"actions": [
|
|
199
|
+
{ "type": "Log", "inputs": { "log_level": "info", "log_message": "Ticket received" } }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`scope` is required; JSN never defaults it to a tenant-specific or captured
|
|
205
|
+
scope. The record-trigger condition currently accepts one simple encoded-query
|
|
206
|
+
predicate (`fieldSTARTSWITHvalue`, `field=value`, `fieldLIKEvalue`, and the
|
|
207
|
+
equivalent single-predicate operators). Compound `^`/`^OR` expressions are
|
|
208
|
+
rejected until their predicate AST mapping is proven against the instance.
|
|
209
|
+
|
|
210
|
+
Creation leaves the flow in Draft and inactive, then creates and reads back an
|
|
211
|
+
Autosave version. `flows publish` remains a separate lifecycle operation: it
|
|
212
|
+
reads the saved ProcessFlow definition, POSTs `/snapshot`, creates an
|
|
213
|
+
`Activate/Publish` version, reads version history, and verifies `active=true`.
|
|
214
|
+
Do not use it in a dry-run: publishing activates the flow.
|
|
215
|
+
|
|
216
|
+
`jsn flows test <identifier> --record <sys_id>` is also a mutating operation.
|
|
217
|
+
It requires confirmation (or `--force`), reads the saved definition, verifies
|
|
218
|
+
that the supplied record exists in the record-trigger table, and POSTs the full
|
|
219
|
+
definition to the ProcessFlow test endpoint. A response with only a context ID
|
|
220
|
+
is reported as accepted, not completed. Test runs execute actions and can have
|
|
221
|
+
side effects; this slice does not execute test flows during development.
|
|
222
|
+
|
|
223
|
+
Flow Logic, subflows, flow-variable lifecycle, and error handling are deferred
|
|
224
|
+
and rejected by this slice rather than silently ignored.
|
|
225
|
+
|
|
226
|
+
`jsn flows update <identifier> --data-file update.json` updates supported fields
|
|
227
|
+
of existing inactive Draft flows. The manifest may contain only `trigger` (with
|
|
228
|
+
`table` and/or `condition`) and `actions`. Each action selects one saved instance
|
|
229
|
+
by its one-based `order` and reusable action `type` (name or sys_id). Its `inputs`
|
|
230
|
+
object may change only inputs already present in that saved Action. The CLI reads
|
|
231
|
+
the saved component identities itself; manifests never carry UI UUIDs. Omitted
|
|
232
|
+
trigger fields, Action inputs, metadata, and other flow components are preserved.
|
|
233
|
+
Names must resolve to exactly one flow, and the protected `jace-test-flow` is
|
|
234
|
+
rejected. Update sends the captured GraphQL `triggerInstances.update` and/or
|
|
235
|
+
`actions.update` shapes, reads the full ProcessFlow definition back, creates an
|
|
236
|
+
Autosave version, and reports update-set captures. It never publishes or
|
|
237
|
+
activates the flow. Unsupported Flow Logic, subflows, variables, error handling,
|
|
238
|
+
new Action inputs, ambiguous selectors, active flows, and non-Draft flows are
|
|
239
|
+
rejected before mutation.
|
|
240
|
+
|
|
98
241
|
`jsn flows executions` reads `sys_flow_context` and returns both the raw row and a normalized `execution` object. JSN discovers the runtime columns from `sys_dictionary` first, then uses these mappings:
|
|
99
242
|
|
|
100
243
|
- `started`: `started`, `start_time`, `started_at`, then `sys_created_on`
|
package/package.json
CHANGED
package/src/commands/_simple.js
CHANGED
|
@@ -45,7 +45,7 @@ async function fetchExtensionChain(sdk, record) {
|
|
|
45
45
|
return chain;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export
|
|
48
|
+
export { actionsCmd } from './actions.js';
|
|
49
49
|
|
|
50
50
|
export const includesCmd = (wrap) => buildDevCmd('includes', 'sys_script_include', ['include', 'si'], ['name', 'api_name', 'active', 'sys_scope'], wrap, { singular: 'script include', scopeValidation: true });
|
|
51
51
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { buildDevCmd } from './_generic.js';
|
|
2
|
+
import { parseDataArg, confirmDelete } from '../helpers.js';
|
|
3
|
+
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
4
|
+
import { declareCapabilities } from '../capabilities.js';
|
|
5
|
+
import { ACTION_TABLE, actionScope, validateScope } from '../processflow-actions.js';
|
|
6
|
+
import { errUsage } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
const scopeOption = y => y.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id; inferred from existing action when omitted' });
|
|
9
|
+
const dataOptions = y => y
|
|
10
|
+
.option('data-file', { type: 'string', describe: 'Full action definition JSON file, not table fields' })
|
|
11
|
+
.option('data', { type: 'string', describe: 'Full action definition as JSON' });
|
|
12
|
+
async function actionID(argv, app) {
|
|
13
|
+
return unwrapSysId(await resolveRecord(app.sdk, { table: ACTION_TABLE, identifier: argv.identifier, matchField: 'name', resource: 'action' }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const actionsCmd = wrap => {
|
|
17
|
+
// Reuse the existing list/show picker and output contract only.
|
|
18
|
+
const base = buildDevCmd('actions', ACTION_TABLE, ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, {
|
|
19
|
+
singular: 'action', readOnly: true,
|
|
20
|
+
showBreadcrumbs: (_record, id) => [
|
|
21
|
+
{ action: 'definition', cmd: `jsn actions definition ${id}`, description: 'Read complete executable definition' },
|
|
22
|
+
{ action: 'list', cmd: 'jsn actions list', description: 'Back to actions' },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
declareCapabilities('actions', { mutationSubcommands: ['create', 'update', 'edit', 'delete', 'test'] });
|
|
26
|
+
// Retain the common wrap and output controller; domain failures get a
|
|
27
|
+
// structured error envelope and nonzero exit, never a success-shaped dispatch.
|
|
28
|
+
const run = handler => wrap(async (argv, app) => {
|
|
29
|
+
try { await handler(argv, app); }
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!error.code?.startsWith('action_')) throw error;
|
|
32
|
+
if (error.details) error.hint = JSON.stringify(error.details);
|
|
33
|
+
app.err(error);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
describe: 'Flow Designer actions: create, edit full definitions, and test with outputs',
|
|
40
|
+
builder: y => base.builder(y)
|
|
41
|
+
.command({
|
|
42
|
+
command: 'create', describe: 'Create parent and executable Process Flow action; verify persistence',
|
|
43
|
+
builder: yy => dataOptions(yy).option('scope', { type: 'string', demandOption: true, describe: 'Exact target scope sys_id, or global' }),
|
|
44
|
+
handler: run(async (argv, app) => {
|
|
45
|
+
const definition = parseDataArg(argv);
|
|
46
|
+
const result = await app.sdk.createProcessFlowAction(argv.scope, definition);
|
|
47
|
+
app.ok(result, { summary: `Created and verified action ${result.sys_id}` });
|
|
48
|
+
}),
|
|
49
|
+
})
|
|
50
|
+
.command({
|
|
51
|
+
command: 'definition <identifier>', describe: 'Read full JSON definition including executable step_instances',
|
|
52
|
+
builder: scopeOption,
|
|
53
|
+
handler: run(async (argv, app) => {
|
|
54
|
+
const id = await actionID(argv, app);
|
|
55
|
+
const scope = await actionScope(app.sdk, id, argv.scope);
|
|
56
|
+
app.ok(await app.sdk.getProcessFlowAction(id, scope), { summary: `Read action definition ${id}` });
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
59
|
+
.command({
|
|
60
|
+
command: 'update <identifier>', aliases: ['edit'], describe: 'Save a full definition through Process Flow PUT and verify readback',
|
|
61
|
+
builder: yy => dataOptions(scopeOption(yy)),
|
|
62
|
+
handler: run(async (argv, app) => {
|
|
63
|
+
const definition = parseDataArg(argv);
|
|
64
|
+
const id = await actionID(argv, app);
|
|
65
|
+
const result = await app.sdk.updateProcessFlowAction(id, argv.scope, definition);
|
|
66
|
+
app.ok(result, { summary: `Saved and verified action ${id}` });
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
.command({
|
|
70
|
+
command: 'test <identifier>', describe: 'Execute saved or supplied definition; optionally wait for actual outputs',
|
|
71
|
+
builder: yy => dataOptions(scopeOption(yy))
|
|
72
|
+
.option('force', { type: 'boolean', default: false, describe: 'Confirm execution of action scripts, which may have side effects' })
|
|
73
|
+
.option('output-map', { type: 'string', default: '{}', describe: 'JSON action input names to values; sent as outputMap' })
|
|
74
|
+
.option('run-on-thread', { type: 'boolean', default: true, describe: 'Run test on a worker thread' })
|
|
75
|
+
.option('tracing-enabled', { type: 'boolean', default: false, describe: 'Enable Flow Designer tracing' })
|
|
76
|
+
.option('wait', { type: 'boolean', default: false, describe: 'Wait for COMPLETE context and actual runtime outputs' })
|
|
77
|
+
.option('timeout', { type: 'number', default: 60, describe: 'Maximum wait seconds after dispatch, greater than 0, at most 3600' }),
|
|
78
|
+
handler: run(async (argv, app) => {
|
|
79
|
+
let outputMap;
|
|
80
|
+
try { outputMap = JSON.parse(argv['output-map'] ?? '{}'); }
|
|
81
|
+
catch { throw errUsage('--output-map must be valid JSON'); }
|
|
82
|
+
const definition = argv['data-file'] || argv.data ? parseDataArg(argv) : undefined;
|
|
83
|
+
const id = await actionID(argv, app);
|
|
84
|
+
await confirmDelete(app, argv, `Execute action '${argv.identifier}' scripts, which may have side effects`);
|
|
85
|
+
const result = await app.sdk.testProcessFlowAction(id, argv.scope, definition, outputMap, {
|
|
86
|
+
wait: argv.wait, timeout: argv.timeout, runOnThread: argv['run-on-thread'], tracingEnabled: argv['tracing-enabled'],
|
|
87
|
+
});
|
|
88
|
+
app.ok(result, { summary: result.status === 'complete' ? `Test completed ${result.context}` : `Test dispatched ${result.context}; outputs not yet verified` });
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
.command({
|
|
92
|
+
command: 'step-types', describe: 'Read step schema templates for a target scope',
|
|
93
|
+
builder: yy => yy.option('scope', { type: 'string', demandOption: true, describe: 'Exact transaction scope sys_id, or global' }),
|
|
94
|
+
handler: run(async (argv, app) => {
|
|
95
|
+
await validateScope(app.sdk, argv.scope);
|
|
96
|
+
app.ok(await app.sdk.getProcessFlowStepTypes(argv.scope), { summary: 'Process Flow step schemas' });
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
.command({
|
|
100
|
+
command: 'delete <identifier>', describe: 'Delete action parent explicitly',
|
|
101
|
+
builder: yy => scopeOption(yy).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
102
|
+
handler: run(async (argv, app) => {
|
|
103
|
+
const id = await actionID(argv, app);
|
|
104
|
+
await actionScope(app.sdk, id, argv.scope);
|
|
105
|
+
await confirmDelete(app, argv, `Delete action '${argv.identifier}'`);
|
|
106
|
+
await app.sdk.delete(ACTION_TABLE, id);
|
|
107
|
+
// Verify the exact target is absent. Do not interpret a 403 as deletion.
|
|
108
|
+
const query = new URLSearchParams({ sysparm_query: `sys_id=${id}`, sysparm_limit: '1', sysparm_fields: 'sys_id', sysparm_display_value: 'false' });
|
|
109
|
+
const response = await app.sdk.request(`${app.sdk.baseURL}/api/now/table/${ACTION_TABLE}?${query}`, { method: 'GET' });
|
|
110
|
+
if (!response || response.error || !Array.isArray(response.result) || response.result.length) {
|
|
111
|
+
throw new Error(`Delete is unverified; action ${id} absence was not explicitly confirmed`);
|
|
112
|
+
}
|
|
113
|
+
app.ok({ name: argv.identifier, sys_id: id, deleted: true }, { summary: `Deleted action ${id}` });
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
handler: wrap(async (_argv, app) => {
|
|
117
|
+
app.ok({ commands: ['list', 'show', 'definition', 'step-types', 'create', 'update', 'test', 'delete'] }, {
|
|
118
|
+
summary: 'Use jsn actions <command> --help. Create/update require full JSON definitions, not table fields.',
|
|
119
|
+
});
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
};
|
package/src/commands/flows.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { formatRecordForDisplay, getStringField, interactiveList, confirmDelete } from '../helpers.js';
|
|
2
3
|
import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../flow-context.js';
|
|
3
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
5
|
import { inspectFlow } from '../flow-inspection.js';
|
|
5
|
-
import { publishFlows, publishDoctor, flowStatus } from '../flow-publish.js';
|
|
6
|
+
import { publishFlows, publishFlowLifecycle, publishDoctor, flowStatus } from '../flow-publish.js';
|
|
6
7
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
8
|
+
import { createFlowFromManifest, updateFlowFromManifest, readCurrentUpdateSet } from '../flow-create.js';
|
|
9
|
+
import { testFlow } from '../flow-test.js';
|
|
7
10
|
|
|
8
11
|
function flowInspectionAdapter(app) {
|
|
9
12
|
return {
|
|
@@ -12,7 +15,7 @@ function flowInspectionAdapter(app) {
|
|
|
12
15
|
};
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish'] });
|
|
18
|
+
declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish', 'test'] });
|
|
16
19
|
|
|
17
20
|
function renderChecks(checks) {
|
|
18
21
|
return checks.map((c) => {
|
|
@@ -179,9 +182,14 @@ export function flowsCmd(wrap) {
|
|
|
179
182
|
resource: argv.action ? 'Custom action' : 'Flow',
|
|
180
183
|
}));
|
|
181
184
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
if (!argv.action) {
|
|
186
|
+
const lifecycle = await publishFlowLifecycle(app.sdk, app.getEffectiveInstance(), sysID);
|
|
187
|
+
return app.ok(lifecycle, {
|
|
188
|
+
summary: `Published ${sysID}; active=${lifecycle.active}; version=${lifecycle.version?.sys_id ?? lifecycle.version?.id ?? 'read back'}`,
|
|
189
|
+
breadcrumbs: [{ action: 'status', cmd: `jsn flows status ${sysID}`, description: 'Verify the flow will run' }],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const result = await publishFlows(app.sdk, [], [sysID]);
|
|
185
193
|
|
|
186
194
|
const { summary, results, via } = result;
|
|
187
195
|
const lines = results.map(r => ` ${r.status === 'success' ? '✅' : '❌'} ${r.flow_name ?? r.sys_id}: ${r.message ?? r.status}${r.errorMessage ? ` — ${r.errorMessage}` : ''}`);
|
|
@@ -208,6 +216,23 @@ export function flowsCmd(wrap) {
|
|
|
208
216
|
});
|
|
209
217
|
}),
|
|
210
218
|
})
|
|
219
|
+
.command({
|
|
220
|
+
command: 'test <identifier>',
|
|
221
|
+
describe: 'Execute a record-triggered flow against one record (may cause side effects)',
|
|
222
|
+
builder: (y) => y
|
|
223
|
+
.option('record', { type: 'string', demandOption: true, describe: 'Trigger record sys_id to test against' })
|
|
224
|
+
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
225
|
+
handler: wrap(async (argv, app) => {
|
|
226
|
+
app.requireInstance();
|
|
227
|
+
await confirmDelete(app, argv, `Execute flow test for ${argv.identifier} against record ${argv.record}; actions may cause side effects`);
|
|
228
|
+
const result = await testFlow(app.sdk, app.getEffectiveInstance(), argv.identifier, argv.record);
|
|
229
|
+
return app.ok(result, {
|
|
230
|
+
summary: result.status === 'completed'
|
|
231
|
+
? `Flow test completed (${result.success ? 'success' : 'failed'})`
|
|
232
|
+
: 'Flow test accepted for execution',
|
|
233
|
+
});
|
|
234
|
+
}),
|
|
235
|
+
})
|
|
211
236
|
.command({
|
|
212
237
|
command: 'doctor',
|
|
213
238
|
describe: 'Check whether this instance can publish flows at all',
|
|
@@ -224,22 +249,56 @@ export function flowsCmd(wrap) {
|
|
|
224
249
|
})
|
|
225
250
|
.command({
|
|
226
251
|
command: 'create',
|
|
227
|
-
describe: 'Create a
|
|
252
|
+
describe: 'Create a draft flow from a schema-driven JSON manifest',
|
|
228
253
|
builder: (y) => y
|
|
229
|
-
.option('data', { type: 'string', describe: '
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
.option('data', { type: 'string', describe: 'Flow manifest as inline JSON' })
|
|
255
|
+
.option('data-file', { type: 'string', describe: 'Read the flow manifest from a JSON file' }),
|
|
256
|
+
handler: wrap(async (argv, app) => {
|
|
257
|
+
app.requireInstance();
|
|
258
|
+
const dataFile = argv.dataFile ?? argv['data-file'];
|
|
259
|
+
if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow manifest');
|
|
260
|
+
let manifest;
|
|
261
|
+
try {
|
|
262
|
+
const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
|
|
263
|
+
manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throw new Error(`Invalid flow manifest JSON: ${error.message}`, { cause: error });
|
|
266
|
+
}
|
|
267
|
+
const result = await createFlowFromManifest(app.sdk, app.getEffectiveInstance(), manifest, {
|
|
268
|
+
readUpdateSet: () => readCurrentUpdateSet(app.sdk),
|
|
269
|
+
});
|
|
270
|
+
app.ok(result, {
|
|
271
|
+
summary: `Created draft flow "${result.flow.name}" (${result.flow.id}) without activation`,
|
|
272
|
+
breadcrumbs: [{ action: 'show', cmd: `jsn flows show ${result.flow.id}`, description: 'Inspect the draft flow' }],
|
|
273
|
+
});
|
|
233
274
|
}),
|
|
234
275
|
})
|
|
235
276
|
.command({
|
|
236
277
|
command: 'update <identifier>',
|
|
237
|
-
describe: '
|
|
278
|
+
describe: 'Safely update supported fields of an existing inactive Draft flow',
|
|
238
279
|
builder: (y) => y
|
|
239
|
-
.option('data', { type: 'string', describe: '
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
280
|
+
.option('data', { type: 'string', describe: 'Update manifest as inline JSON' })
|
|
281
|
+
.option('data-file', { type: 'string', describe: 'Read the update manifest from a JSON file' })
|
|
282
|
+
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
283
|
+
handler: wrap(async (argv, app) => {
|
|
284
|
+
app.requireInstance();
|
|
285
|
+
const dataFile = argv.dataFile ?? argv['data-file'];
|
|
286
|
+
if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow update manifest');
|
|
287
|
+
let manifest;
|
|
288
|
+
try {
|
|
289
|
+
const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
|
|
290
|
+
manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error(`Invalid flow update manifest JSON: ${error.message}`, { cause: error });
|
|
293
|
+
}
|
|
294
|
+
await confirmDelete(app, argv, `Update draft flow ${argv.identifier}; unspecified components and inputs are preserved`);
|
|
295
|
+
const result = await updateFlowFromManifest(app.sdk, app.getEffectiveInstance(), argv.identifier, manifest, {
|
|
296
|
+
readUpdateSet: () => readCurrentUpdateSet(app.sdk),
|
|
297
|
+
});
|
|
298
|
+
return app.ok(result, {
|
|
299
|
+
summary: `Updated draft flow "${result.flow.name}" (${result.flow.id}) without activation`,
|
|
300
|
+
breadcrumbs: [{ action: 'show', cmd: `jsn flows show ${result.flow.id}`, description: 'Inspect the updated draft flow' }],
|
|
301
|
+
});
|
|
243
302
|
}),
|
|
244
303
|
})
|
|
245
304
|
.command({
|
|
@@ -260,9 +319,10 @@ export function flowsCmd(wrap) {
|
|
|
260
319
|
console.log(' show <identifier> Show flow details by name or sys_id');
|
|
261
320
|
console.log(' status <identifier> Check whether a flow is actually able to run');
|
|
262
321
|
console.log(' publish <identifier> Publish (activate) a flow');
|
|
322
|
+
console.log(' test <identifier> Execute a record-triggered flow against one record');
|
|
263
323
|
console.log(' doctor Check whether this instance can publish flows');
|
|
264
|
-
console.log(' create Create a
|
|
265
|
-
console.log(' update <identifier>
|
|
324
|
+
console.log(' create Create a draft flow from a JSON manifest');
|
|
325
|
+
console.log(' update <identifier> Safely update supported fields of an existing Draft flow');
|
|
266
326
|
console.log(' delete <identifier> Delete a flow (not yet implemented)');
|
|
267
327
|
console.log('');
|
|
268
328
|
console.log('Run "jsn flows <command> --help" for details.');
|