@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,113 @@
|
|
|
1
|
+
// THE FLOWS FACADE, as HTTP.
|
|
2
|
+
//
|
|
3
|
+
// Nothing but plumbing: each route reads its parameters, calls one function in
|
|
4
|
+
// lib/flows.js, and answers. Everything that could be wrong — a comparison the
|
|
5
|
+
// engine does not have, a transition pointing at nothing, a run belonging to
|
|
6
|
+
// another company — is decided there, where it can be tested without a socket.
|
|
7
|
+
//
|
|
8
|
+
// ── what this answers with, and why it is not `dataArray` ───────────────
|
|
9
|
+
//
|
|
10
|
+
// Every other route in this package answers `{ dataArray: [...] }`, because
|
|
11
|
+
// every other route is talked to by this product's own builder through
|
|
12
|
+
// @xeplr/base-apis' generic controller. This one is a FACADE for a different
|
|
13
|
+
// app: it hands back the object the caller asked about, and an error as the
|
|
14
|
+
// same `{ message, code, details }` body the rest of the package already uses.
|
|
15
|
+
// A client of this router should not have to learn one product's response
|
|
16
|
+
// envelope to ask what screen a run is on.
|
|
17
|
+
//
|
|
18
|
+
// ── the gate ────────────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// Applied per route here rather than at the mount, so this router carries its
|
|
21
|
+
// own authorization wherever it is mounted — inside the workflow router at
|
|
22
|
+
// <mount>/flows, or by a host at a path of its own. See buildWorkflowRouter's
|
|
23
|
+
// note on the same `config.mtMembershipGate`.
|
|
24
|
+
|
|
25
|
+
var express = require('express');
|
|
26
|
+
var flows = require('./flows');
|
|
27
|
+
|
|
28
|
+
function noop(req, res, next) { next(); }
|
|
29
|
+
|
|
30
|
+
// One shape for every refusal: the status the service decided, and the body
|
|
31
|
+
// the rest of this package already answers errors with. `code` is what a
|
|
32
|
+
// client branches on — FLOW_NOT_FOUND, FLOW_PUBLISHED, RUN_NOT_WAITING — and
|
|
33
|
+
// `message` is the sentence somebody reads.
|
|
34
|
+
function sendError(res, err) {
|
|
35
|
+
res.status(err.status || 400).json({
|
|
36
|
+
message: err.message,
|
|
37
|
+
code: err.code || null,
|
|
38
|
+
details: err.details || []
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handle(fn) {
|
|
43
|
+
return async function(req, res) {
|
|
44
|
+
try {
|
|
45
|
+
var result = await fn(req);
|
|
46
|
+
if (result && result.created) return res.status(201).json(result.body);
|
|
47
|
+
res.json(result);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
sendError(res, err);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} [config]
|
|
56
|
+
* @param {Function} [config.mtMembershipGate] - the same gate the rest of this
|
|
57
|
+
* package's authenticated routes use. Omit to leave every route ungated,
|
|
58
|
+
* which is only ever right for local dev.
|
|
59
|
+
* @returns {import('express').Router}
|
|
60
|
+
*/
|
|
61
|
+
function buildFlowsRouter(config) {
|
|
62
|
+
config = config || {};
|
|
63
|
+
var gate = config.mtMembershipGate || noop;
|
|
64
|
+
var router = express.Router();
|
|
65
|
+
|
|
66
|
+
// BEFORE /:key. A flow keyed "runs" would otherwise shadow these two, which
|
|
67
|
+
// is why normaliseKey reserves the word rather than leaving it to routing
|
|
68
|
+
// order to decide.
|
|
69
|
+
router.get('/runs/:runId', gate, handle(function(req) {
|
|
70
|
+
return flows.getRun(req.params.runId);
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// The browser never handles a resume key — see flows.submitRun. This is an
|
|
74
|
+
// ordinary gated route that looks the key up from the run it was given.
|
|
75
|
+
router.post('/runs/:runId/submit', gate, handle(function(req) {
|
|
76
|
+
return flows.submitRun(req.params.runId, req.body || {});
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
router.get('/', gate, handle(function() {
|
|
80
|
+
return flows.listFlows();
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
router.post('/', gate, handle(async function(req) {
|
|
84
|
+
return { created: true, body: await flows.createFlow(req.body || {}) };
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
router.get('/:key', gate, handle(function(req) {
|
|
88
|
+
return flows.getFlow(req.params.key);
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
router.put('/:key', gate, handle(function(req) {
|
|
92
|
+
return flows.putFlow(req.params.key, req.body || {});
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
router.post('/:key/publish', gate, handle(function(req) {
|
|
96
|
+
return flows.publishFlow(req.params.key);
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
router.post('/:key/runs', gate, handle(async function(req) {
|
|
100
|
+
return { created: true, body: await flows.startFlowRun(req.params.key, req.body || {}, req.user) };
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// ?mine=1 — started by whoever is asking. Anything else is every run of this
|
|
104
|
+
// flow that is still going, within the caller's own tenant.
|
|
105
|
+
router.get('/:key/runs', gate, handle(function(req) {
|
|
106
|
+
var mine = req.query && (req.query.mine === '1' || req.query.mine === 'true');
|
|
107
|
+
return flows.listRuns(req.params.key, { mine: mine, userId: req.user && req.user.id });
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
return router;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = buildFlowsRouter;
|
package/lib/router.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var { genericRoute } = require('@xeplr/base-apis');
|
|
3
|
+
// Both used ONLY by the builder's try-run route below, and both are the same
|
|
4
|
+
// calls the engine itself makes (see workflowRunner) — deliberately, so a
|
|
5
|
+
// try-run resolves references and validates input exactly the way a real run
|
|
6
|
+
// would, rather than approximating it.
|
|
7
|
+
var { runAction } = require('@xeplr/actions');
|
|
8
|
+
var { interpolateAll } = require('@xeplr/schema-handler');
|
|
9
|
+
var db = require('./db');
|
|
10
|
+
var actionCatalog = require('./actionCatalog');
|
|
11
|
+
var workflowRunner = require('./workflowRunner');
|
|
12
|
+
var flows = require('./flows');
|
|
13
|
+
var buildFlowsRouter = require('./flowsRouter');
|
|
14
|
+
var { exposedEnv } = require('./envExposed');
|
|
15
|
+
|
|
16
|
+
function noop(req, res, next) { next(); }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the workflow HTTP router. Pure config in, Express Router out —
|
|
20
|
+
* NEVER reads process.env FOR ITS CONFIGURATION and never requires
|
|
21
|
+
* @xeplr/auth directly, so the exact same router works whether
|
|
22
|
+
* orchestration/standalone.js built it for a solo process, or a host app's
|
|
23
|
+
* own registerWorkflow() call built it to mount into itself.
|
|
24
|
+
*
|
|
25
|
+
* The one process.env touch is inside the try-run handler: exposedEnv()
|
|
26
|
+
* resolves `{env.NAME}` bindings, and it is deliberately NOT lifted up here
|
|
27
|
+
* into config. It is per-request runtime DATA, identical embedded and
|
|
28
|
+
* standalone, and reading it at build time would freeze whatever the
|
|
29
|
+
* environment happened to be when the router was constructed. The invariant
|
|
30
|
+
* this comment protects is that no BEHAVIOUR differs between the two mounts,
|
|
31
|
+
* and that still holds.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} [config]
|
|
34
|
+
* @param {Function} [config.mtMembershipGate] - middleware checking THIS
|
|
35
|
+
* product's own apis/menus grants (see
|
|
36
|
+
* migrations-auth/0001_workflow_access.sql). Distinct from basic
|
|
37
|
+
* authentication — by the time a request reaches here, something upstream
|
|
38
|
+
* (a host app's own middleware, or this package's own standalone
|
|
39
|
+
* orchestration) has already established WHO the request is from; this
|
|
40
|
+
* checks whether that identity is allowed to hit THIS specific route.
|
|
41
|
+
* Omit to leave every route ungated — fine for local, unauthenticated dev
|
|
42
|
+
* use, never for anything actually reachable by anyone else.
|
|
43
|
+
* @param {Function} [config.authMiddleware] - populates req.user; used only
|
|
44
|
+
* by GET /me as an "is auth actually wired" smoke check.
|
|
45
|
+
* @returns {import('express').Router}
|
|
46
|
+
*/
|
|
47
|
+
function buildWorkflowRouter(config) {
|
|
48
|
+
config = config || {};
|
|
49
|
+
var gate = config.mtMembershipGate || noop;
|
|
50
|
+
var router = express.Router();
|
|
51
|
+
|
|
52
|
+
// BOUND to workflow's own connection, and resolved HERE rather than at
|
|
53
|
+
// require time — connectDb has run by the time registerWorkflow calls this,
|
|
54
|
+
// and a class captured before it would carry the process's global binding
|
|
55
|
+
// instead. See lib/db.js's model().
|
|
56
|
+
var Company = db.model('Company');
|
|
57
|
+
var Workspace = db.model('Workspace');
|
|
58
|
+
var Workflow = db.model('Workflow');
|
|
59
|
+
var WorkflowStep = db.model('WorkflowStep');
|
|
60
|
+
|
|
61
|
+
router.get('/', function(req, res) {
|
|
62
|
+
res.json({ service: 'api', status: 'running', name: 'xeplr-workflow-api' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (config.authMiddleware) {
|
|
66
|
+
router.get('/me', config.authMiddleware, function(req, res) {
|
|
67
|
+
res.json({ id: req.user.id, email: req.user.email, name: req.user.name, roles: req.user.roles || [] });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── the action catalog ───────────────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// Served from the registry itself rather than from a list kept beside it —
|
|
74
|
+
// a step editor's form is generated from these schemas, so a second
|
|
75
|
+
// description of an action is a second thing to keep in step.
|
|
76
|
+
router.get('/actions', gate, function(req, res) {
|
|
77
|
+
res.json({ dataArray: actionCatalog.catalog() });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── tenancy ──────────────────────────────────────────────────────────────
|
|
81
|
+
//
|
|
82
|
+
// NO gate on /companies — Company opted out at the model level
|
|
83
|
+
// (multiTenant = false), and gating it would 403 the "pick a company"
|
|
84
|
+
// screen whenever a header is present but not yet authorized.
|
|
85
|
+
router.use('/companies', genericRoute({ key: 'company', model: Company }));
|
|
86
|
+
router.use('/workspaces', gate, genericRoute({ key: 'workspace', model: Workspace }));
|
|
87
|
+
|
|
88
|
+
// ── the workflow document ───────────────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// One POST /workflows/save writes the workflow and its whole step list in
|
|
91
|
+
// one transaction. GET /workflows/:id comes back with `steps` eager-loaded.
|
|
92
|
+
//
|
|
93
|
+
// BEFORE the genericRoute mount, and only on /save: a workflow of kind
|
|
94
|
+
// 'screens' is GENERATED from a design held in another app (see lib/flows.js
|
|
95
|
+
// and the /flows mount below), so an edit made here survives only until the
|
|
96
|
+
// next PUT /flows/:key and then disappears without a record. Reading one is
|
|
97
|
+
// untouched and so is deleting one — runs and history stay visible, and a
|
|
98
|
+
// flow nobody wants any more must still be removable from the list it is in.
|
|
99
|
+
router.post('/workflows/save', gate, flows.refuseScreensEdit);
|
|
100
|
+
router.use('/workflows', gate, genericRoute({
|
|
101
|
+
key: 'workflow',
|
|
102
|
+
model: Workflow,
|
|
103
|
+
children: [{ key: 'steps', model: WorkflowStep, foreignKey: 'workflowId' }]
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// `details` is passed through when the engine refuses on the workflow's own
|
|
107
|
+
// declared params — [{ field, message }], the same shape applySchema throws
|
|
108
|
+
// everywhere else. Without it the dialog can only print one sentence; with
|
|
109
|
+
// it, it can mark the field that is actually missing.
|
|
110
|
+
router.post('/workflows/:id/run', gate, async function(req, res) {
|
|
111
|
+
try {
|
|
112
|
+
var run = await workflowRunner.startRun(req.params.id, req.body || {}, { user: req.user });
|
|
113
|
+
res.json({ dataArray: [run] });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
res.status(400).json({ message: err.message, code: err.code, details: err.details || [] });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── flows: a screen is a step ───────────────────────────────────────────
|
|
120
|
+
//
|
|
121
|
+
// A small facade over the same engine for an app that designs screens: it
|
|
122
|
+
// says "this screen, then that one if the answer was X", and never learns
|
|
123
|
+
// what a workflow, a wait step or a resume key is. See lib/flows.js.
|
|
124
|
+
//
|
|
125
|
+
// Mounted HERE, inside this router, so a host that already mounts
|
|
126
|
+
// registerWorkflow()'s router gets <mount>/flows with no further wiring.
|
|
127
|
+
// registerWorkflow ALSO returns it on its own (`flowsRouter`) for a host
|
|
128
|
+
// that would rather serve it from a path of its own — it is the same
|
|
129
|
+
// builder, gating itself, so both mounts behave identically.
|
|
130
|
+
router.use('/flows', buildFlowsRouter(config));
|
|
131
|
+
|
|
132
|
+
// ── email templates ─────────────────────────────────────────────────────
|
|
133
|
+
//
|
|
134
|
+
// Served FROM @xeplr/email, not reimplemented here: the templates belong to
|
|
135
|
+
// the email service, which is what auth and jobs also send through. This
|
|
136
|
+
// mount just makes them reachable from the workflow builder, so an
|
|
137
|
+
// email-send step can offer a dropdown of real template names instead of
|
|
138
|
+
// asking somebody to type one exactly right.
|
|
139
|
+
//
|
|
140
|
+
// Conditional because templates are optional: an install that never called
|
|
141
|
+
// initTemplates() serves no such routes rather than serving routes that
|
|
142
|
+
// always 500.
|
|
143
|
+
try {
|
|
144
|
+
var emailPkg = require('@xeplr/email');
|
|
145
|
+
if (emailPkg.templatesReady && emailPkg.templatesReady()) {
|
|
146
|
+
router.use(emailPkg.templatesRouter({ auth: gate }));
|
|
147
|
+
}
|
|
148
|
+
} catch (_) {
|
|
149
|
+
// @xeplr/email not installed — nothing to mount, and the email-send
|
|
150
|
+
// action says so specifically if a step names a template.
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── filling in a step's SAMPLE OUTPUT ───────────────────────────────────
|
|
154
|
+
//
|
|
155
|
+
// Two ways, because they trade off differently and the builder offers both.
|
|
156
|
+
//
|
|
157
|
+
// The SAFE one: whatever this step actually returned the last time a real
|
|
158
|
+
// run reached it. No side effects at all — it is a read of history.
|
|
159
|
+
router.get('/workflows/:id/steps/:stepKey/last-output', gate, async function(req, res) {
|
|
160
|
+
try {
|
|
161
|
+
var runs = await db.model('WorkflowRun').query().where({ workflowId: req.params.id }).select('id');
|
|
162
|
+
if (!runs.length) return res.json({ dataArray: [] });
|
|
163
|
+
var last = await db.model('WorkflowStepRun').query()
|
|
164
|
+
.whereIn('runId', runs.map(function(r) { return r.id; }))
|
|
165
|
+
.where({ stepKey: req.params.stepKey })
|
|
166
|
+
// 'waiting' counts: a wait step's action has already run and produced
|
|
167
|
+
// its output by then — that is exactly the shape the builder wants.
|
|
168
|
+
.whereIn('status', ['success', 'waiting'])
|
|
169
|
+
.orderBy('recordCreatedDate', 'desc')
|
|
170
|
+
.first();
|
|
171
|
+
res.json({ dataArray: last ? [{ output: last.output || {}, at: last.recordCreatedDate }] : [] });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
res.status(400).json({ message: err.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// The LIVE one: run the action for real, right now, with the values the
|
|
178
|
+
// builder currently has on screen (which may be unsaved — hence they come in
|
|
179
|
+
// the body rather than being read back off the step).
|
|
180
|
+
//
|
|
181
|
+
// THIS IS NOT A SIMULATION and there is nothing here that could make it one.
|
|
182
|
+
// runAction is the same call the engine makes, so email-send sends,
|
|
183
|
+
// email-delete expunges, db-push writes. The UI confirms before calling
|
|
184
|
+
// this; that confirmation is the only thing standing between a builder
|
|
185
|
+
// click and a real side effect, so it must not be removed on the grounds
|
|
186
|
+
// that it is "just a preview".
|
|
187
|
+
//
|
|
188
|
+
// References are interpolated against the OTHER steps' recorded sample
|
|
189
|
+
// outputs rather than a live context — there is no run here to have a
|
|
190
|
+
// context. So a value bound to {steps.x.output.id} resolves to whatever
|
|
191
|
+
// sample step x carries, which is the point: it is what makes a try-run
|
|
192
|
+
// produce a realistic result instead of posting the literal string
|
|
193
|
+
// "{steps.x.output.id}" to somebody's API.
|
|
194
|
+
router.post('/workflows/:id/steps/try', gate, async function(req, res) {
|
|
195
|
+
var body = req.body || {};
|
|
196
|
+
if (!body.actionName) return res.status(400).json({ message: 'actionName is required' });
|
|
197
|
+
try {
|
|
198
|
+
var steps = await db.model('WorkflowStep').query()
|
|
199
|
+
.where({ workflowId: req.params.id }).orderBy('position');
|
|
200
|
+
|
|
201
|
+
// Same `env` branch the real engine builds, for the same reason the
|
|
202
|
+
// sample outputs are here: a try-run that resolves a binding
|
|
203
|
+
// differently from the run is worse than no try-run at all.
|
|
204
|
+
var context = { params: body.params || {}, steps: {}, previous_step: {}, item: null, env: exposedEnv(), resumeKey: null };
|
|
205
|
+
steps.forEach(function(s) {
|
|
206
|
+
if (s.stepKey) context.steps[s.stepKey] = { output: s.sampleOutput || {} };
|
|
207
|
+
});
|
|
208
|
+
var selfIndex = steps.findIndex(function(s) { return s.stepKey === body.stepKey; });
|
|
209
|
+
var prev = selfIndex > 0 ? steps[selfIndex - 1] : null;
|
|
210
|
+
if (prev) context.previous_step = { output: prev.sampleOutput || {} };
|
|
211
|
+
|
|
212
|
+
var input = interpolateAll(body.values || {}, context);
|
|
213
|
+
var result = await runAction({ name: body.actionName, input: input });
|
|
214
|
+
res.json({
|
|
215
|
+
dataArray: [{
|
|
216
|
+
status: result.status,
|
|
217
|
+
output: result.output,
|
|
218
|
+
error: result.error,
|
|
219
|
+
durationMs: result.durationMs,
|
|
220
|
+
// Echoed back so the builder can show what it actually sent — a
|
|
221
|
+
// try-run that fails is usually a binding that resolved to
|
|
222
|
+
// something unexpected, and guessing at that is the slow way.
|
|
223
|
+
input: input
|
|
224
|
+
}]
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
res.status(400).json({ message: err.message });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Deliberately NOT gated — see workflowRunner.resumeByKey's own doc
|
|
232
|
+
// comment. Whatever mounts this router (this package's own standalone app,
|
|
233
|
+
// or a host's) must exempt "<mountPath>/public/*" from its OWN
|
|
234
|
+
// authentication middleware too, the same way orchestration/standalone.js
|
|
235
|
+
// does for itself — by the time a request reaches this router, any
|
|
236
|
+
// app-level auth gate has already run and this route can no longer opt out
|
|
237
|
+
// of it from in here.
|
|
238
|
+
router.post('/public/resume/:key', async function(req, res) {
|
|
239
|
+
try {
|
|
240
|
+
var body = req.body || {};
|
|
241
|
+
// `status: 'failed'` resolves the parked step as FAILED rather than
|
|
242
|
+
// succeeded — see resumeByKey. Absent means success, which is what every
|
|
243
|
+
// caller that predates this means: an approval click, a confirmation
|
|
244
|
+
// link. @xeplr/jobs sends it when a job ended any way other than
|
|
245
|
+
// completing, so a chain does not walk on past a movement that never
|
|
246
|
+
// happened.
|
|
247
|
+
var result = await workflowRunner.resumeByKey(req.params.key, body.output, {
|
|
248
|
+
status: body.status,
|
|
249
|
+
error: body.error
|
|
250
|
+
});
|
|
251
|
+
res.json({ dataArray: [result] });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
res.status(400).json({ message: err.message });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return router;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = buildWorkflowRouter;
|