@xeplr/workflow-engine 3.2.0 → 3.2.1
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 +13 -2
- package/lib/conditions.js +43 -3
- package/lib/triggers.js +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,8 @@ host gives workflow is the boundary.
|
|
|
117
117
|
function, one written for another event, no answer, or an answer outside
|
|
118
118
|
`targets` **fails the run**, naming the function. Functions registered with an
|
|
119
119
|
`event` are screen functions, never steps: the action catalog leaves them out.
|
|
120
|
-
- **Fan-out**: a matching `each` transition
|
|
120
|
+
- **Fan-out**: a matching `each` transition goes through the list its `source`
|
|
121
|
+
names (`output.rows`, `item.lines`, `params.…`, `trigger.…`) and creates one child run per matching
|
|
121
122
|
element (`item`), recorded in `workflow_run_edges`. With `joinStep` the parent
|
|
122
123
|
waits and resumes at the join once all children are terminal (guarded by an
|
|
123
124
|
atomic `waiting → running` patch); without it the parent finishes.
|
|
@@ -166,7 +167,9 @@ The same rules hold for every kind:
|
|
|
166
167
|
- A trigger seen for the first time, or whose settings changed, is given its next due time and does **not** fire, so publishing never starts a run for a time already past.
|
|
167
168
|
- A due trigger is **claimed** by a conditional update of its due time, so two processes running the tick start it once.
|
|
168
169
|
|
|
169
|
-
**
|
|
170
|
+
**What a designer offers.** `triggers.catalog()` lists every kind as `{ kind, label, description, fields }`, where `fields` are the form's boxes (`{ name, label, placeholder?, help?, required?, options? }`). The API serves it at `GET /workflow/triggers`. A designer builds its trigger form from it, so it never offers a kind this engine refuses (one once offered "A file arrives", which no engine had).
|
|
171
|
+
|
|
172
|
+
**A host's own kinds.** `registerTrigger(kind, { label?, description?, fields?, validate, due?, matches? })`:
|
|
170
173
|
- a time kind gives `due(trigger, from)`, its next due `Date`;
|
|
171
174
|
- an event kind gives `matches(trigger, event)`, and the host calls `triggers.emit(kind, event)` when the event happens.
|
|
172
175
|
|
|
@@ -184,6 +187,14 @@ The comparisons are the flows facade's: `=`, `!=`, `>`, `>=`, `<`, `<=`, `contai
|
|
|
184
187
|
|
|
185
188
|
A bad `when` is refused when it is saved, naming the step and the arrow. That is how a screen of your own (and the jobs screen's "After this job") writes branches without knowing the node format.
|
|
186
189
|
|
|
190
|
+
**An arrow the runner could not follow is refused when it is saved** (`checkArrow`, code `TRANSITION_INVALID`, naming the step and arrow):
|
|
191
|
+
- `mode` is `single` (the default) or `each`;
|
|
192
|
+
- an arrow needs a `target` (a step key, `end_success` or `end_failed`), unless it names a `decide` function;
|
|
193
|
+
- an `each` arrow needs `source`, the list it goes through, starting with `output`, `item`, `params` or `trigger`;
|
|
194
|
+
- an arrow either decides or goes through a list, not both, and `targets` is a list of step keys.
|
|
195
|
+
|
|
196
|
+
Why: each of these used to save, and then the live run ended with "No transition matched". A designer once saved every `each` arrow without its `source`, so nothing ever fanned out.
|
|
197
|
+
|
|
187
198
|
## Jobs as steps and as triggers (`lib/jobs.js`)
|
|
188
199
|
|
|
189
200
|
A job meets a workflow in two places, and both are ordinary workflows:
|
package/lib/conditions.js
CHANGED
|
@@ -62,11 +62,51 @@ function whenToCondition(when, where) {
|
|
|
62
62
|
function compileTransitions(transitions, stepName) {
|
|
63
63
|
if (!Array.isArray(transitions)) return transitions;
|
|
64
64
|
return transitions.map(function(t, i) {
|
|
65
|
-
|
|
65
|
+
var where = (stepName ? 'Step "' + stepName + '"' : 'A step') + ', arrow ' + (i + 1);
|
|
66
|
+
checkArrow(t, where);
|
|
67
|
+
if (t.when === undefined) return t;
|
|
66
68
|
var out = Object.assign({}, t);
|
|
67
|
-
out.condition = whenToCondition(t.when,
|
|
69
|
+
out.condition = whenToCondition(t.when, where);
|
|
68
70
|
return out;
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
// What an arrow may be. Each of these used to save and then fail on the live
|
|
75
|
+
// run instead: an "each" arrow without its list matched nothing, and the run
|
|
76
|
+
// ended with "No transition matched" (workflowRunner's routeAfterStep).
|
|
77
|
+
var ARROW_MODES = ['single', 'each'];
|
|
78
|
+
var SOURCE_ROOTS = ['output', 'item', 'params', 'trigger'];
|
|
79
|
+
|
|
80
|
+
function invalidArrow(message) {
|
|
81
|
+
var err = invalid(message);
|
|
82
|
+
err.code = 'TRANSITION_INVALID';
|
|
83
|
+
err.data = { transitions: [{ message: message, keyword: 'transition' }] };
|
|
84
|
+
return err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Refuses an arrow the runner could not follow, naming the step and arrow. */
|
|
88
|
+
function checkArrow(t, where) {
|
|
89
|
+
if (!t || typeof t !== 'object' || Array.isArray(t)) throw invalidArrow(where + ': an arrow must be an object such as { "target": "next-step" }.');
|
|
90
|
+
if (t.mode !== undefined && t.mode !== null && ARROW_MODES.indexOf(t.mode) === -1) {
|
|
91
|
+
throw invalidArrow(where + ': "' + t.mode + '" is not a kind of arrow. Kinds: ' + ARROW_MODES.join(', ') + '.');
|
|
92
|
+
}
|
|
93
|
+
if (t.decide !== undefined && t.decide !== null) {
|
|
94
|
+
if (typeof t.decide !== 'string' || !t.decide.trim()) throw invalidArrow(where + ': `decide` names a registered function.');
|
|
95
|
+
if (t.mode === 'each') throw invalidArrow(where + ': an arrow either lets a function decide or goes through a list, not both.');
|
|
96
|
+
if (t.targets !== undefined && t.targets !== null &&
|
|
97
|
+
(!Array.isArray(t.targets) || t.targets.some(function(k) { return typeof k !== 'string' || !k; }))) {
|
|
98
|
+
throw invalidArrow(where + ': `targets` is a list of step keys.');
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (typeof t.target !== 'string' || !t.target.trim()) throw invalidArrow(where + ': say where it goes (`target`): a step key, end_success or end_failed.');
|
|
103
|
+
if (t.mode === 'each') {
|
|
104
|
+
var source = typeof t.source === 'string' ? t.source.trim() : '';
|
|
105
|
+
if (!source) throw invalidArrow(where + ': an arrow for each item needs the list to go through (`source`), e.g. "output.rows".');
|
|
106
|
+
if (!FIELD_PATTERN.test(source) || SOURCE_ROOTS.indexOf(source.split('.')[0]) === -1) {
|
|
107
|
+
throw invalidArrow(where + ': "' + source + '" is not a list it can read. Start it with ' + SOURCE_ROOTS.join(', ') + ', e.g. "output.rows".');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { whenToCondition: whenToCondition, compileTransitions: compileTransitions, checkArrow: checkArrow, ARROW_MODES: ARROW_MODES };
|
package/lib/triggers.js
CHANGED
|
@@ -54,6 +54,19 @@ function registerTrigger(kind, def) {
|
|
|
54
54
|
|
|
55
55
|
function kinds() { return Object.keys(KINDS); }
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Every kind, as a designer offers it: { kind, label, description, fields }.
|
|
59
|
+
* `fields` is what the form asks for ({ name, label, placeholder?, help?,
|
|
60
|
+
* required?, options? }). A designer builds its trigger form from this rather
|
|
61
|
+
* than from its own list, so it never offers a kind this engine refuses.
|
|
62
|
+
*/
|
|
63
|
+
function catalog() {
|
|
64
|
+
return Object.keys(KINDS).map(function(k) {
|
|
65
|
+
var d = KINDS[k];
|
|
66
|
+
return { kind: k, label: d.label || k, description: d.description || '', fields: d.fields || [] };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function common(trigger) {
|
|
58
71
|
var out = { kind: trigger.kind };
|
|
59
72
|
if (trigger.enabled === false) out.enabled = false;
|
|
@@ -61,17 +74,26 @@ function common(trigger) {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
registerTrigger('manual', {
|
|
77
|
+
label: 'Someone presses Start',
|
|
64
78
|
description: 'Someone presses Start.',
|
|
79
|
+
fields: [],
|
|
65
80
|
validate: common
|
|
66
81
|
});
|
|
67
82
|
|
|
68
83
|
registerTrigger('api', {
|
|
84
|
+
label: 'Another system calls it',
|
|
69
85
|
description: 'Another system starts it through the API.',
|
|
86
|
+
fields: [],
|
|
70
87
|
validate: common
|
|
71
88
|
});
|
|
72
89
|
|
|
73
90
|
registerTrigger('schedule', {
|
|
91
|
+
label: 'On a schedule',
|
|
74
92
|
description: 'On a schedule — a cron expression, e.g. "0 9 * * 1" for Mondays at 9.',
|
|
93
|
+
fields: [
|
|
94
|
+
{ name: 'cron', label: 'When', placeholder: '0 9 * * 1', help: 'Minute hour day month weekday', required: true },
|
|
95
|
+
{ name: 'timezone', label: 'Time zone', placeholder: 'Asia/Kolkata', help: 'Empty: the server\'s' }
|
|
96
|
+
],
|
|
75
97
|
validate: function(trigger) {
|
|
76
98
|
var cron = String(trigger.cron == null ? '' : trigger.cron).trim();
|
|
77
99
|
if (!cron) throw invalid('A schedule needs `cron`, e.g. "0 9 * * 1" (Mondays at 9:00).');
|
|
@@ -92,7 +114,14 @@ registerTrigger('schedule', {
|
|
|
92
114
|
});
|
|
93
115
|
|
|
94
116
|
registerTrigger('job', {
|
|
117
|
+
label: 'A job finishes',
|
|
95
118
|
description: 'When a job finishes — successfully, with a failure, or either way.',
|
|
119
|
+
fields: [
|
|
120
|
+
{ name: 'jobId', label: 'Job', placeholder: 'the job\'s id', required: true },
|
|
121
|
+
{ name: 'when', label: 'When it', options: [
|
|
122
|
+
{ value: 'success', label: 'succeeds' }, { value: 'failed', label: 'fails' }, { value: 'finished', label: 'finishes either way' }
|
|
123
|
+
] }
|
|
124
|
+
],
|
|
96
125
|
validate: function(trigger) {
|
|
97
126
|
var jobId = String(trigger.jobId == null ? '' : trigger.jobId).trim();
|
|
98
127
|
if (!jobId) throw invalid('A job trigger needs `jobId`: the job whose finish starts this workflow.');
|
|
@@ -286,6 +315,7 @@ async function describe(workflow) {
|
|
|
286
315
|
module.exports = {
|
|
287
316
|
registerTrigger: registerTrigger,
|
|
288
317
|
kinds: kinds,
|
|
318
|
+
catalog: catalog,
|
|
289
319
|
validate: validate,
|
|
290
320
|
isOn: isOn,
|
|
291
321
|
fire: fire,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/workflow-engine",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The free workflow engine: runs, actions as steps, branching, waits, and triggers — manual, API, a schedule, a job finishing. No HTTP: @xeplr/workflow-api puts the routes on top.",
|
|
6
6
|
"keywords": [
|