@xeplr/workflow-engine 3.0.1 → 3.0.3

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–0015, workflow's own tables
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 converted each "After this job" chain (kind `job-chain`) into an ordinary workflow:
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>' → none; the trigger says which job
57
- -- 'job-chain-decide' step → 'decide' (a plain branching step)
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
- "transitions" = replace(s."transitions"::text, '"params.', '"trigger.')::jsonb
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" = NULL
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,8 +1,17 @@
1
1
  {
2
2
  "name": "@xeplr/workflow-engine",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "license": "MIT",
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.",
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
+ "keywords": [
7
+ "workflow",
8
+ "orchestration",
9
+ "automation",
10
+ "triggers",
11
+ "cron",
12
+ "jobs",
13
+ "engine"
14
+ ],
6
15
  "main": "index.js",
7
16
  "files": [
8
17
  "index.js",