@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// screen-show — PARK THE RUN AND LET A PERSON FILL IN A SCREEN.
|
|
2
|
+
//
|
|
3
|
+
// The step behind every node of a 'screens' flow (see lib/flows.js). It is a
|
|
4
|
+
// `wait` step whose action does nothing at all, and the nothing is the whole
|
|
5
|
+
// design:
|
|
6
|
+
//
|
|
7
|
+
// 1. the engine mints the resume key BEFORE the action runs (see
|
|
8
|
+
// workflowRunner's executeFrom) and parks the run at status 'waiting';
|
|
9
|
+
// 2. this returns immediately, so there is nothing to undo and nothing that
|
|
10
|
+
// can fail between "the person opened the screen" and "the run is
|
|
11
|
+
// waiting for them";
|
|
12
|
+
// 3. the person submits; POST /flows/runs/:runId/submit resumes the step
|
|
13
|
+
// with what they typed, and resumeByKey MERGES that onto this output.
|
|
14
|
+
//
|
|
15
|
+
// ── why it returns {} and not { screen } ─────────────────────────────────
|
|
16
|
+
//
|
|
17
|
+
// The step's output IS the submitted values, and nothing else. Transitions on
|
|
18
|
+
// a screens flow read `output.<field>` where <field> is a field of the screen,
|
|
19
|
+
// so anything this action put in the output would occupy a name a screen might
|
|
20
|
+
// legitimately use — a screen with a field called `screen` would then branch on
|
|
21
|
+
// this action's own bookkeeping until the moment it was submitted.
|
|
22
|
+
//
|
|
23
|
+
// Which screen was shown is not lost by returning nothing: the resolved input
|
|
24
|
+
// is persisted on the step run (workflow_step_runs.input), so history records
|
|
25
|
+
// it, and the facade reads the screen off the STEP row (values.screen) rather
|
|
26
|
+
// than off a run, because that is where the flow's design lives.
|
|
27
|
+
//
|
|
28
|
+
// ── why it is not in the action palette ──────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// It is registered (the engine resolves a step's actionName through the
|
|
31
|
+
// registry, so a step could not run without it) but hidden from GET /actions —
|
|
32
|
+
// see lib/actionCatalog.js's HIDDEN. Dropping one onto the ordinary workflow
|
|
33
|
+
// canvas would produce a step that parks forever: nothing outside the flows
|
|
34
|
+
// facade knows how to show a screen or how to resume it.
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
name: 'screen-show',
|
|
38
|
+
description: 'Show a screen to a person and wait for them to submit it. The values they ' +
|
|
39
|
+
'submit become this step\'s output, so a transition can branch on them.',
|
|
40
|
+
requires: [],
|
|
41
|
+
|
|
42
|
+
inputSchema: [
|
|
43
|
+
{ name: 'screen', type: 'string', required: true, order: 1,
|
|
44
|
+
description: 'Key of the screen to show. The app that owns the screens resolves it; ' +
|
|
45
|
+
'this engine only carries it.' }
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
// Whatever the person submitted, which is not knowable from here — declared
|
|
49
|
+
// as nothing rather than as a lie. See the note above.
|
|
50
|
+
outputSchema: null,
|
|
51
|
+
|
|
52
|
+
execute: async function() {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
};
|
package/lib/db.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
var { getConnection, registerMTs } = require('@xeplr/db');
|
|
2
|
+
var models = require('../models');
|
|
3
|
+
|
|
4
|
+
var _registered = false;
|
|
5
|
+
var _conn = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bind workflow's own models to a connection. Config-driven — never reads
|
|
9
|
+
* process.env; that is orchestration/standalone.js's job when nothing else
|
|
10
|
+
* supplies one.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} config
|
|
13
|
+
* @param {string} config.name - database name (workflow's OWN domain data —
|
|
14
|
+
* this is always workflow's responsibility, embedded or standalone; that
|
|
15
|
+
* data belongs to workflow, not whatever it's mounted into)
|
|
16
|
+
* @param {string} config.connection - connection string/encrypted blob, same
|
|
17
|
+
* shape @xeplr/db's getConnection already expects
|
|
18
|
+
* @param {object} [config.mts] - { l1: {name, header}, ... }. registerMTs()
|
|
19
|
+
* is PROCESS-GLOBAL — only pass this when nothing else in this process has
|
|
20
|
+
* already called it. A host app embedding workflow has already registered
|
|
21
|
+
* its own levels; passing config.mts in that case would silently overwrite
|
|
22
|
+
* them. Standalone mode passes it; registerWorkflow's embedded branch does
|
|
23
|
+
* not.
|
|
24
|
+
*/
|
|
25
|
+
async function connectDb(config) {
|
|
26
|
+
config = config || {};
|
|
27
|
+
if (!config.name || !config.connection) {
|
|
28
|
+
throw new Error('connectDb: { name, connection } are required');
|
|
29
|
+
}
|
|
30
|
+
if (config.mts && !_registered) {
|
|
31
|
+
registerMTs(config.mts);
|
|
32
|
+
_registered = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// bind:false — A SECONDARY CONNECTION, never the process's global one.
|
|
36
|
+
//
|
|
37
|
+
// @xeplr/db's bindModels does `Model.knex(instance)` on OBJECTION'S BASE
|
|
38
|
+
// CLASS, so it is global: the last getConnection with bind:true wins for
|
|
39
|
+
// every model in the process, whoever declared it. Standalone that is
|
|
40
|
+
// harmless, because workflow is the only thing here. Embedded it is not —
|
|
41
|
+
// connecting to workflow's own database re-pointed the HOST's models at it
|
|
42
|
+
// too, and the host's own routes started failing on tables that were never
|
|
43
|
+
// in this database.
|
|
44
|
+
//
|
|
45
|
+
// So workflow binds its OWN models to its OWN connection and leaves the
|
|
46
|
+
// global binding alone. Exactly what @xeplr/auth's attach() does to reach
|
|
47
|
+
// the auth database from the api process, and what @xeplr/actions'
|
|
48
|
+
// attachConfig() does for xeplr_configs — this is the third instance of the
|
|
49
|
+
// same shape, not a new idea.
|
|
50
|
+
_conn = await getConnection(config.name, config.connection, {
|
|
51
|
+
bind: false,
|
|
52
|
+
connectionName: config.connectionName || 'workflow'
|
|
53
|
+
});
|
|
54
|
+
return _conn;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function conn() {
|
|
58
|
+
if (!_conn) throw new Error('workflow: connection not ready — await connectDb() first');
|
|
59
|
+
return _conn;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A workflow model BOUND to workflow's own connection.
|
|
64
|
+
*
|
|
65
|
+
* Every query in this package goes through here rather than through the class
|
|
66
|
+
* exported by models/index.js. An unbound class falls back to the global
|
|
67
|
+
* binding, which belongs to whatever else is in this process — the host app,
|
|
68
|
+
* embedded — so it would read the wrong database and mostly report that
|
|
69
|
+
* workflow's tables do not exist.
|
|
70
|
+
*
|
|
71
|
+
* Objection caches per (Model, knex) pair, so calling this per request costs a
|
|
72
|
+
* map lookup rather than a new class.
|
|
73
|
+
*/
|
|
74
|
+
function model(name) {
|
|
75
|
+
var M = models[name];
|
|
76
|
+
if (!M) throw new Error('workflow: unknown model "' + name + '"');
|
|
77
|
+
return M.bindKnex(conn());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = connectDb;
|
|
81
|
+
module.exports.conn = conn;
|
|
82
|
+
module.exports.model = model;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// WHICH ENV VARS A WORKFLOW STEP MAY REFERENCE, as `{env.NAME}`.
|
|
2
|
+
//
|
|
3
|
+
// Nothing, unless named:
|
|
4
|
+
//
|
|
5
|
+
// WORKFLOW_ENV_EXPOSED=PUBLIC_BASE_URL,DEPLOY_TAG
|
|
6
|
+
//
|
|
7
|
+
// AN ALLOWLIST, NOT A DENYLIST, and that is the whole design. A denylist works
|
|
8
|
+
// on the day it is written and fails silently afterwards: the day somebody
|
|
9
|
+
// adds STRIPE_SECRET_KEY to the environment it is readable until a second
|
|
10
|
+
// person remembers to go and hide it. This way a new variable is invisible
|
|
11
|
+
// until it is deliberately published, so the failure mode of forgetting is
|
|
12
|
+
// "the template renders empty" rather than "the key leaked".
|
|
13
|
+
//
|
|
14
|
+
// WHY THIS MATTERS MORE THAN IT LOOKS. A resolved step input is not private:
|
|
15
|
+
//
|
|
16
|
+
// - POST /workflows/:id/steps/try ECHOES the interpolated input straight
|
|
17
|
+
// back to the browser, by design, so the builder can show what it sent
|
|
18
|
+
// (see lib/router.js). The step does not even have to be saved — `values`
|
|
19
|
+
// comes off the request body.
|
|
20
|
+
// - workflowRunner writes the resolved values into WorkflowStepRun.input,
|
|
21
|
+
// where they stay for the life of the run history.
|
|
22
|
+
//
|
|
23
|
+
// So without a list, "can author a workflow step" would silently mean "can
|
|
24
|
+
// read ENCRYPTION_KEY and AUTH_JWT_SECRET" — the two that decrypt every
|
|
25
|
+
// stored connection string and mint a token for any user. Both halves of that
|
|
26
|
+
// escalation look ordinary in an access-control table, which is exactly why
|
|
27
|
+
// it has to be shut off here rather than noticed later.
|
|
28
|
+
//
|
|
29
|
+
// KEEP SECRETS OFF THIS LIST. It is for values that are awkward to hardcode
|
|
30
|
+
// per environment and harmless to read: a public base URL, an environment
|
|
31
|
+
// tag, a bucket name. A credential belongs on the path where the ACTION reads
|
|
32
|
+
// it server-side at execution time and it never enters the template context
|
|
33
|
+
// at all — that is what `useCustomConnection: false` on the email actions
|
|
34
|
+
// does with SMTP_*, and it is strictly safer than any list.
|
|
35
|
+
|
|
36
|
+
// Exact names only. No prefixes and no globs: `PUBLIC_*` reads as a small
|
|
37
|
+
// convenience right up until somebody names a secret PUBLIC_something, and
|
|
38
|
+
// then the list no longer says what it exposes. A literal '*' matches a
|
|
39
|
+
// variable actually called '*', which does not exist — so the tempting
|
|
40
|
+
// shortcut fails closed rather than exposing everything.
|
|
41
|
+
function exposedNames() {
|
|
42
|
+
var raw = process.env.WORKFLOW_ENV_EXPOSED;
|
|
43
|
+
if (!raw) return [];
|
|
44
|
+
return String(raw).split(',')
|
|
45
|
+
.map(function(n) { return n.trim(); })
|
|
46
|
+
.filter(function(n) { return n.length > 0; });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `env` branch of a step's interpolation context.
|
|
51
|
+
*
|
|
52
|
+
* Read per call rather than memoized at load: this module is required before
|
|
53
|
+
* the host has finished loading its .env in at least one boot order, and a
|
|
54
|
+
* snapshot taken then would be permanently empty with nothing to explain it.
|
|
55
|
+
* The cost is splitting a short string once per step.
|
|
56
|
+
*
|
|
57
|
+
* A name on the list that is not set is OMITTED rather than included as
|
|
58
|
+
* undefined — either way `{env.NAME}` renders as empty string, since that is
|
|
59
|
+
* what interpolate() does with any unknown path (see @xeplr/schema-handler's
|
|
60
|
+
* templating.js). Worth knowing when debugging: a misspelled name and an
|
|
61
|
+
* unexposed one look identical in the output.
|
|
62
|
+
*/
|
|
63
|
+
function exposedEnv() {
|
|
64
|
+
var out = {};
|
|
65
|
+
exposedNames().forEach(function(name) {
|
|
66
|
+
if (process.env[name] !== undefined) out[name] = process.env[name];
|
|
67
|
+
});
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Say at boot what is readable, so it is a fact in the log rather than a
|
|
73
|
+
* thing you would have to go and derive from an .env file. A list that
|
|
74
|
+
* accidentally names a secret is only catchable if somebody can see it.
|
|
75
|
+
*/
|
|
76
|
+
function logExposure() {
|
|
77
|
+
var names = exposedNames();
|
|
78
|
+
if (!names.length) return;
|
|
79
|
+
var missing = names.filter(function(n) { return process.env[n] === undefined; });
|
|
80
|
+
console.log('[workflow] {env.*} exposes ' + names.length + ' var(s) to step templates: ' + names.join(', '));
|
|
81
|
+
if (missing.length) {
|
|
82
|
+
console.warn('[workflow] WORKFLOW_ENV_EXPOSED names ' + missing.length +
|
|
83
|
+
' var(s) that are not set (they resolve to empty string): ' + missing.join(', '));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { exposedNames: exposedNames, exposedEnv: exposedEnv, logExposure: logExposure };
|