@xeplr/workflow-engine 3.0.1 → 3.0.2
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
CHANGED
|
@@ -62,7 +62,7 @@ engine/
|
|
|
62
62
|
├─ models/ Company, Workspace, Workflow, WorkflowStep, WorkflowRun,
|
|
63
63
|
│ WorkflowStepRun, WorkflowResumeKey, WorkflowRunEdge,
|
|
64
64
|
│ WorkflowTriggerState
|
|
65
|
-
├─ migrations/ 0003–
|
|
65
|
+
├─ migrations/ 0003–0016, workflow's own tables
|
|
66
66
|
├─ migrations-tenants/ 0001–0002: standalone workflow's companies / workspaces
|
|
67
67
|
└─ test/ run.mjs + *.test.mjs
|
|
68
68
|
```
|
|
@@ -221,11 +221,13 @@ await jobs.init({ /* … */ onFinish: (report) => engine.jobFinished(report) });
|
|
|
221
221
|
|
|
222
222
|
`jobFinished(report)` takes `@xeplr/jobs`' finish envelope. It resumes the `job-run` step that started the job (retrying while that step parks) and starts every workflow the job triggers. Without `startJob`, `job-run` starts jobs over HTTP (`jobsUrl`, and `WORKFLOW_PUBLIC_URL` for the callback).
|
|
223
223
|
|
|
224
|
-
**Job chains are gone.** Migration 0015
|
|
225
|
-
- it gets a `job` trigger (`when: finished`);
|
|
226
|
-
- its `decide` and `job-run` steps keep the same behaviour
|
|
224
|
+
**Job chains are gone.** Migration 0015 converts each "After this job" chain (kind `job-chain`) into an ordinary workflow:
|
|
225
|
+
- it gets a `job` trigger (`when: finished`) and the key `after-job-<jobId>`, the one the jobs screen's shortcut uses;
|
|
226
|
+
- its `decide` and `job-run` steps keep the same behaviour, and each arrow keeps its rule's `when`;
|
|
227
227
|
- conditions and bindings now read `trigger.*`.
|
|
228
228
|
|
|
229
|
+
Migration 0016 repairs a database that ran 0015's first release, which left the key empty and the arrows without `when`. A single comparison gets its `when` back. A nested one still runs as before, and the jobs screen says it was changed elsewhere.
|
|
230
|
+
|
|
229
231
|
## Tests
|
|
230
232
|
|
|
231
233
|
```bash
|
|
@@ -245,7 +247,8 @@ No database or network needed, except `triggers.test.mjs`, which runs on a real
|
|
|
245
247
|
- a loop stopped
|
|
246
248
|
- a workflow that cannot start, recorded as a skip
|
|
247
249
|
- the schedule's first sight, its due time, two ticks starting it once, a changed cron
|
|
248
|
-
- migration 0015 turning an old job chain into a workflow that runs
|
|
250
|
+
- migration 0015 turning an old job chain into a workflow that runs, with its key and `when` arrows
|
|
251
|
+
- migration 0016 repairing one converted early, and changing nothing on a second run |
|
|
249
252
|
| `actionCatalog.test.mjs` | Ready vs placeholder detection; registered names are kebab-case; `spawnProgram` not offered; `decide` and `job-run` offered |
|
|
250
253
|
| `envExposed.test.mjs` | Nothing exposed by default; exact, case-sensitive matching; globs fail closed (against real `interpolateAll`) |
|
|
251
254
|
| `requires.test.mjs` | Every relative `require` in this package points at a file (Node only notices a missing one when the line runs; a bundler refuses it up front) |
|
|
@@ -53,8 +53,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS "workflow_trigger_states_workflow_unique" ON "
|
|
|
53
53
|
-- kind 'job-chain' → 'workflow', with trigger
|
|
54
54
|
-- { kind: 'job', jobId, when: 'finished' }
|
|
55
55
|
-- (the decide step still routes on status)
|
|
56
|
-
-- key 'job-chain-<jobId>' →
|
|
57
|
-
--
|
|
56
|
+
-- key 'job-chain-<jobId>' → 'after-job-<jobId>', the key the jobs
|
|
57
|
+
-- screen's "After this job" shortcut keeps
|
|
58
|
+
-- 'job-chain-decide' step → 'decide' (a plain branching step), each
|
|
59
|
+
-- arrow given its rule's `when` as written
|
|
58
60
|
-- 'job-chain-next' steps → 'job-run', which carries the period on
|
|
59
61
|
-- conditions on params.x → trigger.x (status, output, window, carry…)
|
|
60
62
|
-- bindings {output.x} etc. → {trigger.output.x}
|
|
@@ -65,7 +67,17 @@ CREATE UNIQUE INDEX IF NOT EXISTS "workflow_trigger_states_workflow_unique" ON "
|
|
|
65
67
|
UPDATE "workflow_steps" s
|
|
66
68
|
SET "actionName" = 'decide',
|
|
67
69
|
"values" = '{}'::jsonb,
|
|
68
|
-
|
|
70
|
+
-- Arrow N is rule N, so it takes that rule's `when` (fields now read
|
|
71
|
+
-- the trigger); the closing "nothing matched" arrow is "otherwise".
|
|
72
|
+
"transitions" = (
|
|
73
|
+
SELECT COALESCE(jsonb_agg(
|
|
74
|
+
CASE WHEN x.ord <= COALESCE(jsonb_array_length(s."values"->'rules'), 0)
|
|
75
|
+
THEN x.t || jsonb_build_object('when', regexp_replace(
|
|
76
|
+
COALESCE(s."values"->'rules'->((x.ord - 1)::int)->'when', 'null'::jsonb)::text,
|
|
77
|
+
'"field"\s*:\s*"(status|error|output|carry|window|jobId|given|used)', '"field": "trigger.\1', 'g')::jsonb)
|
|
78
|
+
ELSE x.t || '{"when": null}'::jsonb END
|
|
79
|
+
ORDER BY x.ord), '[]'::jsonb)
|
|
80
|
+
FROM jsonb_array_elements(replace(s."transitions"::text, '"params.', '"trigger.')::jsonb) WITH ORDINALITY AS x(t, ord))
|
|
69
81
|
FROM "workflows" w
|
|
70
82
|
WHERE w."id" = s."workflowId" AND w."kind" = 'job-chain' AND s."actionName" = 'job-chain-decide';
|
|
71
83
|
|
|
@@ -81,5 +93,5 @@ UPDATE "workflow_steps" s
|
|
|
81
93
|
UPDATE "workflows"
|
|
82
94
|
SET "trigger" = jsonb_build_object('kind', 'job', 'jobId', substring("key" from 11), 'when', 'finished'),
|
|
83
95
|
"kind" = 'workflow',
|
|
84
|
-
"key" =
|
|
96
|
+
"key" = 'after-job-' || substring("key" from 11)
|
|
85
97
|
WHERE "kind" = 'job-chain' AND "key" LIKE 'job-chain-%';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- 0016_after_job_shortcut.sql
|
|
2
|
+
--
|
|
3
|
+
-- CONVERTED CHAINS, READABLE BY THE JOBS SCREEN AGAIN.
|
|
4
|
+
--
|
|
5
|
+
-- The first release of 0015 converted each job chain into an ordinary
|
|
6
|
+
-- workflow, but with no key and with arrows holding only their compiled
|
|
7
|
+
-- conditions. The jobs screen's "After this job" finds a job's rules by the
|
|
8
|
+
-- key `after-job-<jobId>` and reads each arrow's `when`, so a converted chain
|
|
9
|
+
-- showed as "changed in the workflow screens" instead of as its rules. 0015
|
|
10
|
+
-- now does both; this repairs a database that ran it before it did. On any
|
|
11
|
+
-- other database it changes nothing.
|
|
12
|
+
--
|
|
13
|
+
-- (No jsonb key-exists operator anywhere: the migrator's driver takes that
|
|
14
|
+
-- character as a parameter placeholder. jsonb_typeof(x->'k') is NULL exactly
|
|
15
|
+
-- when k is absent.)
|
|
16
|
+
--
|
|
17
|
+
-- 1. The key, for a converted chain that has none (and only if the job has
|
|
18
|
+
-- no shortcut already).
|
|
19
|
+
UPDATE "workflows" w
|
|
20
|
+
SET "key" = 'after-job-' || (w."trigger"->>'jobId')
|
|
21
|
+
WHERE w."key" IS NULL
|
|
22
|
+
AND w."trigger"->>'kind' = 'job'
|
|
23
|
+
AND w."description" LIKE 'Made from the "After this job" rules of job %'
|
|
24
|
+
AND NOT EXISTS (
|
|
25
|
+
SELECT 1 FROM "workflows" o
|
|
26
|
+
WHERE o."key" = 'after-job-' || (w."trigger"->>'jobId')
|
|
27
|
+
AND COALESCE(o."mtId1", '') = COALESCE(w."mtId1", '')
|
|
28
|
+
AND COALESCE(o."mtId2", '') = COALESCE(w."mtId2", ''));
|
|
29
|
+
|
|
30
|
+
-- 2. `when` on each of their decide step's arrows, rebuilt from the compiled
|
|
31
|
+
-- condition: none is "otherwise"; a single comparison is { field, op,
|
|
32
|
+
-- value } in the jobs screen's own words. A nested all/any is left as it
|
|
33
|
+
-- is — the jobs screen then says it was changed elsewhere, and it still
|
|
34
|
+
-- runs exactly as before.
|
|
35
|
+
UPDATE "workflow_steps" s
|
|
36
|
+
SET "transitions" = (
|
|
37
|
+
SELECT COALESCE(jsonb_agg(
|
|
38
|
+
CASE
|
|
39
|
+
WHEN jsonb_typeof(x.t->'when') IS NOT NULL THEN x.t
|
|
40
|
+
WHEN jsonb_typeof(x.t->'condition') IS NULL OR jsonb_typeof(x.t->'condition') = 'null' THEN x.t || '{"when": null}'::jsonb
|
|
41
|
+
WHEN jsonb_typeof(x.t->'condition'->'left') IS NOT NULL AND (CASE x.t->'condition'->>'op'
|
|
42
|
+
WHEN 'eq' THEN '=' WHEN 'neq' THEN '!=' WHEN 'gt' THEN '>' WHEN 'gte' THEN '>='
|
|
43
|
+
WHEN 'lt' THEN '<' WHEN 'lte' THEN '<=' WHEN 'contains' THEN 'contains'
|
|
44
|
+
WHEN 'notContains' THEN 'notContains' WHEN 'startsWith' THEN 'startsWith'
|
|
45
|
+
WHEN 'endsWith' THEN 'endsWith' WHEN 'in' THEN 'in' WHEN 'notIn' THEN 'notIn'
|
|
46
|
+
WHEN 'between' THEN 'between' WHEN 'isNull' THEN 'isEmpty' WHEN 'isNotNull' THEN 'isNotEmpty'
|
|
47
|
+
END) IS NOT NULL
|
|
48
|
+
THEN x.t || jsonb_build_object('when', jsonb_strip_nulls(jsonb_build_object(
|
|
49
|
+
'field', x.t->'condition'->'left'->>'field',
|
|
50
|
+
'op', CASE x.t->'condition'->>'op'
|
|
51
|
+
WHEN 'eq' THEN '=' WHEN 'neq' THEN '!=' WHEN 'gt' THEN '>' WHEN 'gte' THEN '>='
|
|
52
|
+
WHEN 'lt' THEN '<' WHEN 'lte' THEN '<=' WHEN 'contains' THEN 'contains'
|
|
53
|
+
WHEN 'notContains' THEN 'notContains' WHEN 'startsWith' THEN 'startsWith'
|
|
54
|
+
WHEN 'endsWith' THEN 'endsWith' WHEN 'in' THEN 'in' WHEN 'notIn' THEN 'notIn'
|
|
55
|
+
WHEN 'between' THEN 'between' WHEN 'isNull' THEN 'isEmpty' WHEN 'isNotNull' THEN 'isNotEmpty'
|
|
56
|
+
END,
|
|
57
|
+
'value', x.t->'condition'->'right'->'value')))
|
|
58
|
+
ELSE x.t
|
|
59
|
+
END ORDER BY x.ord), '[]'::jsonb)
|
|
60
|
+
FROM jsonb_array_elements(s."transitions") WITH ORDINALITY AS x(t, ord))
|
|
61
|
+
FROM "workflows" w
|
|
62
|
+
WHERE w."id" = s."workflowId"
|
|
63
|
+
AND w."key" LIKE 'after-job-%'
|
|
64
|
+
AND s."stepKey" = 'decide' AND s."actionName" = 'decide'
|
|
65
|
+
AND s."transitions" IS NOT NULL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/workflow-engine",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The workflow engine — workflow's own database and tables, the action catalog, starting and resuming runs, and the flows facade. No HTTP: @xeplr/workflow-api puts the routes on top.",
|
|
6
6
|
"main": "index.js",
|