@xeplr/workflow 1.0.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/LICENSE +21 -0
- package/README.md +352 -0
- package/bin/www +7 -0
- package/db/xcfgSetup.js +8 -0
- package/env.required.js +41 -0
- package/index.js +313 -0
- package/lib/actionCatalog.js +193 -0
- package/lib/actions/jobRun.js +149 -0
- package/lib/actions/screenShow.js +55 -0
- package/lib/db.js +82 -0
- package/lib/envExposed.js +87 -0
- package/lib/flows.js +832 -0
- package/lib/flowsRouter.js +113 -0
- package/lib/router.js +260 -0
- package/lib/workflowRunner.js +649 -0
- package/migrations/0001_companies.sql +23 -0
- package/migrations/0002_workspaces.sql +22 -0
- package/migrations/0003_workflows.sql +35 -0
- package/migrations/0004_workflow_steps.sql +79 -0
- package/migrations/0005_workflow_runs.sql +49 -0
- package/migrations/0006_workflow_step_runs.sql +42 -0
- package/migrations/0007_workflow_resume_keys.sql +42 -0
- package/migrations/0008_workflow_run_edges.sql +43 -0
- package/migrations/0009_workflow_steps_layout.sql +11 -0
- package/migrations/0010_workflow_steps_sample_output.sql +17 -0
- package/migrations/0011_workflow_steps_params.sql +27 -0
- package/migrations/0012_workflows_kind.sql +27 -0
- package/migrations/0013_workflows_key.sql +48 -0
- package/migrations-auth/0001_workflow_access.sql +129 -0
- package/migrations-auth/0003_nav_menus.sql +62 -0
- package/migrations-auth/0004_flows_access.sql +76 -0
- package/models/Company.js +82 -0
- package/models/Workflow.js +63 -0
- package/models/WorkflowResumeKey.js +32 -0
- package/models/WorkflowRun.js +64 -0
- package/models/WorkflowRunEdge.js +49 -0
- package/models/WorkflowStep.js +66 -0
- package/models/WorkflowStepRun.js +49 -0
- package/models/Workspace.js +75 -0
- package/models/index.js +25 -0
- package/orchestration/standalone.js +123 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xeplr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# @xeplr/workflow
|
|
2
|
+
|
|
3
|
+
**Workflows over [`@xeplr/actions`](https://www.npmjs.com/package/@xeplr/actions).** A workflow is a sequence of steps. Each step names a registered action, binds its inputs to what earlier steps produced, and routes to the next — branching on conditions, fanning out over lists, and waiting for a person or a job before it carries on. This package is the workflow document (a workflow and its steps), the engine that runs it, and the HTTP router the builder talks to. The screens are [`@xeplr/ui-workflow`](https://www.npmjs.com/package/@xeplr/ui-workflow).
|
|
4
|
+
|
|
5
|
+
It runs two ways: **embedded**, where a host calls `registerWorkflow()` and mounts the returned router (Xeplr BI does this at `/workflow`, with its own auth and its own `DB_WORKFLOW` database), or **standalone** via `npm run start-api` / the `xeplr-workflow-server` command, which is the same `registerWorkflow()` call with its settings read from `development.env`.
|
|
6
|
+
|
|
7
|
+
| what | how |
|
|
8
|
+
|---|---|
|
|
9
|
+
| A step is an action | Each step names an action registered in `@xeplr/actions`; its form comes from that action's own input schema (`GET /actions`), not a second description of it. Inputs bind to earlier steps' output, the run's parameters, or a setting on an allowlist. |
|
|
10
|
+
| Branch, fan out, join | Conditions are compiled with the same `@xeplr/expression-handler` the engine runs. An *each* transition starts one child run per item, and a join step picks the parent back up once every child has finished. |
|
|
11
|
+
| Wait for a person or a job | A wait step parks the run and issues a single-use resume link — followed from an email or called back by a job — that cannot be claimed until the step is actually waiting. |
|
|
12
|
+
| Screens as steps (flows) | An app's forms, one after another, with arrows that test one field — the `/flows` facade (`lib/flows.js`). |
|
|
13
|
+
| Parameters checked first | A run's parameters are the union of what every step declares, validated before the run is recorded. |
|
|
14
|
+
|
|
15
|
+
What it does not do yet: a step's timeout is saved but not enforced; a draft workflow can still be run; past runs are recorded but there is no screen to browse them.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm i @xeplr/workflow express
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
MIT. Its dependencies are the other `@xeplr/*` packages it builds on — actions, auth, base-apis, db, email, expression-handler, schema-handler, utils.
|
|
24
|
+
|
|
25
|
+
**One repo, two packages.** [`Xeplr/x-flow`](https://github.com/Xeplr/x-flow) holds this package at its root and `@xeplr/ui-workflow` in [`ui/`](ui). Each is installed, tested and released on its own: CI runs once per folder, and a push releases only the package whose files changed — tags `workflow-v<version>` here, `ui-workflow-v<version>` for the screens (the `xeplr` field in each `package.json`).
|
|
26
|
+
|
|
27
|
+
## Run it
|
|
28
|
+
|
|
29
|
+
### Standalone
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm run check-env # fails naming any missing required var
|
|
34
|
+
npm run db:create # creates the DB_API database
|
|
35
|
+
npm run start-api # NODE_ENV=development node ./bin/www — listens on WORKFLOW_PORT
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`start-api` runs this package's migrations on boot, so `migrate:up` is only
|
|
39
|
+
needed to migrate without starting the server.
|
|
40
|
+
|
|
41
|
+
| Script | What it does |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `start-api` | `NODE_ENV=development node ./bin/www` → `orchestration/standalone.js` |
|
|
44
|
+
| `check-env` | `xeplr-check-env` against `development.env` |
|
|
45
|
+
| `db:create` | `xeplr-migrate create-db` for connection `api` (`WORKFLOW_CONNECTION`) |
|
|
46
|
+
| `migrate:up` / `migrate:status` | `xeplr-migrate` over `./migrations` |
|
|
47
|
+
| `db:encrypt` | `xeplr-db-encrypt` — produce an encrypted connection string |
|
|
48
|
+
| `test` | `node test/run.mjs` |
|
|
49
|
+
|
|
50
|
+
Port in `development.env`: **19122** (`WORKFLOW_PORT`). No port literal exists
|
|
51
|
+
in code; a missing `WORKFLOW_PORT` stops the process at `checkEnv`.
|
|
52
|
+
|
|
53
|
+
Required services for standalone:
|
|
54
|
+
|
|
55
|
+
| Service | Why |
|
|
56
|
+
|---|---|
|
|
57
|
+
| PostgreSQL | `DB_API` (this package's tables), `XCFG_DB_NAME` (shared `xeplr_configs`), `EMAIL_DB_NAME` (template store) |
|
|
58
|
+
| An `@xeplr/auth` server + its database | `@xeplr/auth` `attach()` reads users/tenant grants from `AUTH_DB_NAME`; the createApp gate validates tokens against `AUTH_URL`. This package has **no** `start-auth` script — the auth server is run separately (`xeplr-auth-server`), and it applies `migrations-auth/` via `XEPLR_AUTH_MIGRATIONS` |
|
|
59
|
+
|
|
60
|
+
Boot order (`orchestration/standalone.js`): load `<NODE_ENV>.env` → `checkEnv`
|
|
61
|
+
→ `registerApplication(WORKFLOW_APPLICATION_ID || 'xeplr-workflow')` → run
|
|
62
|
+
migrations → `xeplr_configs` ready (**fail-fast**) → register action catalog →
|
|
63
|
+
`@xeplr/auth` attach + email config → email templates (**best-effort**) →
|
|
64
|
+
`registerWorkflow({ port, mountPath: '/', auth: { publicPaths: ['/public/', '/events'] }, ... })`.
|
|
65
|
+
|
|
66
|
+
### Embedded (how Xeplr BI does it)
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// host env.required.js
|
|
70
|
+
...require('@xeplr/workflow').embedRequiredEnv, // ['DB_WORKFLOW']
|
|
71
|
+
|
|
72
|
+
// host startup, after the host's own registerApplication() and registerMTs()
|
|
73
|
+
var { registerWorkflow } = require('@xeplr/workflow');
|
|
74
|
+
var { router } = await registerWorkflow({
|
|
75
|
+
db: { name: process.env.DB_WORKFLOW, connection: hostConnection, connectionName: 'workflow' },
|
|
76
|
+
mtMembershipGate: hostGate,
|
|
77
|
+
authMiddleware: require('@xeplr/auth').authMiddleware
|
|
78
|
+
});
|
|
79
|
+
routes['/workflow'] = router; // into createApp's route map, not app.use() afterwards
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Embedded, `registerWorkflow` creates the database if missing, runs this
|
|
83
|
+
package's migrations (plus `XEPLR_WORKFLOW_MIGRATIONS` or `config.migrations`),
|
|
84
|
+
tries `xeplr_configs` (warns and continues on failure), registers the action
|
|
85
|
+
catalog, and returns the router.
|
|
86
|
+
|
|
87
|
+
### Environment
|
|
88
|
+
|
|
89
|
+
Names and meaning only. Values live in `development.env` — copy
|
|
90
|
+
`development.env.example`, which has every name and no values. It is
|
|
91
|
+
git-ignored (`.gitignore`) because it carries secrets. Never commit it.
|
|
92
|
+
|
|
93
|
+
**Standalone — `env.required.js` (checked at boot):**
|
|
94
|
+
|
|
95
|
+
| Variable | Meaning | Comes from |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `AUTH_URL` | Auth server the createApp gate validates tokens against | `@xeplr/base-apis` `gateRequiredEnv` |
|
|
98
|
+
| `ENCRYPTION_KEY`, `AUTH_JWT_SECRET`, `AUTH_PORT`, `AUTH_DB_NAME`, `XEPLR_AUTH_MIGRATIONS` | Decrypts stored connection strings; auth DB and token settings | `@xeplr/auth` `requiredEnv` (the inline comment in `env.required.js` lists older names) |
|
|
99
|
+
| `EMAIL_PROVIDER`, `BREVO_*` | Outbound mail provider | `@xeplr/email` `requiredEnv` |
|
|
100
|
+
| `EMAIL_DB_NAME` | This app's own email template store | `@xeplr/email` `templatesRequiredEnv` |
|
|
101
|
+
| `XCFG_DB_NAME`, `XCFG_DB_CONNECTION_INFO_ENCRYPTED` | Shared `xeplr_configs` control-plane DB | `@xeplr/actions` `configRequiredEnv` |
|
|
102
|
+
| `DB_API` | This package's database name | app |
|
|
103
|
+
| `WORKFLOW_PORT` | API port | app |
|
|
104
|
+
| `AUTH_SUPER_ADMIN_PASSWORD` | First account's password, consumed by auth's super-admin migration | app |
|
|
105
|
+
|
|
106
|
+
**Read but not in the required list:**
|
|
107
|
+
|
|
108
|
+
| Variable | Meaning |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `WORKFLOW_CONNECTION` | Override for the server login. Normally the shared `XEPLR_DB_CONNECTION` is used; `resolveDbConnection` names both if neither is set |
|
|
111
|
+
| `XEPLR_DB_CONNECTION` | Shared server login every xeplr service reads |
|
|
112
|
+
| `WORKFLOW_APPLICATION_ID` | Application id for rows written to `xeplr_configs`; defaults to `xeplr-workflow` standalone |
|
|
113
|
+
| `LOG_DIR` | Log directory; `./logs` if unset |
|
|
114
|
+
| `NODE_ENV` | Selects `<NODE_ENV>.env` |
|
|
115
|
+
| `WORKFLOW_ENV_EXPOSED` | Comma-separated **exact** env var names step templates may read as `{env.NAME}`. Empty = nothing readable |
|
|
116
|
+
| `WORKFLOW_PUBLIC_URL` | Base URL callers reach this service on, mount path included. Builds `{resumeUrl}`. Exported as `callbackRequiredEnv`; only needed for steps that call out (today `job-run`) |
|
|
117
|
+
| `JOBS_API_URL` | Where the server reaches the jobs API; the jobs canvas binds `job-run.jobsUrl` to `{env.JOBS_API_URL}`, so it must also be named in `WORKFLOW_ENV_EXPOSED` |
|
|
118
|
+
| `XEPLR_WORKFLOW_MIGRATIONS` | Extra migration directories a host adds to workflow's database |
|
|
119
|
+
| `AUTH_DB_CONNECTION_INFO_ENCRYPTED`, `AUTH_ACTIVATION_BASE_URL` (or `AUTH_ACTIVATION_URL`), `AUTH_ACCESS_TOKEN_TTL_MINUTES`, `AUTH_ACCESS_TOKEN_TOLERANCE_SECONDS`, `AUTH_SUPER_ADMIN_EMAIL` | Read by `@xeplr/auth` / `xeplr-auth-server` |
|
|
120
|
+
| `BREVO_API_KEY`, `BREVO_FROM_EMAIL`, `BREVO_FROM_NAME`, `EMAIL_TEST_TO` | Read by `@xeplr/email` |
|
|
121
|
+
| `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_DB` | Redis (commented in `development.env`: defaults `localhost:6379`) |
|
|
122
|
+
|
|
123
|
+
**Embedded — `embedRequiredEnv`:** `DB_WORKFLOW` only. The host passes the
|
|
124
|
+
database name and connection into `registerWorkflow({ db })`; this package
|
|
125
|
+
never reads either variable itself.
|
|
126
|
+
|
|
127
|
+
## Structure
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
xeplr-workflow/
|
|
131
|
+
├─ index.js registerWorkflow, resumeByKey, embedRequiredEnv, callbackRequiredEnv
|
|
132
|
+
├─ bin/www process entry → orchestration/standalone.js
|
|
133
|
+
├─ orchestration/
|
|
134
|
+
│ └─ standalone.js the ONLY file that reads env for config
|
|
135
|
+
├─ env.required.js standalone required-env list
|
|
136
|
+
├─ lib/
|
|
137
|
+
│ ├─ router.js buildWorkflowRouter(config) — all HTTP routes
|
|
138
|
+
│ ├─ workflowRunner.js the engine: startRun, resumeByKey, resolveParams, collectParams
|
|
139
|
+
│ ├─ actionCatalog.js whitelist of @xeplr/actions built-ins + local actions
|
|
140
|
+
│ ├─ actions/jobRun.js local `job-run` action (start a job, park until callback)
|
|
141
|
+
│ ├─ envExposed.js WORKFLOW_ENV_EXPOSED allowlist for {env.*}
|
|
142
|
+
│ └─ db.js connectDb + model(name) bound to workflow's own connection
|
|
143
|
+
├─ db/xcfgSetup.js shared @xeplr/actions attachConfig({ service: 'xeplr-workflow' })
|
|
144
|
+
├─ models/ Company, Workspace, Workflow, WorkflowStep, WorkflowRun,
|
|
145
|
+
│ WorkflowStepRun, WorkflowResumeKey, WorkflowRunEdge
|
|
146
|
+
├─ migrations/ 0001–0012, this package's own database
|
|
147
|
+
├─ migrations-auth/ rows for the AUTH database (roles, menus, apis)
|
|
148
|
+
└─ test/ run.mjs + *.test.mjs
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## API
|
|
152
|
+
|
|
153
|
+
### Exports (`index.js`)
|
|
154
|
+
|
|
155
|
+
| Export | Purpose |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `registerWorkflow(config)` | Connect, create DB if missing, migrate, register actions, build router. Returns `{ router }` (no `app`, no `port`), `{ router, app }` (with `config.app`, mounted at `mountPath`, default `/workflow`), or `{ router, app, server }` (with `config.port`, via `@xeplr/base-apis` `createApp`). `config.db` is always required. Throws if no application id is registered |
|
|
158
|
+
| `resumeByKey(key, output?, opts?)` | Server-side, in-process resume of a run parked on a `wait` step. `opts.status: 'failed'` resolves the step as failed. Throws `err.code` `RESUME_KEY_INVALID` (used/unknown — normal for host routes) or `RESUME_KEY_NOT_READY` (step still running; nothing consumed, retry). Requires `registerWorkflow()` to have run in the process |
|
|
159
|
+
| `embedRequiredEnv` | `['DB_WORKFLOW']` — spread into a host's `env.required.js` |
|
|
160
|
+
| `callbackRequiredEnv` | `['WORKFLOW_PUBLIC_URL']` — only for hosts using steps that call out |
|
|
161
|
+
|
|
162
|
+
`registerWorkflow` config: `app`, `db` (`{ name, connection, connectionName?, mts? }`),
|
|
163
|
+
`applicationId`, `mountPath`, `port`, `appName` (default `xeplr_workflow_api`),
|
|
164
|
+
`mtMembershipGate`, `authMiddleware`, `middleware`, `log`, `auth`, `migrations`.
|
|
165
|
+
Pass `db.mts` only when nothing else in the process has called `registerMTs()`
|
|
166
|
+
(it is process-global).
|
|
167
|
+
|
|
168
|
+
### Routes (`lib/router.js`)
|
|
169
|
+
|
|
170
|
+
Paths are relative to the mount (`/` standalone, `/workflow` in xeplr-bi).
|
|
171
|
+
"Gated" = `config.mtMembershipGate` (a no-op if not supplied).
|
|
172
|
+
|
|
173
|
+
| Method | Path | Gated | Purpose |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| GET | `/` | no | Health: `{ service, status, name }` |
|
|
176
|
+
| GET | `/me` | via `authMiddleware` | Current user; only mounted when `authMiddleware` is passed |
|
|
177
|
+
| GET | `/actions` | yes | Action catalog: `name`, `description`, `inputSchema`, `outputSchema` (never the executor) |
|
|
178
|
+
| GET | `/companies`, `/companies/:id` | **no** | Company `genericRoute` (Company is `multiTenant = false`) |
|
|
179
|
+
| POST | `/companies/save`, `/companies/delete` | **no** | " |
|
|
180
|
+
| GET | `/workspaces`, `/workspaces/:id` | yes | Workspace `genericRoute` |
|
|
181
|
+
| POST | `/workspaces/save`, `/workspaces/delete` | yes | " |
|
|
182
|
+
| GET | `/workflows`, `/workflows/:id` | yes | Workflow list/get with `steps` eager-loaded |
|
|
183
|
+
| POST | `/workflows/save` | yes | Save a workflow and its whole step list in one transaction (changeset) |
|
|
184
|
+
| POST | `/workflows/delete` | yes | `{ ids: [...] }` |
|
|
185
|
+
| POST | `/workflows/:id/run` | yes | `startRun(id, body)`. Runs synchronously to the first wait step or the end; 400 carries `{ message, code, details }` |
|
|
186
|
+
| GET | `/workflows/:id/steps/:stepKey/last-output` | yes | Output of the latest `success`/`waiting` step run for that key. No side effects |
|
|
187
|
+
| POST | `/workflows/:id/steps/try` | yes | **Runs the action for real** with `{ actionName, stepKey, values, params }`, interpolated against other steps' `sampleOutput`. Echoes the resolved `input` |
|
|
188
|
+
| POST | `/public/resume/:key` | **no** | Resume a wait step; body `{ output?, status?, error? }`. The host must also exempt `<mount>/public/*` from its own auth |
|
|
189
|
+
| GET | `/email-templates`, `/email-templates/:name` | yes | From `@xeplr/email` `templatesRouter`; mounted only if templates are initialised |
|
|
190
|
+
| POST | `/email-templates/save`, `/email-templates/delete`, `/email-templates/:name/preview` | yes | " |
|
|
191
|
+
|
|
192
|
+
### Flows — screens as steps (`lib/flows.js`, `lib/flowsRouter.js`)
|
|
193
|
+
|
|
194
|
+
A **flow** is a workflow of kind `screens`: every step shows a screen (a
|
|
195
|
+
`@xeplr/ui-factory` form) to a person and waits for them to submit it, and the
|
|
196
|
+
transitions leaving it decide which screen comes next. The engine does not know
|
|
197
|
+
the difference — a screen step is an ordinary `wait` step whose action is
|
|
198
|
+
`screen-show`, and the submitted values are that step's `output`, so a
|
|
199
|
+
transition reads `output.<field>` exactly as it does anywhere else.
|
|
200
|
+
|
|
201
|
+
A flow is **designed in the app that owns the screens** (Configure UI → Flows),
|
|
202
|
+
through this facade, and addressed by a `key` that app chooses. The workflow
|
|
203
|
+
routes refuse to create or save one — *"This flow is designed in Configure UI"*
|
|
204
|
+
— and the builder shows it read-only. Runs and history are visible as usual.
|
|
205
|
+
|
|
206
|
+
Mounted at `<mount>/flows` by `registerWorkflow`, and returned on its own as
|
|
207
|
+
`flowsRouter` for a host that mounts it elsewhere. Every route is gated.
|
|
208
|
+
|
|
209
|
+
| Method | Path | Right | Body → answer |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| GET | `/flows` | view | → `[{ id, key, name, status, steps }]` (`steps` is a count) |
|
|
212
|
+
| POST | `/flows` | create | `{ key, name }` → the new draft flow |
|
|
213
|
+
| GET | `/flows/:key` | view | → `{ id, key, name, status, steps: [{ stepKey, label, screen, layout, transitions }] }` |
|
|
214
|
+
| PUT | `/flows/:key` | create | `{ name?, steps }` → the flow. Drafts only |
|
|
215
|
+
| POST | `/flows/:key/publish` | create | → the flow, once it checks out (below) |
|
|
216
|
+
| POST | `/flows/:key/runs` | run | → `{ runId, status, stepKey, screen }` |
|
|
217
|
+
| GET | `/flows/runs/:runId` | view | → `{ runId, status, stepKey, screen, values }` — `values` is what that step already holds, so a resumed run reopens filled in |
|
|
218
|
+
| POST | `/flows/runs/:runId/submit` | run | `{ values, recordId? }` → the next `{ status, stepKey, screen }`, or `{ status: 'done' }` |
|
|
219
|
+
| GET | `/flows/:key/runs?mine=1` | view | → runs still going, newest first |
|
|
220
|
+
|
|
221
|
+
**A transition is structured, never a formula**, in and out:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{ "when": { "field": "type", "op": "=", "value": "contractor" }, "target": "contract" }
|
|
225
|
+
{ "when": null, "target": "payroll" }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`op` is one of `= != > >= < <= contains notContains startsWith endsWith in notIn
|
|
229
|
+
between isEmpty isNotEmpty` (the engine's own names, `eq`/`gt`…, are accepted on
|
|
230
|
+
the way in); `in`, `notIn` and `between` take a list. A `target` is a step key,
|
|
231
|
+
`end_success` or `end_failed`.
|
|
232
|
+
|
|
233
|
+
**Publishing checks** that every step names a screen, every target exists, a
|
|
234
|
+
step has at most one catch-all and it comes last, every step leads somewhere
|
|
235
|
+
or ends, and every step is reachable from the first.
|
|
236
|
+
|
|
237
|
+
**The browser never holds a resume key.** Submit is an authenticated call; the
|
|
238
|
+
facade finds the waiting step's key itself and resumes through `resumeByKey`.
|
|
239
|
+
`recordId` — the row the screen wrote into its own table — is kept on the step's
|
|
240
|
+
output, so later steps and their conditions can use it.
|
|
241
|
+
|
|
242
|
+
**A run belongs to the company it was started in.** Another company's flow is
|
|
243
|
+
not there, and its run ids are 404, not 403 — nothing confirms they exist.
|
|
244
|
+
|
|
245
|
+
Rights come from `migrations-auth/0004_flows_access.sql`: submitting is a
|
|
246
|
+
**run** right, not a view right, because it moves the run on and everything
|
|
247
|
+
after it follows.
|
|
248
|
+
|
|
249
|
+
## Data model and migrations
|
|
250
|
+
|
|
251
|
+
Two directories, two databases:
|
|
252
|
+
|
|
253
|
+
| Directory | Target DB | How it runs |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `migrations/` | workflow's own (`DB_API` standalone, `DB_WORKFLOW` in BI) | On every boot inside `registerWorkflow` (and once more in `standalone.js`); idempotent. Also `npm run migrate:up`. Plain `.sql`, ledger-tracked by filename, `precede` order |
|
|
256
|
+
| `migrations-auth/` | the auth database | Not run by this package. Pointed at by `XEPLR_AUTH_MIGRATIONS`; applied by `@xeplr/auth` (`xeplr-auth-server` boot or `xeplr-auth-migrate up`) after auth's own migrations |
|
|
257
|
+
|
|
258
|
+
| Migration | Creates |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `0001_companies` | `companies` (l1 tenant) |
|
|
261
|
+
| `0002_workspaces` | `workspaces` (l2, under a company) |
|
|
262
|
+
| `0003_workflows` | `workflows` — `name`, `description`, `params` (jsonb), `status` (default `draft`) |
|
|
263
|
+
| `0004_workflow_steps` | `workflow_steps` — `stepKey` (unique per workflow), `actionName`, `values`, `kind` (`auto`/`wait`), `timeoutMs`, `onError` (`stop`/`continue`), `transitions`, `joinStep`, `position` |
|
|
264
|
+
| `0005_workflow_runs` | `workflow_runs` — `status` (`queued`/`running`/`waiting`/`success`/`failed`), `params`, `item`, `trigger`, timings, `error` |
|
|
265
|
+
| `0006_workflow_step_runs` | `workflow_step_runs` — copied `stepKey`/`actionName`, resolved `input`, `output`, `error` |
|
|
266
|
+
| `0007_workflow_resume_keys` | `workflow_resume_keys` — unique random `key`, `consumedDate` |
|
|
267
|
+
| `0008_workflow_run_edges` | `workflow_run_edges` — parent → child run for `each` fan-out; `childRunId` unique |
|
|
268
|
+
| `0009` | `workflow_steps.layout` (canvas `{x, y}`) |
|
|
269
|
+
| `0010` | `workflow_steps.sampleOutput` |
|
|
270
|
+
| `0011` | `workflow_steps.params` |
|
|
271
|
+
| `0012` | `workflows.kind` (`workflow` \| `jobs`, default `workflow`) |
|
|
272
|
+
| `migrations-auth/0001_workflow_access` | Roles `Super Admin`, `CompanyAdmin`, `Creator`, `Viewer`; menus `Home`, `Actions`, `Configuration`, `Access Control`; `apis` rows; role → menu/api matrix |
|
|
273
|
+
| `migrations-auth/0003_nav_menus` | Menus `Dashboards`, `Jobs`, `Select Workspace`, `Select Company` (all `workflows:view`) + mappings |
|
|
274
|
+
|
|
275
|
+
Every table carries `mtId1`–`mtId4` and **no `applicationId`**: the database a
|
|
276
|
+
host gives workflow is the boundary.
|
|
277
|
+
|
|
278
|
+
## Engine behaviour (`lib/workflowRunner.js`)
|
|
279
|
+
|
|
280
|
+
- **Step values** are interpolated with `@xeplr/schema-handler` against
|
|
281
|
+
`{ params, item, steps.<key>.output, previous_step.output, env, resumeKey, resumeUrl }`.
|
|
282
|
+
`previous_step`/`steps` come from what ran in this run, not array position.
|
|
283
|
+
- **Routing**: no `transitions` → next step by `position`. With transitions →
|
|
284
|
+
first match wins (`@xeplr/expression-handler`); a blank condition is the
|
|
285
|
+
catch-all; targets are a `stepKey`, `end_success` or `end_failed`. No match
|
|
286
|
+
and no catch-all **fails the run**.
|
|
287
|
+
- **Fan-out**: a matching `each` transition creates one child run per matching
|
|
288
|
+
element (`item`), recorded in `workflow_run_edges`. With `joinStep` the parent
|
|
289
|
+
waits and resumes at the join once all children are terminal (guarded by an
|
|
290
|
+
atomic `waiting → running` patch); without it the parent finishes.
|
|
291
|
+
- **Wait steps**: the resume key row is written **before** the action runs;
|
|
292
|
+
the run parks with status `waiting`. If the action fails the key is marked
|
|
293
|
+
consumed.
|
|
294
|
+
- **Action failure**: `onError: 'continue'` advances by position; otherwise the
|
|
295
|
+
run fails.
|
|
296
|
+
- **Run params**: the run schema is the union of every step's `params` (plus
|
|
297
|
+
legacy `workflows.params` first). Any step requiring a name makes it required;
|
|
298
|
+
otherwise first declaration by position wins. Validated with `applySchema`
|
|
299
|
+
**before** the run row is inserted; undeclared keys are dropped; failure has
|
|
300
|
+
`code: 'PARAMS_INVALID'` and `details`. A workflow that declares nothing
|
|
301
|
+
accepts params unchanged.
|
|
302
|
+
- **Tenant context**: after the first lookup, all DB work runs inside
|
|
303
|
+
`runWithMt()` with the run's stored `mtId`s, because a resume can arrive long
|
|
304
|
+
after the starting request.
|
|
305
|
+
|
|
306
|
+
Not read by the runner: `workflows.status`, `workflows.kind`,
|
|
307
|
+
`workflow_steps.layout`, `workflow_steps.sampleOutput`, `workflow_steps.timeoutMs`.
|
|
308
|
+
|
|
309
|
+
## Rules the code enforces, and why
|
|
310
|
+
|
|
311
|
+
| Rule | Why (from the code comments) |
|
|
312
|
+
|---|---|
|
|
313
|
+
| Action catalog is a **whitelist** (`WANTED`, `LOCAL`, `WANTED_WITH_META`) | Offering a dangerous built-in should be a visible decision in a diff, not a side effect of upgrading. Placeholders in `@xeplr/actions` would appear in the picker and fail when used |
|
|
314
|
+
| `spawnProgram` is deliberately absent (asserted in tests) | It runs arbitrary executables |
|
|
315
|
+
| Placeholders (no `execute`/`name`) are skipped and logged, not thrown | The app works without them; they land when the package implements them |
|
|
316
|
+
| `{env.*}` reaches only names in `WORKFLOW_ENV_EXPOSED`; exact names, no globs | Resolved step input is echoed by `/steps/try` and persisted on step runs, so an unlisted env would let any step author read `ENCRYPTION_KEY` / `AUTH_JWT_SECRET`. An allowlist fails closed when someone adds a new secret |
|
|
317
|
+
| Never put a credential in `WORKFLOW_ENV_EXPOSED` | Secrets belong where the action reads them server-side (e.g. `useCustomConnection: false` with `SMTP_*`) |
|
|
318
|
+
| `WORKFLOW_PUBLIC_URL` is not defaulted | A wrong-but-present callback address makes a run wait forever on work that finished; unset, `job-run` refuses by name |
|
|
319
|
+
| Models are always bound via `db.model(name)`; connection uses `bind: false` | `bindModels` is global in Objection; binding workflow's connection globally re-pointed the host's models at workflow's database |
|
|
320
|
+
| `registerWorkflow` runs its own migrations and action registration | Previously only standalone did, so embedded hosts got missing tables and an empty catalog with no error |
|
|
321
|
+
| `xeplr_configs` is fail-fast standalone, best-effort embedded | Standalone owns its process and should not boot half-configured; embedded, one metadata-writing action should not take down the host's product |
|
|
322
|
+
| An application id must be registered | Rows written to shared `xeplr_configs` are attributed by application |
|
|
323
|
+
| Hosts must not mount the router at `/` | It serves `/companies`, `/workspaces`, `/actions`, which a host already serves |
|
|
324
|
+
| Hosts should take the router, not `app.use()` it onto a built createApp app | createApp adds a catch-all 404 after its routes, so later mounts are unreachable |
|
|
325
|
+
| `/steps/try` is real execution; the UI confirms first | Same `runAction` the engine uses — `email-delete` expunges, `db-push` writes |
|
|
326
|
+
| `/public/resume/:key` is ungated | A resume link is followed from an email client or a job callback with no token |
|
|
327
|
+
| `resumeByKey` refuses (`RESUME_KEY_NOT_READY`) until the step is `waiting`; claim is an atomic conditional patch | The key exists before the action finishes (mail scanners pre-fetch links); resuming mid-action would route the run on while the action is still running |
|
|
328
|
+
| `job-run`: HTTP 409 from the jobs API is a failure, not a retry | A chain must not continue past a movement that never ran |
|
|
329
|
+
| Role matrix: Creator can run, Viewer cannot | A run has side effects (mail, files, database writes) |
|
|
330
|
+
|
|
331
|
+
## Tests
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
npm test # node test/run.mjs — each *.test.mjs in its own process
|
|
335
|
+
node --preserve-symlinks test/runParams.test.mjs # one suite on its own
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Plain scripts, no framework; each prints `ok`/`FAIL` lines and exits non-zero on
|
|
339
|
+
failure. A suite that needs node flags declares them in a `// @flags:` header
|
|
340
|
+
(three use `--preserve-symlinks` so dev-linked `@xeplr/*` packages resolve their
|
|
341
|
+
peers from this app's `node_modules`). No database or network needed; needs
|
|
342
|
+
`npm install` (real `@xeplr/actions` and `@xeplr/schema-handler`).
|
|
343
|
+
|
|
344
|
+
| Suite | Covers |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `actionCatalog.test.mjs` | Ready vs placeholder detection; registered names are kebab-case; `spawnProgram` not offered |
|
|
347
|
+
| `flows.test.mjs` | The `/flows` facade: flows as workflows, steps and arrows, runs and submits (against a fake database) |
|
|
348
|
+
| `envExposed.test.mjs` | Nothing exposed by default; exact, case-sensitive matching; globs fail closed (against real `interpolateAll`) |
|
|
349
|
+
| `runParams.test.mjs` | `resolveParams` / `collectParams`: required, defaults, types, typos, union rules |
|
|
350
|
+
|
|
351
|
+
Not covered by tests: the router, `startRun`/`resumeByKey` against a database,
|
|
352
|
+
migrations, `job-run`.
|
package/bin/www
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// All of this process's startup sequence — env loading, migrations,
|
|
4
|
+
// xeplr_configs, action registration, auth wiring, and finally
|
|
5
|
+
// registerWorkflow() — lives in orchestration/standalone.js now. This file
|
|
6
|
+
// is just the process entry point.
|
|
7
|
+
require('../orchestration/standalone');
|
package/db/xcfgSetup.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Single shared instance of @xeplr/actions' attachConfig() — so bin/www (which
|
|
2
|
+
// calls ready()) and anything else reading control-plane metadata see the same
|
|
3
|
+
// connection. All the bootstrap/bind/metaStore logic lives in @xeplr/actions;
|
|
4
|
+
// this file is just the one place xeplr-workflow holds its instance, naming
|
|
5
|
+
// itself 'xeplr-workflow'.
|
|
6
|
+
var { attachConfig } = require('@xeplr/actions');
|
|
7
|
+
|
|
8
|
+
module.exports = attachConfig({ service: 'xeplr-workflow' });
|
package/env.required.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandatory env vars for the xeplr-workflow API process running STANDALONE.
|
|
3
|
+
* Checked by orchestration/standalone.js at startup and at build (`npm run
|
|
4
|
+
* check-env`). The auth service (xeplr-auth-server) reads its own env directly.
|
|
5
|
+
*
|
|
6
|
+
* NOT the list for an EMBEDDED mount — see `embedRequiredEnv` in index.js. A
|
|
7
|
+
* host app passes workflow's connection and database name as arguments to
|
|
8
|
+
* registerWorkflow({ db }), so none of the connection vars below apply to it,
|
|
9
|
+
* and registerWorkflow never runs this check.
|
|
10
|
+
*
|
|
11
|
+
* Framework var NAMES are owned by the libraries — spread their `requiredEnv`
|
|
12
|
+
* so you never re-list them, and a new lib requirement lands in every app for
|
|
13
|
+
* free. Only APP-SPECIFIC vars are listed literally.
|
|
14
|
+
*
|
|
15
|
+
* Same shape as xeplr-bi's, deliberately: two apps that boot differently are
|
|
16
|
+
* two apps to learn.
|
|
17
|
+
*/
|
|
18
|
+
module.exports = [
|
|
19
|
+
...require('@xeplr/base-apis').gateRequiredEnv, // AUTH_URL — standalone uses createApp's own gate
|
|
20
|
+
...require('@xeplr/auth').requiredEnv, // ENCRYPTION_KEY, JWT_SECRET, ACTIVATION_BASE_URL
|
|
21
|
+
...require('@xeplr/email').requiredEnv, // EMAIL_PROVIDER, BREVO_* (app sends invite mail)
|
|
22
|
+
// This app also OWNS A TEMPLATE STORE (orchestration/standalone.js calls
|
|
23
|
+
// initTemplates). Templates are app-specific — a registration mail is
|
|
24
|
+
// written for one product — so the store is this app's own, not a shared
|
|
25
|
+
// xeplr_email that another product could overwrite by template name.
|
|
26
|
+
...require('@xeplr/email').templatesRequiredEnv, // EMAIL_DB_NAME
|
|
27
|
+
...require('@xeplr/actions').configRequiredEnv, // XCFG_DB_NAME, XCFG_DB_CONNECTION_INFO_ENCRYPTED
|
|
28
|
+
|
|
29
|
+
// App-specific — names this app chose (checked by the API process):
|
|
30
|
+
//
|
|
31
|
+
// WORKFLOW_CONNECTION is NOT here, deliberately: it is an OVERRIDE. The
|
|
32
|
+
// server login normally comes from the shared XEPLR_DB_CONNECTION that every
|
|
33
|
+
// xeplr service reads, so demanding the workflow-specific name would fail a
|
|
34
|
+
// correctly configured install. It still wins when set. Missing-ness is
|
|
35
|
+
// caught at the point of use by resolveDbConnection (see standalone.js),
|
|
36
|
+
// which names both variables. Same call auth and actions already made —
|
|
37
|
+
// xeplr-auth/index.js and @xeplr/actions' attach.js.
|
|
38
|
+
'DB_API', // api database name
|
|
39
|
+
'WORKFLOW_PORT', // api port — NOT hardcoded anywhere
|
|
40
|
+
'AUTH_SUPER_ADMIN_PASSWORD' // consumed by migrations-auth/0002_super_admin.sql
|
|
41
|
+
];
|