@nanobpm/workflow 0.11.0 → 0.12.0
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 +32 -2
- package/dist/index.d.ts +1 -0
- package/dist/nodes/human.d.ts +57 -0
- package/dist/nodes/human.js +111 -0
- package/dist/nodes/index.d.ts +1 -0
- package/dist/nodes/index.js +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/nodes/human.ts +179 -0
- package/src/nodes/index.ts +1 -0
package/README.md
CHANGED
|
@@ -36,13 +36,24 @@ Both compile to the same engine durability; pick per workflow.
|
|
|
36
36
|
**The recommended surface.** Describe the flow as a **tree of nodes**: `w.run` (a
|
|
37
37
|
locally-hosted service task), `w.task` (an external-worker service task — job
|
|
38
38
|
type `${flowId}:${name}` by default, or pass `w.task(name, { jobType })` to
|
|
39
|
-
target an existing worker pool, e.g. a `senior:pr-review` agent token),
|
|
39
|
+
target an existing worker pool, e.g. a `senior:pr-review` agent token),
|
|
40
40
|
`w.signal` (a durable message catch that resumes via a correlated message — the
|
|
41
|
-
human-in-the-loop path),
|
|
41
|
+
human-in-the-loop path), and `w.human` (a BPMN **user task** — a human-in-the-loop
|
|
42
|
+
approval gate rendered on a task list), composed with control-flow combinators:
|
|
42
43
|
|
|
43
44
|
- `w.switch(subject, cases)` — a multi-way exclusive choice; each case key routes
|
|
44
45
|
when `subject = value`; an optional `default` case is the fallback.
|
|
45
46
|
- `w.branch(condition, { then, else? })` — a two-way choice on a FEEL boolean.
|
|
47
|
+
- `w.human(name, { form, assignee?, candidateGroups?, io? })` — a durable **user
|
|
48
|
+
task** (`<bpmn:userTask>` with the Zeebe user-task marker). `form` binds a
|
|
49
|
+
`zeebe:formDefinition` (the form id shown to the assignee). Add an assignment
|
|
50
|
+
with `assignee` (a static user id or a FEEL expression like
|
|
51
|
+
`=escalationAssignee`) and/or `candidateGroups` (who may claim it) — supply
|
|
52
|
+
either or both. `io` adds a `zeebe:ioMapping` of `{ input?, output? }` entries
|
|
53
|
+
(`{ source, target }`: a FEEL `source` copied to/from the process variable
|
|
54
|
+
`target`), e.g. to stamp the reviewer's decision back onto a variable on
|
|
55
|
+
completion. The token waits durably until the task is completed from a task
|
|
56
|
+
list / the Zeebe user-task API.
|
|
46
57
|
- `w.loop(body)` — a durable loop (back-edge to the loop head).
|
|
47
58
|
- `w.break()` / `w.continue()` — exit the enclosing loop, or jump back to its head.
|
|
48
59
|
- `w.parallel([blockA, blockB, …])` — a static fork/join: every block runs
|
|
@@ -88,6 +99,25 @@ const { processInstanceKey } = await client.start(onboarding, {});
|
|
|
88
99
|
await client.signal(onboarding, "approved", userId, { by: "alice" });
|
|
89
100
|
```
|
|
90
101
|
|
|
102
|
+
A human-in-the-loop **approval gate** with a form, an assignment, and an output
|
|
103
|
+
mapping that stamps the decision back onto a process variable:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const review = defineFlow("plan-review", (w) => {
|
|
107
|
+
w.human("plan-review-decision", {
|
|
108
|
+
form: "plan-review-decision", // zeebe:formDefinition formId
|
|
109
|
+
assignee: "=escalationAssignee", // a FEEL expression (or a static user id)
|
|
110
|
+
candidateGroups: "operators", // who may claim it
|
|
111
|
+
io: { output: [{ source: "=verdict", target: "planVerdict" }] },
|
|
112
|
+
});
|
|
113
|
+
w.switch("planVerdict", {
|
|
114
|
+
approve: (c) => c.run("apply", applyPlan),
|
|
115
|
+
default: (c) => c.run("revise", revisePlan),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
91
121
|
A durable convergence loop (the shape `urban-pr-review` uses) — a loop wrapping a
|
|
92
122
|
status switch with a nested guard:
|
|
93
123
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { Journal, ReplayStep } from "./imperative.js";
|
|
|
3
3
|
export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
|
|
4
4
|
export type { FlowBuilder } from "./declarative.js";
|
|
5
5
|
export type { BoundaryOptions } from "./nodes/boundary.js";
|
|
6
|
+
export type { HumanOptions, HumanIoMapping, HumanIoEntry } from "./nodes/human.js";
|
|
6
7
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
7
8
|
export { envelope } from "./envelope.js";
|
|
8
9
|
export type { Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, ScalarTs, FieldTs, } from "./envelope.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** A single Zeebe I/O mapping entry — a FEEL `source` copied to/from a process
|
|
2
|
+
* variable `target`. */
|
|
3
|
+
export interface HumanIoEntry {
|
|
4
|
+
/** FEEL expression evaluated for the mapping (e.g. `=orderId`). */
|
|
5
|
+
source: string;
|
|
6
|
+
/** The process variable the value is written to. */
|
|
7
|
+
target: string;
|
|
8
|
+
}
|
|
9
|
+
/** The `<zeebe:ioMapping>` for a user task: input mappings applied on activation
|
|
10
|
+
* and output mappings applied on completion. */
|
|
11
|
+
export interface HumanIoMapping {
|
|
12
|
+
input?: HumanIoEntry[];
|
|
13
|
+
output?: HumanIoEntry[];
|
|
14
|
+
}
|
|
15
|
+
/** Options for {@link FlowBuilder.human}. `form` is the `zeebe:formDefinition`
|
|
16
|
+
* form id; `assignee` / `candidateGroups` populate a `zeebe:assignmentDefinition`
|
|
17
|
+
* (supply either or both); `io` populates a `zeebe:ioMapping`. */
|
|
18
|
+
export interface HumanOptions {
|
|
19
|
+
/** The `zeebe:formDefinition` form id bound to this task. */
|
|
20
|
+
form: string;
|
|
21
|
+
/** A single assignee (a static user id or a FEEL expression like
|
|
22
|
+
* `=escalationAssignee`). */
|
|
23
|
+
assignee?: string;
|
|
24
|
+
/** The candidate groups allowed to claim the task (a static group id or a FEEL
|
|
25
|
+
* expression). */
|
|
26
|
+
candidateGroups?: string;
|
|
27
|
+
/** Input/output variable mappings. */
|
|
28
|
+
io?: HumanIoMapping;
|
|
29
|
+
}
|
|
30
|
+
/** The `human` node shape (its `FlowNode` variant). */
|
|
31
|
+
interface HumanNode {
|
|
32
|
+
kind: "human";
|
|
33
|
+
name: string;
|
|
34
|
+
form: string;
|
|
35
|
+
assignee?: string;
|
|
36
|
+
candidateGroups?: string;
|
|
37
|
+
io?: HumanIoMapping;
|
|
38
|
+
}
|
|
39
|
+
declare module "../types.js" {
|
|
40
|
+
interface FlowNodeRegistry {
|
|
41
|
+
human: HumanNode;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
declare module "../declarative.js" {
|
|
45
|
+
interface FlowBuilder<C extends object> {
|
|
46
|
+
/**
|
|
47
|
+
* A human-in-the-loop approval step (a BPMN `<bpmn:userTask>` with the Zeebe
|
|
48
|
+
* user-task marker). `opts.form` binds a `zeebe:formDefinition`; the optional
|
|
49
|
+
* `assignee` / `candidateGroups` populate a `zeebe:assignmentDefinition`
|
|
50
|
+
* (either or both), and `io` populates a `zeebe:ioMapping`. Resume it from a
|
|
51
|
+
* task list / the Zeebe user-task API; the token waits durably until the task
|
|
52
|
+
* is completed.
|
|
53
|
+
*/
|
|
54
|
+
human<K extends string>(name: K, opts: HumanOptions): FlowBuilder<C>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Built-in flow-node kind: `human` — a human-in-the-loop approval step (a BPMN
|
|
2
|
+
// `<bpmn:userTask>` with the Zeebe user-task marker). It carries a form binding
|
|
3
|
+
// and, optionally, an assignment (assignee / candidate groups) and an I/O
|
|
4
|
+
// mapping — the surface every nwf approval gate (answer escalation, plan review,
|
|
5
|
+
// trial-merge decision) is built from. Contributed from this module through the
|
|
6
|
+
// extension seam (epic #314, S3/#318) — no central union or dispatch is edited.
|
|
7
|
+
import { escapeXml } from "../xml.js";
|
|
8
|
+
import { incomingOutgoing } from "../declarative.js";
|
|
9
|
+
import { registerNodeKind } from "./registry.js";
|
|
10
|
+
/** A present, non-empty, non-whitespace-only string — the shape every human()
|
|
11
|
+
* id / assignment / mapping-expression field must have. A blank value would
|
|
12
|
+
* emit a meaningless BPMN attribute (an empty formId, assignee, candidateGroups,
|
|
13
|
+
* or ioMapping source/target) that fails or misbehaves at deploy/run time, so we
|
|
14
|
+
* fail fast at build. Non-blank values are kept verbatim — FEEL expressions are
|
|
15
|
+
* never trimmed or normalized. */
|
|
16
|
+
function isNonBlankString(v) {
|
|
17
|
+
return typeof v === "string" && v.trim() !== "";
|
|
18
|
+
}
|
|
19
|
+
function isEntry(e) {
|
|
20
|
+
if (e === null || typeof e !== "object")
|
|
21
|
+
return false;
|
|
22
|
+
if (!("source" in e) || !("target" in e))
|
|
23
|
+
return false;
|
|
24
|
+
const { source, target } = e;
|
|
25
|
+
return isNonBlankString(source) && isNonBlankString(target);
|
|
26
|
+
}
|
|
27
|
+
function validateEntries(name, dir, entries) {
|
|
28
|
+
if (entries === undefined)
|
|
29
|
+
return;
|
|
30
|
+
if (!Array.isArray(entries))
|
|
31
|
+
throw new Error(`human("${name}") io.${dir} must be an array of { source, target }`);
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
if (!isEntry(e))
|
|
34
|
+
throw new Error(`human("${name}") io.${dir} entries must be { source: non-empty string, target: non-empty string }`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function renderAssignment(node) {
|
|
38
|
+
if (node.assignee === undefined && node.candidateGroups === undefined)
|
|
39
|
+
return "";
|
|
40
|
+
const attrs = [
|
|
41
|
+
node.assignee !== undefined ? ` assignee="${escapeXml(node.assignee)}"` : "",
|
|
42
|
+
node.candidateGroups !== undefined ? ` candidateGroups="${escapeXml(node.candidateGroups)}"` : "",
|
|
43
|
+
].join("");
|
|
44
|
+
return ` <zeebe:assignmentDefinition${attrs} />\n`;
|
|
45
|
+
}
|
|
46
|
+
function renderMappings(entries, tag) {
|
|
47
|
+
if (!entries || entries.length === 0)
|
|
48
|
+
return "";
|
|
49
|
+
return entries
|
|
50
|
+
.map((e) => ` <zeebe:${tag} source="${escapeXml(e.source)}" target="${escapeXml(e.target)}" />\n`)
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
function renderIoMapping(node) {
|
|
54
|
+
const io = node.io;
|
|
55
|
+
if (!io || ((io.input?.length ?? 0) === 0 && (io.output?.length ?? 0) === 0))
|
|
56
|
+
return "";
|
|
57
|
+
return (` <zeebe:ioMapping>\n` +
|
|
58
|
+
renderMappings(io.input, "input") +
|
|
59
|
+
renderMappings(io.output, "output") +
|
|
60
|
+
` </zeebe:ioMapping>\n`);
|
|
61
|
+
}
|
|
62
|
+
function renderUserTask(node, inc, outg) {
|
|
63
|
+
const id = node.name;
|
|
64
|
+
const ext = ` <bpmn:extensionElements>\n` +
|
|
65
|
+
` <zeebe:formDefinition formId="${escapeXml(node.form)}" />\n` +
|
|
66
|
+
` <zeebe:userTask />\n` +
|
|
67
|
+
renderAssignment(node) +
|
|
68
|
+
renderIoMapping(node) +
|
|
69
|
+
` </bpmn:extensionElements>`;
|
|
70
|
+
return (` <bpmn:userTask id="${escapeXml(id)}" name="${escapeXml(id)}">\n` +
|
|
71
|
+
ext +
|
|
72
|
+
"\n" +
|
|
73
|
+
incomingOutgoing(inc, outg) +
|
|
74
|
+
` </bpmn:userTask>`);
|
|
75
|
+
}
|
|
76
|
+
registerNodeKind("human", {
|
|
77
|
+
build: (api) => (name, opts) => {
|
|
78
|
+
api.claim(name);
|
|
79
|
+
if (opts === null || typeof opts !== "object") {
|
|
80
|
+
throw new Error(`human("${name}") needs an options object { form, … }`);
|
|
81
|
+
}
|
|
82
|
+
if (!isNonBlankString(opts.form)) {
|
|
83
|
+
throw new Error(`human("${name}") needs a non-empty { form } (the zeebe:formDefinition form id)`);
|
|
84
|
+
}
|
|
85
|
+
if (opts.assignee !== undefined && !isNonBlankString(opts.assignee)) {
|
|
86
|
+
throw new Error(`human("${name}") { assignee } must be a non-empty string`);
|
|
87
|
+
}
|
|
88
|
+
if (opts.candidateGroups !== undefined && !isNonBlankString(opts.candidateGroups)) {
|
|
89
|
+
throw new Error(`human("${name}") { candidateGroups } must be a non-empty string`);
|
|
90
|
+
}
|
|
91
|
+
if (opts.io !== undefined && (opts.io === null || typeof opts.io !== "object" || Array.isArray(opts.io))) {
|
|
92
|
+
throw new Error(`human("${name}") { io } must be an object { input?, output? }`);
|
|
93
|
+
}
|
|
94
|
+
validateEntries(name, "input", opts.io?.input);
|
|
95
|
+
validateEntries(name, "output", opts.io?.output);
|
|
96
|
+
api.out.push({
|
|
97
|
+
kind: "human",
|
|
98
|
+
name,
|
|
99
|
+
form: opts.form,
|
|
100
|
+
assignee: opts.assignee,
|
|
101
|
+
candidateGroups: opts.candidateGroups,
|
|
102
|
+
io: opts.io,
|
|
103
|
+
});
|
|
104
|
+
return api.self();
|
|
105
|
+
},
|
|
106
|
+
emit: (node, incoming, _loop, api) => {
|
|
107
|
+
api.addNode({ id: node.name, render: (inc, outg) => renderUserTask(node, inc, outg) });
|
|
108
|
+
api.connect(incoming, node.name);
|
|
109
|
+
return [api.newEdge(node.name)];
|
|
110
|
+
},
|
|
111
|
+
});
|
package/dist/nodes/index.d.ts
CHANGED
package/dist/nodes/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Code-first durable orchestration for nanobpmn (ADR 0044/0045). Author a durable workflow as a declarative flow — `defineFlow(id, w => { w.run(...); w.signal(...); w.task(...) })` — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. `w.run` is a local step, `w.signal` is a durable human-in-the-loop wait, `w.task` is a step serviced by an external worker. `defineFlow` is the single code-first surface; the imperative `defineWorkflow` replay function is experimental/internal, not a recommended API. No diagram, no task-type wiring, no correlation plumbing authored by hand — and you can optionally auto-generate a diagram (DI) for the derived model with `declarativeToLayoutedBpmn` via the optional `bpmn-auto-layout` peer dependency. Talks to a running nanobpmn gateway through @nanobpm/nano-sdk (the shared engine-transport spine — ADR 0055), which negotiates the Falcon protocol on a Nano server and REST elsewhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type { FlowBuilder } from "./declarative.js";
|
|
|
46
46
|
// The `.boundary(...)` option type (its builder method augments FlowBuilder from
|
|
47
47
|
// the boundary kind module — S2/#317).
|
|
48
48
|
export type { BoundaryOptions } from "./nodes/boundary.js";
|
|
49
|
+
export type { HumanOptions, HumanIoMapping, HumanIoEntry } from "./nodes/human.js";
|
|
49
50
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
50
51
|
export { envelope } from "./envelope.js";
|
|
51
52
|
export type {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Built-in flow-node kind: `human` — a human-in-the-loop approval step (a BPMN
|
|
2
|
+
// `<bpmn:userTask>` with the Zeebe user-task marker). It carries a form binding
|
|
3
|
+
// and, optionally, an assignment (assignee / candidate groups) and an I/O
|
|
4
|
+
// mapping — the surface every nwf approval gate (answer escalation, plan review,
|
|
5
|
+
// trial-merge decision) is built from. Contributed from this module through the
|
|
6
|
+
// extension seam (epic #314, S3/#318) — no central union or dispatch is edited.
|
|
7
|
+
|
|
8
|
+
import { escapeXml } from "../xml.js";
|
|
9
|
+
import { incomingOutgoing } from "../declarative.js";
|
|
10
|
+
import { registerNodeKind } from "./registry.js";
|
|
11
|
+
|
|
12
|
+
/** A single Zeebe I/O mapping entry — a FEEL `source` copied to/from a process
|
|
13
|
+
* variable `target`. */
|
|
14
|
+
export interface HumanIoEntry {
|
|
15
|
+
/** FEEL expression evaluated for the mapping (e.g. `=orderId`). */
|
|
16
|
+
source: string;
|
|
17
|
+
/** The process variable the value is written to. */
|
|
18
|
+
target: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The `<zeebe:ioMapping>` for a user task: input mappings applied on activation
|
|
22
|
+
* and output mappings applied on completion. */
|
|
23
|
+
export interface HumanIoMapping {
|
|
24
|
+
input?: HumanIoEntry[];
|
|
25
|
+
output?: HumanIoEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for {@link FlowBuilder.human}. `form` is the `zeebe:formDefinition`
|
|
29
|
+
* form id; `assignee` / `candidateGroups` populate a `zeebe:assignmentDefinition`
|
|
30
|
+
* (supply either or both); `io` populates a `zeebe:ioMapping`. */
|
|
31
|
+
export interface HumanOptions {
|
|
32
|
+
/** The `zeebe:formDefinition` form id bound to this task. */
|
|
33
|
+
form: string;
|
|
34
|
+
/** A single assignee (a static user id or a FEEL expression like
|
|
35
|
+
* `=escalationAssignee`). */
|
|
36
|
+
assignee?: string;
|
|
37
|
+
/** The candidate groups allowed to claim the task (a static group id or a FEEL
|
|
38
|
+
* expression). */
|
|
39
|
+
candidateGroups?: string;
|
|
40
|
+
/** Input/output variable mappings. */
|
|
41
|
+
io?: HumanIoMapping;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The `human` node shape (its `FlowNode` variant). */
|
|
45
|
+
interface HumanNode {
|
|
46
|
+
kind: "human";
|
|
47
|
+
name: string;
|
|
48
|
+
form: string;
|
|
49
|
+
assignee?: string;
|
|
50
|
+
candidateGroups?: string;
|
|
51
|
+
io?: HumanIoMapping;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare module "../types.js" {
|
|
55
|
+
interface FlowNodeRegistry {
|
|
56
|
+
human: HumanNode;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare module "../declarative.js" {
|
|
61
|
+
interface FlowBuilder<C extends object> {
|
|
62
|
+
/**
|
|
63
|
+
* A human-in-the-loop approval step (a BPMN `<bpmn:userTask>` with the Zeebe
|
|
64
|
+
* user-task marker). `opts.form` binds a `zeebe:formDefinition`; the optional
|
|
65
|
+
* `assignee` / `candidateGroups` populate a `zeebe:assignmentDefinition`
|
|
66
|
+
* (either or both), and `io` populates a `zeebe:ioMapping`. Resume it from a
|
|
67
|
+
* task list / the Zeebe user-task API; the token waits durably until the task
|
|
68
|
+
* is completed.
|
|
69
|
+
*/
|
|
70
|
+
human<K extends string>(name: K, opts: HumanOptions): FlowBuilder<C>;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A present, non-empty, non-whitespace-only string — the shape every human()
|
|
75
|
+
* id / assignment / mapping-expression field must have. A blank value would
|
|
76
|
+
* emit a meaningless BPMN attribute (an empty formId, assignee, candidateGroups,
|
|
77
|
+
* or ioMapping source/target) that fails or misbehaves at deploy/run time, so we
|
|
78
|
+
* fail fast at build. Non-blank values are kept verbatim — FEEL expressions are
|
|
79
|
+
* never trimmed or normalized. */
|
|
80
|
+
function isNonBlankString(v: unknown): v is string {
|
|
81
|
+
return typeof v === "string" && v.trim() !== "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isEntry(e: unknown): e is HumanIoEntry {
|
|
85
|
+
if (e === null || typeof e !== "object") return false;
|
|
86
|
+
if (!("source" in e) || !("target" in e)) return false;
|
|
87
|
+
const { source, target } = e;
|
|
88
|
+
return isNonBlankString(source) && isNonBlankString(target);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateEntries(name: string, dir: "input" | "output", entries: HumanIoEntry[] | undefined): void {
|
|
92
|
+
if (entries === undefined) return;
|
|
93
|
+
if (!Array.isArray(entries)) throw new Error(`human("${name}") io.${dir} must be an array of { source, target }`);
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
if (!isEntry(e)) throw new Error(`human("${name}") io.${dir} entries must be { source: non-empty string, target: non-empty string }`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderAssignment(node: HumanNode): string {
|
|
100
|
+
if (node.assignee === undefined && node.candidateGroups === undefined) return "";
|
|
101
|
+
const attrs = [
|
|
102
|
+
node.assignee !== undefined ? ` assignee="${escapeXml(node.assignee)}"` : "",
|
|
103
|
+
node.candidateGroups !== undefined ? ` candidateGroups="${escapeXml(node.candidateGroups)}"` : "",
|
|
104
|
+
].join("");
|
|
105
|
+
return ` <zeebe:assignmentDefinition${attrs} />\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderMappings(entries: HumanIoEntry[] | undefined, tag: "input" | "output"): string {
|
|
109
|
+
if (!entries || entries.length === 0) return "";
|
|
110
|
+
return entries
|
|
111
|
+
.map((e) => ` <zeebe:${tag} source="${escapeXml(e.source)}" target="${escapeXml(e.target)}" />\n`)
|
|
112
|
+
.join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderIoMapping(node: HumanNode): string {
|
|
116
|
+
const io = node.io;
|
|
117
|
+
if (!io || ((io.input?.length ?? 0) === 0 && (io.output?.length ?? 0) === 0)) return "";
|
|
118
|
+
return (
|
|
119
|
+
` <zeebe:ioMapping>\n` +
|
|
120
|
+
renderMappings(io.input, "input") +
|
|
121
|
+
renderMappings(io.output, "output") +
|
|
122
|
+
` </zeebe:ioMapping>\n`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderUserTask(node: HumanNode, inc: string[], outg: string[]): string {
|
|
127
|
+
const id = node.name;
|
|
128
|
+
const ext =
|
|
129
|
+
` <bpmn:extensionElements>\n` +
|
|
130
|
+
` <zeebe:formDefinition formId="${escapeXml(node.form)}" />\n` +
|
|
131
|
+
` <zeebe:userTask />\n` +
|
|
132
|
+
renderAssignment(node) +
|
|
133
|
+
renderIoMapping(node) +
|
|
134
|
+
` </bpmn:extensionElements>`;
|
|
135
|
+
return (
|
|
136
|
+
` <bpmn:userTask id="${escapeXml(id)}" name="${escapeXml(id)}">\n` +
|
|
137
|
+
ext +
|
|
138
|
+
"\n" +
|
|
139
|
+
incomingOutgoing(inc, outg) +
|
|
140
|
+
` </bpmn:userTask>`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
registerNodeKind("human", {
|
|
145
|
+
build: (api) => (name: string, opts: HumanOptions) => {
|
|
146
|
+
api.claim(name);
|
|
147
|
+
if (opts === null || typeof opts !== "object") {
|
|
148
|
+
throw new Error(`human("${name}") needs an options object { form, … }`);
|
|
149
|
+
}
|
|
150
|
+
if (!isNonBlankString(opts.form)) {
|
|
151
|
+
throw new Error(`human("${name}") needs a non-empty { form } (the zeebe:formDefinition form id)`);
|
|
152
|
+
}
|
|
153
|
+
if (opts.assignee !== undefined && !isNonBlankString(opts.assignee)) {
|
|
154
|
+
throw new Error(`human("${name}") { assignee } must be a non-empty string`);
|
|
155
|
+
}
|
|
156
|
+
if (opts.candidateGroups !== undefined && !isNonBlankString(opts.candidateGroups)) {
|
|
157
|
+
throw new Error(`human("${name}") { candidateGroups } must be a non-empty string`);
|
|
158
|
+
}
|
|
159
|
+
if (opts.io !== undefined && (opts.io === null || typeof opts.io !== "object" || Array.isArray(opts.io))) {
|
|
160
|
+
throw new Error(`human("${name}") { io } must be an object { input?, output? }`);
|
|
161
|
+
}
|
|
162
|
+
validateEntries(name, "input", opts.io?.input);
|
|
163
|
+
validateEntries(name, "output", opts.io?.output);
|
|
164
|
+
api.out.push({
|
|
165
|
+
kind: "human",
|
|
166
|
+
name,
|
|
167
|
+
form: opts.form,
|
|
168
|
+
assignee: opts.assignee,
|
|
169
|
+
candidateGroups: opts.candidateGroups,
|
|
170
|
+
io: opts.io,
|
|
171
|
+
});
|
|
172
|
+
return api.self();
|
|
173
|
+
},
|
|
174
|
+
emit: (node, incoming, _loop, api) => {
|
|
175
|
+
api.addNode({ id: node.name, render: (inc, outg) => renderUserTask(node, inc, outg) });
|
|
176
|
+
api.connect(incoming, node.name);
|
|
177
|
+
return [api.newEdge(node.name)];
|
|
178
|
+
},
|
|
179
|
+
});
|