@sanity/workflow-engine 0.3.0 → 0.4.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/dist/_chunks-cjs/schema.cjs +291 -317
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +291 -317
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +480 -74
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +7404 -4450
- package/dist/define.d.ts +7404 -4450
- package/dist/define.js +482 -76
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +1650 -1521
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10557 -5214
- package/dist/index.d.ts +10557 -5214
- package/dist/index.js +1651 -1522
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,59 +1,15 @@
|
|
|
1
1
|
import { parse, evaluate } from "groq-js";
|
|
2
|
-
import { WORKFLOW_DEFINITION_TYPE } from "./_chunks-es/schema.js";
|
|
2
|
+
import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
for (const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
validateStage(v2, stage);
|
|
12
|
-
if (v2.errors.length > 0)
|
|
13
|
-
throw new Error(
|
|
14
|
-
`defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
|
|
15
|
-
` + v2.errors.join(`
|
|
16
|
-
`)
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
function createDefinitionValidator() {
|
|
20
|
-
const errors = [], tryParse = (groq, where) => {
|
|
21
|
-
try {
|
|
22
|
-
parse(groq);
|
|
23
|
-
} catch (err) {
|
|
24
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
-
errors.push(` \xB7 ${where}: ${message}`);
|
|
26
|
-
}
|
|
27
|
-
}, rejectTypeScan = (groq, where) => {
|
|
28
|
-
/\*\s*\[\s*_type\b/.test(groq) && errors.push(
|
|
29
|
-
` \xB7 ${where}: filter scans by \`_type\` \u2014 that's a discovery query, not a predicate. Filters evaluate against the in-memory snapshot (instance + ancestors + state-declared docs). To bring extra docs in scope, declare a \`workflow.state.doc.ref\` (or \`doc.refs\`) slot on the workflow or this stage. For lake scans like "all articles in this release", use spawn \`forEach.groq\` instead.`
|
|
30
|
-
);
|
|
31
|
-
};
|
|
32
|
-
return { errors, tryParse, checkFilter: (groq, where) => {
|
|
33
|
-
tryParse(groq, where), rejectTypeScan(groq, where);
|
|
34
|
-
}, checkSlot: (slot, slotLabel) => {
|
|
35
|
-
slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
|
|
36
|
-
} };
|
|
4
|
+
function getPath(value, path) {
|
|
5
|
+
let current = value;
|
|
6
|
+
for (const part of path.split(".")) {
|
|
7
|
+
if (current == null || typeof current != "object") return;
|
|
8
|
+
current = current[part];
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
37
11
|
}
|
|
38
|
-
|
|
39
|
-
for (const slot of stage.state ?? [])
|
|
40
|
-
v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
|
|
41
|
-
typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
|
|
42
|
-
for (const t of stage.transitions ?? [])
|
|
43
|
-
typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
|
|
44
|
-
for (const task of stage.tasks ?? [])
|
|
45
|
-
validateTask(v2, stage.id, task);
|
|
46
|
-
}
|
|
47
|
-
function validateTask(v2, stageId, task) {
|
|
48
|
-
const where = `stage "${stageId}" task "${task.id}"`;
|
|
49
|
-
for (const slot of task.state ?? [])
|
|
50
|
-
v2.checkSlot(slot, `${where}.state "${slot.id}"`);
|
|
51
|
-
typeof task.filter == "string" && v2.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v2.checkFilter(task.completeWhen, `${where}.completeWhen`);
|
|
52
|
-
for (const a of task.actions ?? [])
|
|
53
|
-
typeof a.filter == "string" && v2.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
|
|
54
|
-
task.spawns?.forEach?.groq && v2.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
|
|
55
|
-
}
|
|
56
|
-
const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
12
|
+
const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
57
13
|
function parseGdr(uri) {
|
|
58
14
|
const colon = uri.indexOf(":");
|
|
59
15
|
if (colon < 0)
|
|
@@ -110,12 +66,15 @@ function refMediaLibrary(resourceId, documentId, type) {
|
|
|
110
66
|
function refDashboard(resourceId, documentId, type) {
|
|
111
67
|
return { id: gdrUri({ scheme: "dashboard", resourceId, documentId }), type };
|
|
112
68
|
}
|
|
69
|
+
function datasetResourceParts(id) {
|
|
70
|
+
const dot = id.indexOf(".");
|
|
71
|
+
if (dot <= 0 || dot === id.length - 1)
|
|
72
|
+
throw new Error(`Invalid dataset resource id "${id}": expected "<projectId>.<dataset>".`);
|
|
73
|
+
return { projectId: id.slice(0, dot), dataset: id.slice(dot + 1) };
|
|
74
|
+
}
|
|
113
75
|
function gdrFromResource(res, documentId) {
|
|
114
76
|
if (res.type === "dataset") {
|
|
115
|
-
const
|
|
116
|
-
if (dot < 0)
|
|
117
|
-
throw new Error(`Invalid dataset resource id "${res.id}": expected "<projectId>.<dataset>".`);
|
|
118
|
-
const projectId = res.id.slice(0, dot), dataset = res.id.slice(dot + 1);
|
|
77
|
+
const { projectId, dataset } = datasetResourceParts(res.id);
|
|
119
78
|
return gdrUri({ scheme: "dataset", projectId, dataset, documentId });
|
|
120
79
|
}
|
|
121
80
|
return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
|
|
@@ -137,191 +96,157 @@ function gdrRef(res, documentId, type) {
|
|
|
137
96
|
function isGdr(value) {
|
|
138
97
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
139
98
|
}
|
|
140
|
-
function
|
|
141
|
-
return {
|
|
142
|
-
self: selfGdr(instance),
|
|
143
|
-
state: stateMap(instance.state ?? []),
|
|
144
|
-
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
145
|
-
ancestors: instance.ancestors.map((a) => a.id),
|
|
146
|
-
/** Current stage id — bind for filters that scope to the current stage. */
|
|
147
|
-
stage: instance.currentStageId,
|
|
148
|
-
now,
|
|
149
|
-
...extra
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function stateMap(slots) {
|
|
153
|
-
const out = {};
|
|
154
|
-
for (const s of slots) out[s.id] = s;
|
|
155
|
-
return out;
|
|
156
|
-
}
|
|
157
|
-
function paramsForLake(params) {
|
|
158
|
-
return {
|
|
159
|
-
...params,
|
|
160
|
-
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
161
|
-
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
162
|
-
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
163
|
-
state: stripStateForLake(params.state)
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function stripStateForLake(value) {
|
|
167
|
-
if (value == null) return value;
|
|
168
|
-
if (typeof value == "string") return bareId(value);
|
|
169
|
-
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
170
|
-
if (typeof value == "object") {
|
|
171
|
-
const out = {};
|
|
172
|
-
for (const [k, v2] of Object.entries(value))
|
|
173
|
-
out[k] = stripStateForLake(v2);
|
|
174
|
-
return out;
|
|
175
|
-
}
|
|
176
|
-
return value;
|
|
99
|
+
function resourceOf(p) {
|
|
100
|
+
return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
|
|
177
101
|
}
|
|
178
|
-
|
|
179
|
-
|
|
102
|
+
const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
|
|
103
|
+
function isGuardReadExpr(expr) {
|
|
104
|
+
return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
|
|
180
105
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
106
|
+
function resolveGuardRead(expr, ctx) {
|
|
107
|
+
if (expr === "$self") return selfGdr(ctx.instance);
|
|
108
|
+
if (expr === "$now") return ctx.now;
|
|
109
|
+
const stateRead = STATE_READ.exec(expr);
|
|
110
|
+
if (stateRead !== null) {
|
|
111
|
+
const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
|
|
112
|
+
return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
|
|
186
113
|
}
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
function readStateSlot(instance, scope, slotId, stageId, taskId) {
|
|
190
|
-
if (scope === "workflow")
|
|
191
|
-
return slotValue$1(instance.state?.find((s) => s.id === slotId));
|
|
192
|
-
const stageEntry = instance.stages.find(
|
|
193
|
-
(s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
|
|
194
116
|
);
|
|
195
|
-
if (stageEntry === void 0) return;
|
|
196
|
-
if (scope === "stage")
|
|
197
|
-
return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
|
|
198
|
-
const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
|
|
199
|
-
return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
|
|
200
117
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
118
|
+
function resolveIdRefTarget(expr, ctx) {
|
|
119
|
+
const value = resolveGuardRead(expr, ctx);
|
|
120
|
+
if (typeof value == "string" && isGdrUri(value))
|
|
121
|
+
return { parsed: parseGdr(value) };
|
|
122
|
+
if (value && typeof value == "object") {
|
|
123
|
+
const v2 = value;
|
|
124
|
+
if (typeof v2.id == "string" && isGdrUri(v2.id))
|
|
125
|
+
return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
204
128
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
case "actor":
|
|
212
|
-
return { handled: !0, value: ctx.actor };
|
|
213
|
-
case "now":
|
|
214
|
-
return { handled: !0, value: ctx.now };
|
|
215
|
-
default:
|
|
216
|
-
return { handled: !1 };
|
|
129
|
+
function resolveIdRefTargets(idRefs, ctx) {
|
|
130
|
+
const targets = [];
|
|
131
|
+
for (const expr of idRefs ?? []) {
|
|
132
|
+
const target = resolveIdRefTarget(expr, ctx);
|
|
133
|
+
if (target === null) return null;
|
|
134
|
+
targets.push(target);
|
|
217
135
|
}
|
|
136
|
+
return targets.length === 0 ? null : targets;
|
|
218
137
|
}
|
|
219
|
-
function
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
case "stateRead":
|
|
228
|
-
return resolveStateRead(src, ctx);
|
|
229
|
-
case "effectOutput":
|
|
230
|
-
return resolveEffectOutput(src, ctx);
|
|
231
|
-
case "object":
|
|
232
|
-
return resolveObject(src, ctx);
|
|
233
|
-
case "row":
|
|
234
|
-
case "parentState":
|
|
235
|
-
return;
|
|
236
|
-
default:
|
|
237
|
-
return;
|
|
138
|
+
function assertSingleResource(targets) {
|
|
139
|
+
const resource = resourceOf(targets[0].parsed);
|
|
140
|
+
for (const g of targets) {
|
|
141
|
+
const r = resourceOf(g.parsed);
|
|
142
|
+
if (r.type !== resource.type || r.id !== resource.id)
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
|
|
145
|
+
);
|
|
238
146
|
}
|
|
147
|
+
return resource;
|
|
239
148
|
}
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
return src.path !== void 0 ? getPath(value, src.path) : value;
|
|
149
|
+
function bareIdRefs(targets) {
|
|
150
|
+
return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
|
|
243
151
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return value === void 0 ? null : value;
|
|
152
|
+
function resolveMatchTypes(targets, authorTypes) {
|
|
153
|
+
if (authorTypes !== void 0) return authorTypes;
|
|
154
|
+
const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
|
|
155
|
+
return inferred.length > 0 ? inferred : void 0;
|
|
249
156
|
}
|
|
250
|
-
function
|
|
157
|
+
function resolveMetadata(metadata, ctx) {
|
|
251
158
|
const out = {};
|
|
252
|
-
for (const [
|
|
253
|
-
out[
|
|
159
|
+
for (const [k, expr] of Object.entries(metadata ?? {}))
|
|
160
|
+
out[k] = resolveGuardRead(expr, ctx);
|
|
254
161
|
return out;
|
|
255
162
|
}
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for (const [key, src] of Object.entries(bindings))
|
|
266
|
-
resolved[key] = resolveSource(src, ctx);
|
|
267
|
-
}
|
|
268
|
-
return { ...resolved, ...staticInput };
|
|
269
|
-
}
|
|
270
|
-
async function evaluateFilter(args) {
|
|
271
|
-
const { filter, definition, snapshot, params } = args;
|
|
272
|
-
if (filter === void 0) return !0;
|
|
273
|
-
if (typeof filter == "string")
|
|
274
|
-
return runGroqAgainstSnapshot(filter, params, snapshot);
|
|
275
|
-
const predicate = (definition.predicates ?? []).find((p) => p.id === filter.ref);
|
|
276
|
-
if (predicate === void 0)
|
|
277
|
-
throw new Error(`Unknown predicate ref: ${filter.ref}`);
|
|
278
|
-
validatePredicateArgs(predicate, filter.args ?? {});
|
|
279
|
-
const merged = { ...params, ...filter.args };
|
|
280
|
-
return runGroqAgainstSnapshot(predicate.groq, merged, snapshot);
|
|
281
|
-
}
|
|
282
|
-
async function runGroqAgainstSnapshot(groq, params, snapshot) {
|
|
283
|
-
if (!groq.includes("*")) {
|
|
284
|
-
const tree2 = parse(groq, { params });
|
|
285
|
-
return !!await (await evaluate(tree2, { params })).get();
|
|
286
|
-
}
|
|
287
|
-
const tree = parse(groq, { params });
|
|
288
|
-
return !!await (await evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
289
|
-
}
|
|
290
|
-
function validatePredicateArgs(predicate, args) {
|
|
291
|
-
for (const param of predicate.params ?? [])
|
|
292
|
-
validatePredicateParam(predicate.id, param, args);
|
|
293
|
-
}
|
|
294
|
-
function validatePredicateParam(predicateId, param, args) {
|
|
295
|
-
if (!(param.id in args))
|
|
296
|
-
throw new Error(`Predicate "${predicateId}" requires param "${param.id}"`);
|
|
297
|
-
const value = args[param.id];
|
|
298
|
-
if (!matchesParamType(param.type, value))
|
|
163
|
+
function validateDefinition(definition) {
|
|
164
|
+
const v2 = createDefinitionValidator();
|
|
165
|
+
for (const entry of definition.state ?? [])
|
|
166
|
+
v2.checkEntry(entry, `workflow.state "${entry.name}"`);
|
|
167
|
+
for (const [name, groq] of Object.entries(definition.predicates ?? {}))
|
|
168
|
+
v2.checkCondition(groq, `predicate "${name}"`);
|
|
169
|
+
for (const stage of definition.stages)
|
|
170
|
+
validateStage(v2, stage);
|
|
171
|
+
if (v2.errors.length > 0)
|
|
299
172
|
throw new Error(
|
|
300
|
-
`
|
|
173
|
+
`defineWorkflow("${definition.name}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
|
|
174
|
+
` + v2.errors.join(`
|
|
175
|
+
`)
|
|
301
176
|
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
177
|
+
}
|
|
178
|
+
function createDefinitionValidator() {
|
|
179
|
+
const errors = [], tryParse = (groq, where) => {
|
|
180
|
+
try {
|
|
181
|
+
parse(groq);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
errors.push(` \xB7 ${where}: ${message}`);
|
|
185
|
+
}
|
|
186
|
+
}, rejectTypeScan = (groq, where) => {
|
|
187
|
+
/\*\s*\[\s*_type\b/.test(groq) && errors.push(
|
|
188
|
+
` \xB7 ${where}: condition scans by \`_type\` \u2014 that's a discovery query, not a predicate. Conditions evaluate against the in-memory snapshot (instance + ancestors + state-declared docs). To bring extra docs in scope, declare a \`doc.ref\` (or \`doc.refs\`) state entry on the workflow or this stage. For lake scans like "all articles in this release", use \`subworkflows.forEach\` instead.`
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
return { errors, tryParse, checkCondition: (groq, where) => {
|
|
192
|
+
tryParse(groq, where), rejectTypeScan(groq, where);
|
|
193
|
+
}, checkEntry: (entry, label) => {
|
|
194
|
+
entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
|
|
195
|
+
}, checkGuardRead: (expr, where) => {
|
|
196
|
+
isGuardReadExpr(expr) || errors.push(
|
|
197
|
+
` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
|
|
305
198
|
);
|
|
199
|
+
} };
|
|
306
200
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
|
|
317
|
-
case "reference":
|
|
318
|
-
return isGdr(value);
|
|
201
|
+
function validateStage(v2, stage) {
|
|
202
|
+
for (const entry of stage.state ?? [])
|
|
203
|
+
v2.checkEntry(entry, `stage "${stage.name}".state "${entry.name}"`);
|
|
204
|
+
for (const guard of stage.guards ?? [])
|
|
205
|
+
validateGuardReads(v2, stage.name, guard);
|
|
206
|
+
for (const t of stage.transitions ?? []) {
|
|
207
|
+
v2.checkCondition(t.filter, `transition "${t.name}" (${stage.name} \u2192 ${t.to}) filter`);
|
|
208
|
+
for (const [key, groq] of effectBindings(t.effects))
|
|
209
|
+
v2.tryParse(groq, `transition "${t.name}" effect binding "${key}"`);
|
|
319
210
|
}
|
|
211
|
+
for (const task of stage.tasks ?? [])
|
|
212
|
+
validateTask(v2, stage.name, task);
|
|
213
|
+
}
|
|
214
|
+
function validateGuardReads(v2, stageName, guard) {
|
|
215
|
+
const where = `stage "${stageName}" guard "${guard.name}"`;
|
|
216
|
+
for (const expr of guard.match.idRefs ?? [])
|
|
217
|
+
v2.checkGuardRead(expr, `${where} match.idRefs`);
|
|
218
|
+
for (const [key, expr] of Object.entries(guard.metadata ?? {}))
|
|
219
|
+
v2.checkGuardRead(expr, `${where} metadata "${key}"`);
|
|
220
|
+
}
|
|
221
|
+
function validateTask(v2, stageName, task) {
|
|
222
|
+
const where = `stage "${stageName}" task "${task.name}"`;
|
|
223
|
+
for (const entry of task.state ?? [])
|
|
224
|
+
v2.checkEntry(entry, `${where}.state "${entry.name}"`);
|
|
225
|
+
task.filter !== void 0 && v2.checkCondition(task.filter, `${where}.filter`), task.completeWhen !== void 0 && v2.checkCondition(task.completeWhen, `${where}.completeWhen`), task.failWhen !== void 0 && v2.checkCondition(task.failWhen, `${where}.failWhen`);
|
|
226
|
+
for (const [key, groq] of effectBindings(task.effects))
|
|
227
|
+
v2.tryParse(groq, `${where} effect binding "${key}"`);
|
|
228
|
+
validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
|
|
229
|
+
}
|
|
230
|
+
function validateTaskActions(v2, task) {
|
|
231
|
+
for (const a of task.actions ?? []) {
|
|
232
|
+
a.filter !== void 0 && v2.checkCondition(a.filter, `task "${task.name}" action "${a.name}".filter`);
|
|
233
|
+
for (const [key, groq] of effectBindings(a.effects))
|
|
234
|
+
v2.tryParse(groq, `task "${task.name}" action "${a.name}" effect binding "${key}"`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function validateSubworkflows(v2, where, sub) {
|
|
238
|
+
v2.tryParse(sub.forEach, `${where}.subworkflows.forEach`);
|
|
239
|
+
for (const [key, groq] of Object.entries(sub.with ?? {}))
|
|
240
|
+
v2.tryParse(groq, `${where}.subworkflows.with "${key}"`);
|
|
241
|
+
for (const [key, groq] of Object.entries(sub.context ?? {}))
|
|
242
|
+
v2.tryParse(groq, `${where}.subworkflows.context "${key}"`);
|
|
243
|
+
}
|
|
244
|
+
function effectBindings(effects) {
|
|
245
|
+
return (effects ?? []).flatMap(
|
|
246
|
+
(e) => Object.entries(e.bindings ?? {}).map(([k, groq]) => [`${e.name}.${k}`, groq])
|
|
247
|
+
);
|
|
320
248
|
}
|
|
321
|
-
|
|
322
|
-
return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
323
|
-
}
|
|
324
|
-
const GUARD_DOC_TYPE = "temp.system.guard";
|
|
249
|
+
const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
|
|
325
250
|
class MutationGuardDeniedError extends Error {
|
|
326
251
|
denied;
|
|
327
252
|
documentId;
|
|
@@ -330,9 +255,24 @@ class MutationGuardDeniedError extends Error {
|
|
|
330
255
|
const ids = args.denied.map((d) => d.guardId).join(", ");
|
|
331
256
|
super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
|
|
332
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Build the error straight from the denying guard docs — the one mapping
|
|
260
|
+
* from {@link MutationGuardDoc} to the structured `denied` payload, shared
|
|
261
|
+
* by every enforcement site (engine pre-flight, bench write seam).
|
|
262
|
+
*/
|
|
263
|
+
static fromGuards(args) {
|
|
264
|
+
return new MutationGuardDeniedError({
|
|
265
|
+
documentId: args.documentId,
|
|
266
|
+
action: args.action,
|
|
267
|
+
denied: args.guards.map((g) => ({
|
|
268
|
+
guardId: g._id,
|
|
269
|
+
...g.name !== void 0 ? { name: g.name } : {}
|
|
270
|
+
}))
|
|
271
|
+
});
|
|
272
|
+
}
|
|
333
273
|
}
|
|
334
274
|
function lakeGuardId(args) {
|
|
335
|
-
return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.
|
|
275
|
+
return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.guardName}`;
|
|
336
276
|
}
|
|
337
277
|
function compileGuard(args) {
|
|
338
278
|
return {
|
|
@@ -342,8 +282,8 @@ function compileGuard(args) {
|
|
|
342
282
|
resourceId: args.resourceId,
|
|
343
283
|
owner: args.owner,
|
|
344
284
|
sourceInstanceId: args.sourceInstanceId,
|
|
345
|
-
|
|
346
|
-
|
|
285
|
+
sourceDefinition: args.sourceDefinition,
|
|
286
|
+
sourceStage: args.sourceStage,
|
|
347
287
|
...args.name !== void 0 ? { name: args.name } : {},
|
|
348
288
|
...args.description !== void 0 ? { description: args.description } : {},
|
|
349
289
|
match: args.match,
|
|
@@ -391,68 +331,129 @@ async function denyingGuards(args) {
|
|
|
391
331
|
guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
|
|
392
332
|
return denied;
|
|
393
333
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
334
|
+
async function instanceWriteDenials(args) {
|
|
335
|
+
const { instance, guards, identity } = args, enforceable = guards.filter(
|
|
336
|
+
(g) => g.resourceType === instance.workflowResource.type && g.resourceId === instance.workflowResource.id
|
|
337
|
+
);
|
|
338
|
+
if (enforceable.length === 0) return [];
|
|
339
|
+
const doc = instance;
|
|
340
|
+
return denyingGuards({
|
|
341
|
+
guards: enforceable,
|
|
342
|
+
doc: { id: instance._id, type: instance._type },
|
|
343
|
+
context: {
|
|
344
|
+
action: "update",
|
|
345
|
+
before: doc,
|
|
346
|
+
after: doc,
|
|
347
|
+
...identity !== void 0 ? { identity } : {}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
396
350
|
}
|
|
397
|
-
function
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
return null;
|
|
351
|
+
async function assertInstanceWriteAllowed(args) {
|
|
352
|
+
const denied = await instanceWriteDenials(args);
|
|
353
|
+
if (denied.length !== 0)
|
|
354
|
+
throw MutationGuardDeniedError.fromGuards({
|
|
355
|
+
documentId: args.instance._id,
|
|
356
|
+
action: "update",
|
|
357
|
+
guards: denied
|
|
358
|
+
});
|
|
407
359
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
for (const src of idRefs ?? []) {
|
|
411
|
-
const target = resolveIdRefTarget(src, ctx);
|
|
412
|
-
if (target === null) return null;
|
|
413
|
-
targets.push(target);
|
|
414
|
-
}
|
|
415
|
-
return targets.length === 0 ? null : targets;
|
|
360
|
+
function findOpenStageEntry(host) {
|
|
361
|
+
return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
|
|
416
362
|
}
|
|
417
|
-
function
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
363
|
+
function buildParams(args) {
|
|
364
|
+
const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
|
|
365
|
+
return {
|
|
366
|
+
self: selfGdr(instance),
|
|
367
|
+
state: renderedState(instance.state ?? [], snapshot),
|
|
368
|
+
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
369
|
+
ancestors: instance.ancestors.map((a) => a.id),
|
|
370
|
+
stage: instance.currentStage,
|
|
371
|
+
now,
|
|
372
|
+
/** `$effects` — parent context handoff by entry name, completed-effect
|
|
373
|
+
* outputs namespaced under the effect's name (`$effects['x.y'].out`). */
|
|
374
|
+
effects: effectsContextMap(instance),
|
|
375
|
+
/** `$tasks` — the current stage's task rows, statuses included. */
|
|
376
|
+
tasks: currentTasks2,
|
|
377
|
+
allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
|
|
378
|
+
anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
|
|
379
|
+
...extra
|
|
380
|
+
};
|
|
427
381
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
382
|
+
function renderedState(entries, snapshot) {
|
|
383
|
+
const out = {};
|
|
384
|
+
for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
|
|
385
|
+
return out;
|
|
430
386
|
}
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
|
|
434
|
-
return inferred.length > 0 ? inferred : void 0;
|
|
387
|
+
function renderedValue(entry, snapshot) {
|
|
388
|
+
return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
|
|
435
389
|
}
|
|
436
|
-
function
|
|
390
|
+
function scopedStateOverlay(instance, snapshot, taskName) {
|
|
391
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
392
|
+
if (stageEntry === void 0) return {};
|
|
393
|
+
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
394
|
+
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
395
|
+
}
|
|
396
|
+
function assignedFor(instance, taskName, actor) {
|
|
397
|
+
if (actor === void 0) return !1;
|
|
398
|
+
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
399
|
+
return entry === void 0 ? !1 : entry.value.some(
|
|
400
|
+
(a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
function effectsContextMap(instance) {
|
|
437
404
|
const out = {};
|
|
438
|
-
for (const
|
|
439
|
-
out[
|
|
405
|
+
for (const entry of instance.effectsContext)
|
|
406
|
+
out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
|
|
440
407
|
return out;
|
|
441
408
|
}
|
|
409
|
+
function parseJsonContextEntry(entry) {
|
|
410
|
+
try {
|
|
411
|
+
return JSON.parse(entry.value);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
415
|
+
{ cause: err }
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function paramsForLake(params) {
|
|
420
|
+
return {
|
|
421
|
+
...params,
|
|
422
|
+
self: typeof params.self == "string" ? bareId(params.self) : params.self,
|
|
423
|
+
parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
|
|
424
|
+
ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
|
|
425
|
+
state: stripStateForLake(params.state)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function stripStateForLake(value) {
|
|
429
|
+
if (value == null) return value;
|
|
430
|
+
if (typeof value == "string") return bareId(value);
|
|
431
|
+
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
432
|
+
if (typeof value == "object") {
|
|
433
|
+
const out = {};
|
|
434
|
+
for (const [k, v2] of Object.entries(value))
|
|
435
|
+
out[k] = stripStateForLake(v2);
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
function bareId(id) {
|
|
441
|
+
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
442
|
+
}
|
|
442
443
|
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
443
|
-
function resolveGuard(guard,
|
|
444
|
-
const ctx = { instance,
|
|
444
|
+
function resolveGuard(guard, instance, stageName, now) {
|
|
445
|
+
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
445
446
|
if (targets === null) return null;
|
|
446
447
|
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
447
448
|
return { doc: compileGuard({
|
|
448
|
-
id: lakeGuardId({ instanceDocId: instance._id,
|
|
449
|
+
id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
|
|
449
450
|
resourceType: resource.type,
|
|
450
451
|
resourceId: resource.id,
|
|
451
452
|
owner: GUARD_OWNER,
|
|
452
453
|
sourceInstanceId: instance._id,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
sourceDefinition: instance.definition,
|
|
455
|
+
sourceStage: stageName,
|
|
456
|
+
name: guard.name,
|
|
456
457
|
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
457
458
|
match: {
|
|
458
459
|
...types !== void 0 ? { types } : {},
|
|
@@ -473,24 +474,25 @@ async function upsertGuard(client, doc) {
|
|
|
473
474
|
await client.patch(doc._id).set(body).commit();
|
|
474
475
|
}
|
|
475
476
|
function resolvedStageGuards(args) {
|
|
476
|
-
const stage = args.definition.stages.find((s) => s.
|
|
477
|
-
for (const
|
|
478
|
-
const resolved = resolveGuard(guard,
|
|
477
|
+
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
478
|
+
for (const guard of stage?.guards ?? []) {
|
|
479
|
+
const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
|
|
479
480
|
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
480
481
|
}
|
|
481
482
|
return out;
|
|
482
483
|
}
|
|
483
|
-
async function
|
|
484
|
-
return
|
|
484
|
+
async function committedInstance(args) {
|
|
485
|
+
return await args.client.getDocument(args.instance._id) ?? void 0;
|
|
485
486
|
}
|
|
486
487
|
async function deployStageGuards(args) {
|
|
487
|
-
|
|
488
|
+
const live = await committedInstance(args);
|
|
489
|
+
if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
|
|
488
490
|
for (const { client, doc } of resolvedStageGuards(args))
|
|
489
491
|
await upsertGuard(client, doc);
|
|
490
492
|
}
|
|
491
493
|
async function retractStageGuards(args) {
|
|
492
|
-
const live = await
|
|
493
|
-
if (
|
|
494
|
+
const live = await committedInstance(args);
|
|
495
|
+
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
494
496
|
for (const { client, doc } of resolvedStageGuards(args))
|
|
495
497
|
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
496
498
|
}
|
|
@@ -498,6 +500,20 @@ function randomKey(length = 12) {
|
|
|
498
500
|
const bytes = new Uint8Array(length);
|
|
499
501
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
500
502
|
}
|
|
503
|
+
async function evaluateCondition(args) {
|
|
504
|
+
const { condition, snapshot, params } = args;
|
|
505
|
+
return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
|
|
506
|
+
}
|
|
507
|
+
async function evaluatePredicates(args) {
|
|
508
|
+
const out = {};
|
|
509
|
+
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
510
|
+
out[name] = !!await runGroq(groq, args.params, args.snapshot);
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
async function runGroq(groq, params, snapshot) {
|
|
514
|
+
const tree = parse(groq, { params });
|
|
515
|
+
return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
516
|
+
}
|
|
501
517
|
function buildSnapshot(args) {
|
|
502
518
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
503
519
|
for (const { doc, resource } of args.docs) {
|
|
@@ -518,48 +534,59 @@ function rewriteRefsRecursive(value, resource) {
|
|
|
518
534
|
k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
|
|
519
535
|
return out;
|
|
520
536
|
}
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
|
|
538
|
+
function parseDefinitionSnapshot(instance) {
|
|
539
|
+
try {
|
|
540
|
+
return JSON.parse(instance.definitionSnapshot);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
|
|
544
|
+
{ cause: err }
|
|
545
|
+
);
|
|
546
|
+
}
|
|
523
547
|
}
|
|
524
548
|
function collectWatchRefs(instance) {
|
|
525
549
|
const stage = findOpenStageEntry(instance);
|
|
526
550
|
return [
|
|
527
551
|
gdrRef(instance.workflowResource, instance._id, instance._type),
|
|
528
552
|
...instance.ancestors,
|
|
529
|
-
...
|
|
530
|
-
...
|
|
531
|
-
...
|
|
532
|
-
...
|
|
553
|
+
...entryDocRefs(instance.state),
|
|
554
|
+
...entryDocRefs(stage?.state),
|
|
555
|
+
...entryReleaseRefs(instance.state),
|
|
556
|
+
...entryReleaseRefs(stage?.state)
|
|
533
557
|
];
|
|
534
558
|
}
|
|
535
559
|
function readsRaw(ref) {
|
|
536
|
-
return ref.type ===
|
|
560
|
+
return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
|
|
537
561
|
}
|
|
538
562
|
function contentReleaseName(args) {
|
|
539
563
|
const { ref, perspective } = args;
|
|
540
564
|
if (!readsRaw(ref) && Array.isArray(perspective))
|
|
541
565
|
return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
|
|
542
566
|
}
|
|
567
|
+
function subscriptionDocument(ref) {
|
|
568
|
+
return { ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type };
|
|
569
|
+
}
|
|
543
570
|
function subscriptionDocumentsForInstance(instance) {
|
|
544
571
|
const seen = /* @__PURE__ */ new Set(), documents = [];
|
|
545
572
|
for (const ref of collectWatchRefs(instance))
|
|
546
|
-
!isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push(
|
|
573
|
+
!isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push(subscriptionDocument(ref)));
|
|
547
574
|
return {
|
|
548
575
|
documents,
|
|
549
576
|
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
550
577
|
};
|
|
551
578
|
}
|
|
552
|
-
function
|
|
553
|
-
return
|
|
579
|
+
function entryDocRefs(state) {
|
|
580
|
+
return stateEntries(state).flatMap((entry) => entry._type === "doc.ref" ? isGdr(entry.value) ? [entry.value] : [] : entry._type === "doc.refs" ? Array.isArray(entry.value) ? entry.value.filter(isGdr) : [] : []);
|
|
554
581
|
}
|
|
555
|
-
function
|
|
556
|
-
return
|
|
557
|
-
(
|
|
582
|
+
function entryReleaseRefs(state) {
|
|
583
|
+
return stateEntries(state).flatMap(
|
|
584
|
+
(entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
|
|
558
585
|
);
|
|
559
586
|
}
|
|
560
|
-
function
|
|
561
|
-
return Array.isArray(
|
|
562
|
-
(
|
|
587
|
+
function stateEntries(state) {
|
|
588
|
+
return Array.isArray(state) ? state.filter(
|
|
589
|
+
(entry) => !!entry && typeof entry == "object"
|
|
563
590
|
) : [];
|
|
564
591
|
}
|
|
565
592
|
async function hydrateSnapshot(args) {
|
|
@@ -601,17 +628,18 @@ async function readDoc(client, id, perspective) {
|
|
|
601
628
|
function resourceFromParsed(parsed) {
|
|
602
629
|
return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
|
|
603
630
|
}
|
|
604
|
-
function
|
|
605
|
-
return
|
|
631
|
+
function collectEntryDocUris(resolvedStateEntries) {
|
|
632
|
+
return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
|
|
606
633
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
slotId;
|
|
634
|
+
class StateValueShapeError extends Error {
|
|
635
|
+
entryType;
|
|
636
|
+
entryName;
|
|
611
637
|
issues;
|
|
612
638
|
constructor(args) {
|
|
613
639
|
const issueText = args.issues.join("; ");
|
|
614
|
-
super(
|
|
640
|
+
super(
|
|
641
|
+
`State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
|
|
642
|
+
), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
|
|
615
643
|
}
|
|
616
644
|
}
|
|
617
645
|
const GdrShape = v.looseObject({
|
|
@@ -633,8 +661,8 @@ const GdrShape = v.looseObject({
|
|
|
633
661
|
roles: v.optional(v.array(v.string())),
|
|
634
662
|
onBehalfOf: v.optional(v.string())
|
|
635
663
|
}), AssigneeShape = v.union([
|
|
636
|
-
v.looseObject({
|
|
637
|
-
v.looseObject({
|
|
664
|
+
v.looseObject({ type: v.literal("user"), id: v.pipe(v.string(), v.minLength(1)) }),
|
|
665
|
+
v.looseObject({ type: v.literal("role"), role: v.pipe(v.string(), v.minLength(1)) })
|
|
638
666
|
]), ChecklistItemShape = v.looseObject({
|
|
639
667
|
label: v.pipe(v.string(), v.minLength(1)),
|
|
640
668
|
done: v.boolean(),
|
|
@@ -656,59 +684,59 @@ const GdrShape = v.looseObject({
|
|
|
656
684
|
v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
657
685
|
)
|
|
658
686
|
]), NullableUrl = NullableString, valueSchemas = {
|
|
659
|
-
"
|
|
660
|
-
"
|
|
661
|
-
"
|
|
662
|
-
|
|
663
|
-
"
|
|
664
|
-
"
|
|
665
|
-
"
|
|
666
|
-
"
|
|
667
|
-
"
|
|
668
|
-
"
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
687
|
+
"doc.ref": v.union([v.null(), GdrShape]),
|
|
688
|
+
"doc.refs": v.array(GdrShape),
|
|
689
|
+
"release.ref": v.union([v.null(), ReleaseRefShape]),
|
|
690
|
+
query: v.any(),
|
|
691
|
+
"value.string": NullableString,
|
|
692
|
+
"value.url": NullableUrl,
|
|
693
|
+
"value.number": NullableNumber,
|
|
694
|
+
"value.boolean": NullableBoolean,
|
|
695
|
+
"value.dateTime": NullableDateTime,
|
|
696
|
+
"value.actor": v.union([v.null(), ActorShape]),
|
|
697
|
+
checklist: v.array(ChecklistItemShape),
|
|
698
|
+
notes: v.array(NoteItemShape),
|
|
699
|
+
assignees: v.array(AssigneeShape)
|
|
672
700
|
}, itemSchemas = {
|
|
673
|
-
"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
701
|
+
"doc.refs": GdrShape,
|
|
702
|
+
checklist: ChecklistItemShape,
|
|
703
|
+
notes: NoteItemShape,
|
|
704
|
+
assignees: AssigneeShape
|
|
677
705
|
};
|
|
678
|
-
function isAppendable(
|
|
679
|
-
return
|
|
706
|
+
function isAppendable(entryType) {
|
|
707
|
+
return entryType in itemSchemas;
|
|
680
708
|
}
|
|
681
|
-
function
|
|
682
|
-
const schema = valueSchemas[args.
|
|
709
|
+
function validateStateValue(args) {
|
|
710
|
+
const schema = valueSchemas[args.entryType];
|
|
683
711
|
if (schema === void 0)
|
|
684
|
-
throw new
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
issues: [`unknown
|
|
712
|
+
throw new StateValueShapeError({
|
|
713
|
+
entryType: args.entryType,
|
|
714
|
+
entryName: args.entryName,
|
|
715
|
+
issues: [`unknown state entry type ${args.entryType}`],
|
|
688
716
|
mode: "value"
|
|
689
717
|
});
|
|
690
718
|
const result = v.safeParse(schema, args.value);
|
|
691
719
|
if (!result.success)
|
|
692
|
-
throw new
|
|
693
|
-
|
|
694
|
-
|
|
720
|
+
throw new StateValueShapeError({
|
|
721
|
+
entryType: args.entryType,
|
|
722
|
+
entryName: args.entryName,
|
|
695
723
|
issues: formatIssues(result.issues),
|
|
696
724
|
mode: "value"
|
|
697
725
|
});
|
|
698
726
|
}
|
|
699
|
-
function
|
|
700
|
-
if (!isAppendable(args.
|
|
701
|
-
throw new
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
issues: [`
|
|
727
|
+
function validateStateAppendItem(args) {
|
|
728
|
+
if (!isAppendable(args.entryType))
|
|
729
|
+
throw new StateValueShapeError({
|
|
730
|
+
entryType: args.entryType,
|
|
731
|
+
entryName: args.entryName,
|
|
732
|
+
issues: [`state entry type ${args.entryType} does not support append`],
|
|
705
733
|
mode: "item"
|
|
706
734
|
});
|
|
707
|
-
const schema = itemSchemas[args.
|
|
735
|
+
const schema = itemSchemas[args.entryType], result = v.safeParse(schema, args.item);
|
|
708
736
|
if (!result.success)
|
|
709
|
-
throw new
|
|
710
|
-
|
|
711
|
-
|
|
737
|
+
throw new StateValueShapeError({
|
|
738
|
+
entryType: args.entryType,
|
|
739
|
+
entryName: args.entryName,
|
|
712
740
|
issues: formatIssues(result.issues),
|
|
713
741
|
mode: "item"
|
|
714
742
|
});
|
|
@@ -719,115 +747,115 @@ function formatIssues(issues) {
|
|
|
719
747
|
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
|
|
720
748
|
});
|
|
721
749
|
}
|
|
722
|
-
function derivePerspectiveFromState(
|
|
723
|
-
if (
|
|
724
|
-
for (const
|
|
725
|
-
if (
|
|
726
|
-
const releaseName =
|
|
750
|
+
function derivePerspectiveFromState(entries) {
|
|
751
|
+
if (entries !== void 0) {
|
|
752
|
+
for (const entry of entries)
|
|
753
|
+
if (entry._type === "release.ref" && entry.value !== null) {
|
|
754
|
+
const releaseName = entry.value.releaseName;
|
|
727
755
|
if (typeof releaseName == "string" && releaseName.length > 0)
|
|
728
756
|
return [releaseName];
|
|
729
757
|
}
|
|
730
758
|
}
|
|
731
759
|
}
|
|
732
760
|
async function resolveDeclaredState(args) {
|
|
733
|
-
const {
|
|
734
|
-
if (
|
|
761
|
+
const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
|
|
762
|
+
if (entryDefs === void 0 || entryDefs.length === 0) return [];
|
|
735
763
|
const out = [];
|
|
736
|
-
for (const
|
|
764
|
+
for (const entry of entryDefs) {
|
|
737
765
|
const scopedCtx = { ...ctx, resolvedState: out };
|
|
738
|
-
out.push(await
|
|
766
|
+
out.push(await resolveOneEntry(entry, initialState, scopedCtx, randomKey2));
|
|
739
767
|
}
|
|
740
768
|
return out;
|
|
741
769
|
}
|
|
742
770
|
const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
743
|
-
"
|
|
744
|
-
"
|
|
745
|
-
"
|
|
746
|
-
"
|
|
771
|
+
"doc.refs",
|
|
772
|
+
"checklist",
|
|
773
|
+
"notes",
|
|
774
|
+
"assignees"
|
|
747
775
|
]);
|
|
748
|
-
function
|
|
749
|
-
return ARRAY_SLOT_TYPES.has(
|
|
776
|
+
function defaultEntryValue(entryType) {
|
|
777
|
+
return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
|
|
750
778
|
}
|
|
751
|
-
function resolveInitValue(
|
|
752
|
-
const initMatch = initialState.find((s) => s.
|
|
753
|
-
return initMatch === void 0 ? defaultValue : (assertInitValueShape(
|
|
779
|
+
function resolveInitValue(entry, initialState, defaultValue) {
|
|
780
|
+
const initMatch = initialState.find((s) => s.name === entry.name && s.type === entry.type);
|
|
781
|
+
return initMatch === void 0 ? defaultValue : (assertInitValueShape(entry, initMatch.value), initMatch.value);
|
|
754
782
|
}
|
|
755
783
|
function resolveStateReadValue(source, ctx, defaultValue) {
|
|
756
|
-
const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.
|
|
784
|
+
const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.name === source.state), targetValue = target === void 0 ? void 0 : target.value;
|
|
757
785
|
return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
|
|
758
786
|
}
|
|
759
|
-
async function resolveQueryValue(
|
|
787
|
+
async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
|
|
760
788
|
const params = paramsForLake({
|
|
761
789
|
self: ctx.selfId ?? null,
|
|
762
790
|
state: stateMapFromResolved(ctx.resolvedState ?? []),
|
|
763
|
-
stage: ctx.
|
|
764
|
-
task: ctx.
|
|
791
|
+
stage: ctx.stageName ?? null,
|
|
792
|
+
task: ctx.taskName ?? null,
|
|
765
793
|
now: ctx.now,
|
|
766
794
|
engineTags: ctx.tags ?? []
|
|
767
795
|
});
|
|
768
796
|
try {
|
|
769
797
|
const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
|
|
770
|
-
return normalizeQueryResult(
|
|
798
|
+
return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
|
|
771
799
|
} catch (err) {
|
|
772
800
|
throw new Error(
|
|
773
|
-
`Failed to resolve query
|
|
801
|
+
`Failed to resolve query entry "${entry.name}" (${entry.type}): ${err instanceof Error ? err.message : String(err)}`,
|
|
774
802
|
{ cause: err }
|
|
775
803
|
);
|
|
776
804
|
}
|
|
777
805
|
}
|
|
778
|
-
async function
|
|
779
|
-
const source =
|
|
780
|
-
return source.
|
|
806
|
+
async function resolveEntryValue(entry, initialState, ctx, defaultValue) {
|
|
807
|
+
const source = entry.source;
|
|
808
|
+
return source.type === "init" ? resolveInitValue(entry, initialState, defaultValue) : source.type === "literal" ? source.value ?? defaultValue : source.type === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.type === "query" && ctx.client !== void 0 ? resolveQueryValue(entry, source, ctx, ctx.client, defaultValue) : defaultValue;
|
|
781
809
|
}
|
|
782
|
-
function
|
|
783
|
-
const titleProp =
|
|
784
|
-
return
|
|
810
|
+
function buildResolvedEntry(entry, value, _key, now) {
|
|
811
|
+
const titleProp = entry.title !== void 0 ? { title: entry.title } : {}, descriptionProp = entry.description !== void 0 ? { description: entry.description } : {};
|
|
812
|
+
return entry.type === "query" ? {
|
|
785
813
|
_key,
|
|
786
|
-
_type: "
|
|
787
|
-
|
|
814
|
+
_type: "query",
|
|
815
|
+
name: entry.name,
|
|
788
816
|
...titleProp,
|
|
789
817
|
...descriptionProp,
|
|
790
818
|
value,
|
|
791
819
|
resolvedAt: now
|
|
792
820
|
} : {
|
|
793
821
|
_key,
|
|
794
|
-
_type:
|
|
795
|
-
|
|
822
|
+
_type: entry.type,
|
|
823
|
+
name: entry.name,
|
|
796
824
|
...titleProp,
|
|
797
825
|
...descriptionProp,
|
|
798
826
|
value
|
|
799
827
|
};
|
|
800
828
|
}
|
|
801
|
-
async function
|
|
802
|
-
const defaultValue =
|
|
803
|
-
return
|
|
829
|
+
async function resolveOneEntry(entry, initialState, ctx, randomKey2) {
|
|
830
|
+
const defaultValue = defaultEntryValue(entry.type), value = await resolveEntryValue(entry, initialState, ctx, defaultValue);
|
|
831
|
+
return validateStateValue({ entryType: entry.type, entryName: entry.name, value }), buildResolvedEntry(entry, value, randomKey2(), ctx.now);
|
|
804
832
|
}
|
|
805
|
-
function stateMapFromResolved(
|
|
833
|
+
function stateMapFromResolved(entries) {
|
|
806
834
|
const out = {};
|
|
807
|
-
for (const s of
|
|
835
|
+
for (const s of entries) out[s.name] = s.value;
|
|
808
836
|
return out;
|
|
809
837
|
}
|
|
810
|
-
function assertInitValueShape(
|
|
811
|
-
if (
|
|
812
|
-
assertGdrShape(value, `state
|
|
838
|
+
function assertInitValueShape(entry, value) {
|
|
839
|
+
if (entry.type === "doc.ref") {
|
|
840
|
+
assertGdrShape(value, `state entry "${entry.name}" (doc.ref)`);
|
|
813
841
|
return;
|
|
814
842
|
}
|
|
815
|
-
if (
|
|
816
|
-
assertGdrShape(value, `state
|
|
843
|
+
if (entry.type === "release.ref") {
|
|
844
|
+
assertGdrShape(value, `state entry "${entry.name}" (release.ref)`);
|
|
817
845
|
const v2 = value;
|
|
818
846
|
if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
|
|
819
847
|
throw new Error(
|
|
820
|
-
`Invalid init value for state
|
|
848
|
+
`Invalid init value for state entry "${entry.name}" (release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
|
|
821
849
|
);
|
|
822
850
|
return;
|
|
823
851
|
}
|
|
824
|
-
if (
|
|
852
|
+
if (entry.type === "doc.refs") {
|
|
825
853
|
if (!Array.isArray(value))
|
|
826
854
|
throw new Error(
|
|
827
|
-
`Invalid init value for state
|
|
855
|
+
`Invalid init value for state entry "${entry.name}" (doc.refs): expected an array of GDRs, got ${typeof value}.`
|
|
828
856
|
);
|
|
829
857
|
for (const [i, item] of value.entries())
|
|
830
|
-
assertGdrShape(item, `state
|
|
858
|
+
assertGdrShape(item, `state entry "${entry.name}" (doc.refs) item [${i}]`);
|
|
831
859
|
}
|
|
832
860
|
}
|
|
833
861
|
function assertGdrShape(value, context) {
|
|
@@ -845,8 +873,8 @@ function assertGdrShape(value, context) {
|
|
|
845
873
|
`Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
|
|
846
874
|
);
|
|
847
875
|
}
|
|
848
|
-
function normalizeQueryResult(
|
|
849
|
-
return raw == null ? raw :
|
|
876
|
+
function normalizeQueryResult(entryType, raw, workflowResource) {
|
|
877
|
+
return raw == null ? raw : entryType === "doc.ref" ? coerceToGdr(raw, workflowResource) : entryType === "doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v2) => v2 !== null) : [] : raw;
|
|
850
878
|
}
|
|
851
879
|
function toGdrUri(docId, workflowResource) {
|
|
852
880
|
return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
|
|
@@ -871,7 +899,7 @@ async function loadContext(client, instanceId, options) {
|
|
|
871
899
|
const instance = await client.getDocument(instanceId);
|
|
872
900
|
if (!instance)
|
|
873
901
|
throw new Error(`Workflow instance ${instanceId} not found`);
|
|
874
|
-
const definition =
|
|
902
|
+
const definition = parseDefinitionSnapshot(instance), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
|
|
875
903
|
client,
|
|
876
904
|
clientForGdr,
|
|
877
905
|
instance,
|
|
@@ -879,12 +907,28 @@ async function loadContext(client, instanceId, options) {
|
|
|
879
907
|
});
|
|
880
908
|
return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
|
|
881
909
|
}
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
910
|
+
async function ctxConditionParams(ctx, opts) {
|
|
911
|
+
const base = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), state = {
|
|
912
|
+
...base.state,
|
|
913
|
+
...scopedStateOverlay(ctx.instance, ctx.snapshot, opts?.taskName)
|
|
914
|
+
}, params = {
|
|
915
|
+
...base,
|
|
916
|
+
state,
|
|
917
|
+
actor: opts?.actor,
|
|
918
|
+
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
|
|
919
|
+
...opts?.vars
|
|
920
|
+
};
|
|
921
|
+
return { ...await evaluatePredicates({
|
|
922
|
+
predicates: ctx.definition.predicates,
|
|
886
923
|
snapshot: ctx.snapshot,
|
|
887
|
-
params
|
|
924
|
+
params
|
|
925
|
+
}), ...params };
|
|
926
|
+
}
|
|
927
|
+
async function ctxEvaluateCondition(ctx, condition, opts) {
|
|
928
|
+
return condition === void 0 ? !0 : evaluateCondition({
|
|
929
|
+
condition,
|
|
930
|
+
snapshot: ctx.snapshot,
|
|
931
|
+
params: await ctxConditionParams(ctx, opts)
|
|
888
932
|
});
|
|
889
933
|
}
|
|
890
934
|
async function buildEngineContext(args) {
|
|
@@ -899,55 +943,63 @@ async function buildEngineContext(args) {
|
|
|
899
943
|
snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
|
|
900
944
|
};
|
|
901
945
|
}
|
|
902
|
-
function findStage(definition,
|
|
903
|
-
const stage = definition.stages.find((s) => s.
|
|
946
|
+
function findStage(definition, stageName) {
|
|
947
|
+
const stage = definition.stages.find((s) => s.name === stageName);
|
|
904
948
|
if (stage === void 0)
|
|
905
|
-
throw new Error(`Stage "${
|
|
949
|
+
throw new Error(`Stage "${stageName}" not found in definition ${definition.name}`);
|
|
906
950
|
return stage;
|
|
907
951
|
}
|
|
908
|
-
async function
|
|
952
|
+
async function resolveStageStateEntries(args) {
|
|
909
953
|
const { client, instance, stage, now, initialState } = args;
|
|
910
954
|
return resolveDeclaredState({
|
|
911
|
-
|
|
955
|
+
entryDefs: stage.state,
|
|
912
956
|
initialState: initialState ?? [],
|
|
913
957
|
ctx: {
|
|
914
958
|
client,
|
|
915
959
|
now,
|
|
916
960
|
selfId: instance._id,
|
|
917
961
|
tags: instance.tags ?? [],
|
|
918
|
-
|
|
962
|
+
stageName: stage.name,
|
|
919
963
|
workflowResource: instance.workflowResource,
|
|
920
|
-
// Allow stage-scope
|
|
921
|
-
// "workflow", ... }` to see the already-resolved workflow-scope
|
|
922
|
-
// state.
|
|
964
|
+
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
965
|
+
// "workflow", ... }` to see the already-resolved workflow-scope state.
|
|
923
966
|
workflowState: instance.state ?? [],
|
|
924
967
|
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
925
968
|
},
|
|
926
969
|
randomKey
|
|
927
970
|
});
|
|
928
971
|
}
|
|
929
|
-
async function
|
|
972
|
+
async function resolveTaskStateEntries(args) {
|
|
930
973
|
const { client, instance, stage, task, now } = args;
|
|
931
974
|
return resolveDeclaredState({
|
|
932
|
-
|
|
975
|
+
entryDefs: task.state,
|
|
933
976
|
initialState: [],
|
|
934
977
|
ctx: {
|
|
935
978
|
client,
|
|
936
979
|
now,
|
|
937
980
|
selfId: instance._id,
|
|
938
981
|
tags: instance.tags ?? [],
|
|
939
|
-
|
|
982
|
+
stageName: stage.name,
|
|
940
983
|
workflowResource: instance.workflowResource,
|
|
941
|
-
|
|
984
|
+
taskName: task.name,
|
|
942
985
|
workflowState: instance.state ?? [],
|
|
943
986
|
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
944
987
|
},
|
|
945
988
|
randomKey
|
|
946
989
|
});
|
|
947
990
|
}
|
|
948
|
-
function
|
|
991
|
+
async function resolveBindings(args) {
|
|
992
|
+
const resolved = {};
|
|
993
|
+
for (const [key, groq] of Object.entries(args.bindings ?? {}))
|
|
994
|
+
resolved[key] = await runGroq(groq, args.params, args.snapshot);
|
|
995
|
+
return { ...resolved, ...args.staticInput };
|
|
996
|
+
}
|
|
997
|
+
function effectsContextEntry(name, value) {
|
|
949
998
|
const _key = randomKey();
|
|
950
|
-
return typeof value == "string" ? { _key, _type: "effectsContext.string",
|
|
999
|
+
return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
|
|
1000
|
+
}
|
|
1001
|
+
function effectsContextJsonEntry(name, value) {
|
|
1002
|
+
return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
|
|
951
1003
|
}
|
|
952
1004
|
function buildInstanceBase(args) {
|
|
953
1005
|
const { id, now, actor, perspective } = args;
|
|
@@ -959,23 +1011,23 @@ function buildInstanceBase(args) {
|
|
|
959
1011
|
_updatedAt: now,
|
|
960
1012
|
tags: args.tags,
|
|
961
1013
|
workflowResource: args.workflowResource,
|
|
962
|
-
|
|
1014
|
+
definition: args.definitionName,
|
|
963
1015
|
pinnedVersion: args.pinnedVersion,
|
|
964
1016
|
definitionSnapshot: JSON.stringify(args.definition),
|
|
965
1017
|
state: args.state,
|
|
966
1018
|
effectsContext: args.effectsContext,
|
|
967
1019
|
ancestors: args.ancestors,
|
|
968
1020
|
...perspective !== void 0 ? { perspective } : {},
|
|
969
|
-
|
|
1021
|
+
currentStage: args.initialStage,
|
|
970
1022
|
stages: [],
|
|
971
1023
|
pendingEffects: [],
|
|
972
1024
|
effectHistory: [],
|
|
973
1025
|
history: [
|
|
974
1026
|
{
|
|
975
1027
|
_key: randomKey(),
|
|
976
|
-
_type: "
|
|
1028
|
+
_type: "stageEntered",
|
|
977
1029
|
at: now,
|
|
978
|
-
|
|
1030
|
+
stage: args.initialStage,
|
|
979
1031
|
...actor !== void 0 ? { actor } : {}
|
|
980
1032
|
}
|
|
981
1033
|
],
|
|
@@ -983,253 +1035,15 @@ function buildInstanceBase(args) {
|
|
|
983
1035
|
lastChangedAt: now
|
|
984
1036
|
};
|
|
985
1037
|
}
|
|
986
|
-
function startMutation(instance) {
|
|
987
|
-
return {
|
|
988
|
-
currentStageId: instance.currentStageId,
|
|
989
|
-
// Deep-ish copy. Per-scope state slot arrays need their own copies
|
|
990
|
-
// so ops can mutate without leaking into the source.
|
|
991
|
-
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
992
|
-
stages: instance.stages.map((s) => ({
|
|
993
|
-
...s,
|
|
994
|
-
state: (s.state ?? []).map((slot) => ({ ...slot })),
|
|
995
|
-
tasks: s.tasks.map((t) => ({
|
|
996
|
-
...t,
|
|
997
|
-
...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
|
|
998
|
-
}))
|
|
999
|
-
})),
|
|
1000
|
-
pendingEffects: [...instance.pendingEffects],
|
|
1001
|
-
effectHistory: [...instance.effectHistory],
|
|
1002
|
-
effectsContext: [...instance.effectsContext],
|
|
1003
|
-
history: [...instance.history],
|
|
1004
|
-
lastChangedAt: instance.lastChangedAt,
|
|
1005
|
-
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
1006
|
-
pendingCreates: []
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
function taskStatusChangedEntry(args) {
|
|
1010
|
-
return {
|
|
1011
|
-
_key: randomKey(),
|
|
1012
|
-
_type: "workflow.history.taskStatusChanged",
|
|
1013
|
-
at: args.at,
|
|
1014
|
-
stageId: args.stageId,
|
|
1015
|
-
taskId: args.taskId,
|
|
1016
|
-
from: args.from,
|
|
1017
|
-
to: args.to,
|
|
1018
|
-
...args.actor !== void 0 ? { actor: args.actor } : {}
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
function stageTransitionHistory(args) {
|
|
1022
|
-
const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
|
|
1023
|
-
at,
|
|
1024
|
-
...transitionId !== void 0 ? { transitionId } : {},
|
|
1025
|
-
...actor !== void 0 ? { actor } : {},
|
|
1026
|
-
...via !== void 0 ? { via } : {},
|
|
1027
|
-
...reason !== void 0 ? { reason } : {}
|
|
1028
|
-
};
|
|
1029
|
-
return [
|
|
1030
|
-
{
|
|
1031
|
-
_key: randomKey(),
|
|
1032
|
-
_type: "workflow.history.stageExited",
|
|
1033
|
-
stageId: fromStageId,
|
|
1034
|
-
toStageId,
|
|
1035
|
-
...shared
|
|
1036
|
-
},
|
|
1037
|
-
{
|
|
1038
|
-
_key: randomKey(),
|
|
1039
|
-
_type: "workflow.history.transitionFired",
|
|
1040
|
-
fromStageId,
|
|
1041
|
-
toStageId,
|
|
1042
|
-
...shared
|
|
1043
|
-
},
|
|
1044
|
-
{
|
|
1045
|
-
_key: randomKey(),
|
|
1046
|
-
_type: "workflow.history.stageEntered",
|
|
1047
|
-
stageId: toStageId,
|
|
1048
|
-
fromStageId,
|
|
1049
|
-
...shared
|
|
1050
|
-
}
|
|
1051
|
-
];
|
|
1052
|
-
}
|
|
1053
|
-
function instanceStateFields(src) {
|
|
1054
|
-
const fields = {
|
|
1055
|
-
currentStageId: src.currentStageId,
|
|
1056
|
-
state: src.state,
|
|
1057
|
-
stages: src.stages,
|
|
1058
|
-
pendingEffects: src.pendingEffects,
|
|
1059
|
-
effectHistory: src.effectHistory,
|
|
1060
|
-
effectsContext: src.effectsContext,
|
|
1061
|
-
history: src.history
|
|
1062
|
-
};
|
|
1063
|
-
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
|
|
1064
|
-
}
|
|
1065
|
-
function materializeInstance(base, mutation) {
|
|
1066
|
-
return {
|
|
1067
|
-
...base,
|
|
1068
|
-
currentStageId: mutation.currentStageId,
|
|
1069
|
-
state: mutation.state,
|
|
1070
|
-
stages: mutation.stages
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
async function persist(ctx, mutation) {
|
|
1074
|
-
const set = {
|
|
1075
|
-
...instanceStateFields(mutation),
|
|
1076
|
-
lastChangedAt: ctx.now
|
|
1077
|
-
}, pendingCreates = mutation.pendingCreates;
|
|
1078
|
-
if (pendingCreates.length === 0)
|
|
1079
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1080
|
-
const tx = ctx.client.transaction();
|
|
1081
|
-
for (const body of pendingCreates) tx.create(body);
|
|
1082
|
-
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1083
|
-
const actorForPriming = void 0;
|
|
1084
|
-
for (const body of pendingCreates)
|
|
1085
|
-
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
1086
|
-
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1087
|
-
if (!reloaded)
|
|
1088
|
-
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
1089
|
-
return reloaded;
|
|
1090
|
-
}
|
|
1091
|
-
function currentStageEntry(mutation) {
|
|
1092
|
-
const entry = findOpenStageEntry(mutation);
|
|
1093
|
-
if (entry === void 0)
|
|
1094
|
-
throw new Error(
|
|
1095
|
-
`Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
|
|
1096
|
-
);
|
|
1097
|
-
return entry;
|
|
1098
|
-
}
|
|
1099
|
-
function currentTasks(mutation) {
|
|
1100
|
-
return currentStageEntry(mutation).tasks;
|
|
1101
|
-
}
|
|
1102
|
-
function findTaskInCurrentStage(ctx, taskId) {
|
|
1103
|
-
const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
|
|
1104
|
-
if (task === void 0)
|
|
1105
|
-
throw new Error(
|
|
1106
|
-
`Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
|
|
1107
|
-
);
|
|
1108
|
-
return { stage, task };
|
|
1109
|
-
}
|
|
1110
|
-
function requireMutationTaskEntry(mutation, taskId) {
|
|
1111
|
-
const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
|
|
1112
|
-
if (mutEntry === void 0)
|
|
1113
|
-
throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
|
|
1114
|
-
return mutEntry;
|
|
1115
|
-
}
|
|
1116
|
-
function findCurrentStageEntry(instance) {
|
|
1117
|
-
return findOpenStageEntry(instance);
|
|
1118
|
-
}
|
|
1119
|
-
function findCurrentTasks(instance) {
|
|
1120
|
-
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
1121
|
-
}
|
|
1122
|
-
async function completeEffect(args) {
|
|
1123
|
-
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1124
|
-
...options?.clock ? { clock: options.clock } : {}
|
|
1125
|
-
});
|
|
1126
|
-
return commitCompleteEffect(
|
|
1127
|
-
ctx,
|
|
1128
|
-
effectKey,
|
|
1129
|
-
status,
|
|
1130
|
-
outputs,
|
|
1131
|
-
detail,
|
|
1132
|
-
error,
|
|
1133
|
-
durationMs,
|
|
1134
|
-
options?.actor
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
function buildEffectHistoryEntry(pending, outcome) {
|
|
1138
|
-
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
1139
|
-
return {
|
|
1140
|
-
_key: pending._key,
|
|
1141
|
-
effectId: pending.effectId,
|
|
1142
|
-
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
1143
|
-
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
1144
|
-
params: pending.params,
|
|
1145
|
-
origin: pending.origin,
|
|
1146
|
-
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
1147
|
-
ranAt,
|
|
1148
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
1149
|
-
status,
|
|
1150
|
-
...detail !== void 0 ? { detail } : {},
|
|
1151
|
-
...error !== void 0 ? { error } : {},
|
|
1152
|
-
...outputs !== void 0 ? { outputs } : {}
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
function upsertEffectsContext(mutation, outputs) {
|
|
1156
|
-
for (const [id, value] of Object.entries(outputs)) {
|
|
1157
|
-
const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
|
|
1158
|
-
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
1162
|
-
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
1163
|
-
if (pending === void 0)
|
|
1164
|
-
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
1165
|
-
const mutation = startMutation(ctx.instance);
|
|
1166
|
-
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1167
|
-
const ranAt = ctx.now;
|
|
1168
|
-
return mutation.effectHistory.push(
|
|
1169
|
-
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1170
|
-
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
|
|
1171
|
-
_key: randomKey(),
|
|
1172
|
-
_type: "workflow.history.effectCompleted",
|
|
1173
|
-
at: ranAt,
|
|
1174
|
-
effectKey,
|
|
1175
|
-
effectId: pending.effectId,
|
|
1176
|
-
status,
|
|
1177
|
-
...outputs !== void 0 ? { outputs } : {},
|
|
1178
|
-
...detail !== void 0 ? { detail } : {},
|
|
1179
|
-
...actor !== void 0 ? { actor } : {}
|
|
1180
|
-
}), await persist(ctx, mutation), { effectKey, status };
|
|
1181
|
-
}
|
|
1182
|
-
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1183
|
-
const key = randomKey(), pending = {
|
|
1184
|
-
_key: key,
|
|
1185
|
-
_type: "workflow.pendingEffect",
|
|
1186
|
-
effectId: effect.id,
|
|
1187
|
-
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
1188
|
-
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
1189
|
-
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
1190
|
-
params,
|
|
1191
|
-
origin,
|
|
1192
|
-
...actor !== void 0 ? { actor } : {},
|
|
1193
|
-
queuedAt: now
|
|
1194
|
-
}, history = {
|
|
1195
|
-
_key: randomKey(),
|
|
1196
|
-
_type: "workflow.history.effectQueued",
|
|
1197
|
-
at: now,
|
|
1198
|
-
effectKey: key,
|
|
1199
|
-
effectId: effect.id,
|
|
1200
|
-
origin
|
|
1201
|
-
};
|
|
1202
|
-
return { pending, history };
|
|
1203
|
-
}
|
|
1204
|
-
async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
|
|
1205
|
-
if (!effects) return;
|
|
1206
|
-
const now = ctx.now, liveInstance = {
|
|
1207
|
-
...ctx.instance,
|
|
1208
|
-
state: mutation.state,
|
|
1209
|
-
stages: mutation.stages,
|
|
1210
|
-
effectsContext: mutation.effectsContext
|
|
1211
|
-
};
|
|
1212
|
-
for (const effect of effects) {
|
|
1213
|
-
const params = await resolveBindings({
|
|
1214
|
-
bindings: effect.bindings,
|
|
1215
|
-
staticInput: effect.input,
|
|
1216
|
-
instance: liveInstance,
|
|
1217
|
-
now,
|
|
1218
|
-
...callerParams !== void 0 ? { params: callerParams } : {},
|
|
1219
|
-
...actor !== void 0 ? { actor } : {}
|
|
1220
|
-
}), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
|
|
1221
|
-
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
1038
|
class ActionParamsInvalidError extends Error {
|
|
1225
1039
|
action;
|
|
1226
|
-
|
|
1040
|
+
task;
|
|
1227
1041
|
issues;
|
|
1228
1042
|
constructor(args) {
|
|
1229
|
-
const lines = args.issues.map((i) => ` - ${i.
|
|
1043
|
+
const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
|
|
1230
1044
|
`);
|
|
1231
|
-
super(`Action "${args.action}" on task "${args.
|
|
1232
|
-
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.
|
|
1045
|
+
super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
|
|
1046
|
+
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
|
|
1233
1047
|
}
|
|
1234
1048
|
}
|
|
1235
1049
|
class WorkflowStateDivergedError extends Error {
|
|
@@ -1248,13 +1062,13 @@ class WorkflowStateDivergedError extends Error {
|
|
|
1248
1062
|
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
1249
1063
|
class ConcurrentFireActionError extends Error {
|
|
1250
1064
|
instanceId;
|
|
1251
|
-
|
|
1065
|
+
task;
|
|
1252
1066
|
action;
|
|
1253
1067
|
attempts;
|
|
1254
1068
|
constructor(args) {
|
|
1255
1069
|
super(
|
|
1256
|
-
`Action "${args.action}" on task "${args.
|
|
1257
|
-
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.
|
|
1070
|
+
`Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
|
|
1071
|
+
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
|
|
1258
1072
|
}
|
|
1259
1073
|
}
|
|
1260
1074
|
function isRevisionConflict(error) {
|
|
@@ -1295,6 +1109,51 @@ async function deployOrRollback(args) {
|
|
|
1295
1109
|
throw guardError;
|
|
1296
1110
|
}
|
|
1297
1111
|
}
|
|
1112
|
+
function applyTaskStatusChange(args) {
|
|
1113
|
+
const { entry, history, stage, to, at, actor } = args, from = entry.status;
|
|
1114
|
+
entry.status = to, isTerminalTaskStatus(to) && (entry.completedAt = at, actor !== void 0 && (entry.completedBy = actor.id)), history.push({
|
|
1115
|
+
_key: randomKey(),
|
|
1116
|
+
_type: "taskStatusChanged",
|
|
1117
|
+
at,
|
|
1118
|
+
stage,
|
|
1119
|
+
task: entry.name,
|
|
1120
|
+
from,
|
|
1121
|
+
to,
|
|
1122
|
+
...actor !== void 0 ? { actor } : {}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function stageTransitionHistory(args) {
|
|
1126
|
+
const { fromStage, toStage, at, transition, actor, via, reason } = args, shared = {
|
|
1127
|
+
at,
|
|
1128
|
+
...transition !== void 0 ? { transition } : {},
|
|
1129
|
+
...actor !== void 0 ? { actor } : {},
|
|
1130
|
+
...via !== void 0 ? { via } : {},
|
|
1131
|
+
...reason !== void 0 ? { reason } : {}
|
|
1132
|
+
};
|
|
1133
|
+
return [
|
|
1134
|
+
{
|
|
1135
|
+
_key: randomKey(),
|
|
1136
|
+
_type: "stageExited",
|
|
1137
|
+
stage: fromStage,
|
|
1138
|
+
toStage,
|
|
1139
|
+
...shared
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
_key: randomKey(),
|
|
1143
|
+
_type: "transitionFired",
|
|
1144
|
+
fromStage,
|
|
1145
|
+
toStage,
|
|
1146
|
+
...shared
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
_key: randomKey(),
|
|
1150
|
+
_type: "stageEntered",
|
|
1151
|
+
stage: toStage,
|
|
1152
|
+
fromStage,
|
|
1153
|
+
...shared
|
|
1154
|
+
}
|
|
1155
|
+
];
|
|
1156
|
+
}
|
|
1298
1157
|
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1299
1158
|
function validateTags(tags) {
|
|
1300
1159
|
if (tags.length === 0)
|
|
@@ -1312,7 +1171,7 @@ function tagScopeFilter(param = "engineTags") {
|
|
|
1312
1171
|
return `count(tags[@ in $${param}]) > 0`;
|
|
1313
1172
|
}
|
|
1314
1173
|
function definitionLookupGroq(explicit) {
|
|
1315
|
-
const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" &&
|
|
1174
|
+
const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
|
|
1316
1175
|
return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
|
|
1317
1176
|
}
|
|
1318
1177
|
async function discoverItems(args) {
|
|
@@ -1327,23 +1186,49 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
1327
1186
|
return defaultClient;
|
|
1328
1187
|
}
|
|
1329
1188
|
}
|
|
1330
|
-
const MAX_SPAWN_DEPTH = 6;
|
|
1331
|
-
|
|
1189
|
+
const MAX_SPAWN_DEPTH = 6, SUBWORKFLOWS_ALL_DONE = "count($subworkflows[status != 'done']) == 0", FAIL_WHEN_SYSTEM_ID = "engine.failWhen", COMPLETE_WHEN_SYSTEM_ID = "engine.completeWhen";
|
|
1190
|
+
function gateActor(outcome) {
|
|
1191
|
+
return {
|
|
1192
|
+
kind: "system",
|
|
1193
|
+
id: outcome === "failed" ? FAIL_WHEN_SYSTEM_ID : COMPLETE_WHEN_SYSTEM_ID
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function effectiveCompleteWhen(task) {
|
|
1197
|
+
return task.completeWhen ?? (task.subworkflows !== void 0 ? SUBWORKFLOWS_ALL_DONE : void 0);
|
|
1198
|
+
}
|
|
1199
|
+
async function evaluateResolutionGate(ctx, task, vars) {
|
|
1200
|
+
const scope = { taskName: task.name, vars };
|
|
1201
|
+
if (task.failWhen !== void 0 && await ctxEvaluateCondition(ctx, task.failWhen, scope))
|
|
1202
|
+
return "failed";
|
|
1203
|
+
const completeWhen = effectiveCompleteWhen(task);
|
|
1204
|
+
if (completeWhen !== void 0 && await ctxEvaluateCondition(ctx, completeWhen, scope))
|
|
1205
|
+
return "done";
|
|
1206
|
+
}
|
|
1207
|
+
async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
1332
1208
|
if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
|
|
1333
1209
|
const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
|
|
1334
1210
|
throw new Error(
|
|
1335
|
-
`
|
|
1211
|
+
`Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tags ?? []);
|
|
1215
|
+
if (definition === null) {
|
|
1216
|
+
const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`Subworkflow definition "${sub.definition.name}" ${versionLabel} not deployed (parent ${ctx.instance._id}, task ${task.name})`
|
|
1336
1219
|
);
|
|
1337
1220
|
}
|
|
1338
|
-
const
|
|
1221
|
+
const parentScope = await ctxConditionParams(ctx, {
|
|
1222
|
+
taskName: task.name,
|
|
1223
|
+
...actor ? { actor } : {}
|
|
1224
|
+
}), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
|
|
1339
1225
|
for (const row of rows) {
|
|
1340
|
-
const
|
|
1226
|
+
const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
|
|
1341
1227
|
client: ctx.client,
|
|
1342
1228
|
parent: ctx.instance,
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
effectsContext: projected.effectsContext,
|
|
1229
|
+
definition,
|
|
1230
|
+
initialState,
|
|
1231
|
+
effectsContext,
|
|
1347
1232
|
actor,
|
|
1348
1233
|
now
|
|
1349
1234
|
});
|
|
@@ -1351,8 +1236,8 @@ async function spawnChildren(ctx, mutation, task, spawn, actor) {
|
|
|
1351
1236
|
}
|
|
1352
1237
|
return refs;
|
|
1353
1238
|
}
|
|
1354
|
-
async function resolveSpawnRows(ctx,
|
|
1355
|
-
const params = buildParams(ctx.instance, ctx.now), groq =
|
|
1239
|
+
async function resolveSpawnRows(ctx, sub) {
|
|
1240
|
+
const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
|
|
1356
1241
|
let value;
|
|
1357
1242
|
if (groq.includes("*")) {
|
|
1358
1243
|
const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
|
|
@@ -1368,60 +1253,39 @@ async function resolveSpawnRows(ctx, spawn) {
|
|
|
1368
1253
|
}
|
|
1369
1254
|
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
1370
1255
|
}
|
|
1371
|
-
function firstDocRefGdrUri(
|
|
1372
|
-
if (
|
|
1373
|
-
for (const s of
|
|
1374
|
-
if (s._type === "
|
|
1256
|
+
function firstDocRefGdrUri(entries) {
|
|
1257
|
+
if (entries) {
|
|
1258
|
+
for (const s of entries)
|
|
1259
|
+
if (s._type === "doc.ref" && s.value !== null) return s.value.id;
|
|
1375
1260
|
}
|
|
1376
1261
|
}
|
|
1377
|
-
function
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
case "stageId":
|
|
1390
|
-
return parent.currentStageId;
|
|
1391
|
-
case "now":
|
|
1392
|
-
return now;
|
|
1393
|
-
case "object": {
|
|
1394
|
-
const out = {};
|
|
1395
|
-
for (const [k, v2] of Object.entries(source.fields))
|
|
1396
|
-
out[k] = resolveSpawnSource(v2, parent, row, now);
|
|
1397
|
-
return out;
|
|
1398
|
-
}
|
|
1399
|
-
case "actor":
|
|
1400
|
-
case "param":
|
|
1401
|
-
case "stateRead":
|
|
1402
|
-
case "effectOutput":
|
|
1403
|
-
return null;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
function projectSpawnRow(parent, spawn, row, now) {
|
|
1407
|
-
const initialState = [];
|
|
1408
|
-
for (const entry of spawn.forEach.as.initialState ?? []) {
|
|
1409
|
-
const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
|
|
1410
|
-
initialState.push({
|
|
1411
|
-
type: entry.type,
|
|
1412
|
-
id: entry.id,
|
|
1262
|
+
async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
1263
|
+
const out = [];
|
|
1264
|
+
for (const [name, groq] of Object.entries(sub.with ?? {})) {
|
|
1265
|
+
const declared = (childDefinition.state ?? []).find((e) => e.name === name);
|
|
1266
|
+
if (declared === void 0)
|
|
1267
|
+
throw new Error(
|
|
1268
|
+
`subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
|
|
1269
|
+
);
|
|
1270
|
+
const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
|
|
1271
|
+
value != null && out.push({
|
|
1272
|
+
type: declared.type,
|
|
1273
|
+
name,
|
|
1413
1274
|
value
|
|
1414
1275
|
});
|
|
1415
1276
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
async function resolveContextHandoff(ctx, sub, parentScope) {
|
|
1280
|
+
const out = {};
|
|
1281
|
+
for (const [name, groq] of Object.entries(sub.context ?? {})) {
|
|
1282
|
+
const v2 = await runGroq(groq, parentScope, ctx.snapshot);
|
|
1283
|
+
v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (out[name] = v2);
|
|
1420
1284
|
}
|
|
1421
|
-
return
|
|
1285
|
+
return out;
|
|
1422
1286
|
}
|
|
1423
|
-
function canonicaliseSpawnValue(
|
|
1424
|
-
return
|
|
1287
|
+
function canonicaliseSpawnValue(kind, value, workflowResource) {
|
|
1288
|
+
return kind === "doc.ref" ? coerceSpawnGdr(value, workflowResource) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
|
|
1425
1289
|
}
|
|
1426
1290
|
function coerceSpawnGdr(raw, workflowResource) {
|
|
1427
1291
|
const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
|
|
@@ -1437,29 +1301,22 @@ function coerceSpawnGdr(raw, workflowResource) {
|
|
|
1437
1301
|
}
|
|
1438
1302
|
return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
|
|
1439
1303
|
}
|
|
1440
|
-
async function
|
|
1441
|
-
const wantsExplicit = typeof ref.version == "number", params = {
|
|
1304
|
+
async function resolveDefinitionRef(client, ref, tags) {
|
|
1305
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
|
|
1442
1306
|
return wantsExplicit && (params.version = ref.version), await client.fetch(
|
|
1443
1307
|
definitionLookupGroq(wantsExplicit),
|
|
1444
1308
|
params
|
|
1445
1309
|
) ?? null;
|
|
1446
1310
|
}
|
|
1447
1311
|
async function prepareChildInstance(args) {
|
|
1448
|
-
const { client, parent,
|
|
1449
|
-
if (definition === null) {
|
|
1450
|
-
const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
`Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
|
|
1312
|
+
const { client, parent, definition, initialState, effectsContext, actor, now } = args, childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
|
|
1456
1313
|
...parent.ancestors,
|
|
1457
1314
|
{
|
|
1458
1315
|
id: selfGdr(parent),
|
|
1459
1316
|
type: WORKFLOW_INSTANCE_TYPE
|
|
1460
1317
|
}
|
|
1461
1318
|
], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
|
|
1462
|
-
|
|
1319
|
+
entryDefs: definition.state,
|
|
1463
1320
|
initialState,
|
|
1464
1321
|
ctx: {
|
|
1465
1322
|
client,
|
|
@@ -1475,7 +1332,7 @@ async function prepareChildInstance(args) {
|
|
|
1475
1332
|
if (typeof value == "object" && value !== null && "id" in value && "type" in value) {
|
|
1476
1333
|
if (!isGdrUri(value.id))
|
|
1477
1334
|
throw new Error(
|
|
1478
|
-
`
|
|
1335
|
+
`subworkflows.context["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
|
|
1479
1336
|
);
|
|
1480
1337
|
return effectsContextEntry(key, { id: value.id, type: value.type });
|
|
1481
1338
|
}
|
|
@@ -1486,77 +1343,274 @@ async function prepareChildInstance(args) {
|
|
|
1486
1343
|
now,
|
|
1487
1344
|
tags: childTags,
|
|
1488
1345
|
workflowResource,
|
|
1489
|
-
|
|
1346
|
+
definitionName: definition.name,
|
|
1490
1347
|
pinnedVersion: definition.version,
|
|
1491
1348
|
definition,
|
|
1492
1349
|
state: childState,
|
|
1493
1350
|
effectsContext: effectsContextEntries,
|
|
1494
1351
|
ancestors,
|
|
1495
1352
|
perspective: childPerspective,
|
|
1496
|
-
|
|
1353
|
+
initialStage: definition.initialStage,
|
|
1497
1354
|
actor
|
|
1498
1355
|
});
|
|
1499
1356
|
return { ref: childRef, body: base };
|
|
1500
1357
|
}
|
|
1501
|
-
async function
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1358
|
+
async function subworkflowRows(ctx, refs) {
|
|
1359
|
+
if (refs.length === 0) return [];
|
|
1360
|
+
const ids = refs.map((r) => gdrToBareId(r.id));
|
|
1361
|
+
return (await ctx.client.fetch("*[_id in $ids]{_id, currentStage, completedAt}", { ids })).map((c) => ({
|
|
1362
|
+
_id: c._id,
|
|
1363
|
+
stage: c.currentStage,
|
|
1364
|
+
status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
1367
|
+
function resolveStaticSource(src, ctx) {
|
|
1368
|
+
switch (src.type) {
|
|
1369
|
+
case "literal":
|
|
1370
|
+
return { handled: !0, value: src.value };
|
|
1371
|
+
case "param":
|
|
1372
|
+
return { handled: !0, value: ctx.params?.[src.param] };
|
|
1373
|
+
case "actor":
|
|
1374
|
+
return { handled: !0, value: ctx.actor };
|
|
1375
|
+
case "now":
|
|
1376
|
+
return { handled: !0, value: ctx.now };
|
|
1377
|
+
default:
|
|
1378
|
+
return { handled: !1 };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const STATE_OP_TYPES = [
|
|
1382
|
+
"state.set",
|
|
1383
|
+
"state.unset",
|
|
1384
|
+
"state.append",
|
|
1385
|
+
"state.updateWhere",
|
|
1386
|
+
"state.removeWhere"
|
|
1387
|
+
];
|
|
1388
|
+
function isStateOp(summary) {
|
|
1389
|
+
return STATE_OP_TYPES.includes(summary.opType);
|
|
1390
|
+
}
|
|
1391
|
+
function validateActionParams(action, taskName, callerParams) {
|
|
1392
|
+
const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
|
|
1393
|
+
for (const decl of declared) {
|
|
1394
|
+
const value = params[decl.name], present = decl.name in params && value !== void 0 && value !== null;
|
|
1395
|
+
if (decl.required === !0 && !present) {
|
|
1396
|
+
issues.push({ param: decl.name, reason: "required but missing" });
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
present && (checkParamType(value, decl) || issues.push({
|
|
1400
|
+
param: decl.name,
|
|
1401
|
+
reason: `expected type=${decl.type}, got ${typeof value}`
|
|
1402
|
+
}));
|
|
1403
|
+
}
|
|
1404
|
+
if (issues.length > 0)
|
|
1405
|
+
throw new ActionParamsInvalidError({ action: action.name, task: taskName, issues });
|
|
1406
|
+
return params;
|
|
1407
|
+
}
|
|
1408
|
+
function hasStringField(value, field) {
|
|
1409
|
+
return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
|
|
1410
|
+
}
|
|
1411
|
+
function checkParamType(value, decl) {
|
|
1412
|
+
switch (decl.type) {
|
|
1413
|
+
case "string":
|
|
1414
|
+
case "url":
|
|
1415
|
+
case "dateTime":
|
|
1416
|
+
return typeof value == "string";
|
|
1417
|
+
case "number":
|
|
1418
|
+
return typeof value == "number" && Number.isFinite(value);
|
|
1419
|
+
case "boolean":
|
|
1420
|
+
return typeof value == "boolean";
|
|
1421
|
+
case "actor":
|
|
1422
|
+
return hasStringField(value, "kind");
|
|
1423
|
+
case "doc.ref":
|
|
1424
|
+
return hasStringField(value, "id");
|
|
1425
|
+
case "doc.refs":
|
|
1426
|
+
return Array.isArray(value) && value.every((v2) => checkParamType(v2, { type: "doc.ref" }));
|
|
1427
|
+
case "json":
|
|
1428
|
+
return !0;
|
|
1429
|
+
default:
|
|
1430
|
+
return !1;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function runOps(args) {
|
|
1434
|
+
const { ops, mutation, stage, origin, params, actor, self, now } = args;
|
|
1435
|
+
if (ops === void 0 || ops.length === 0) return [];
|
|
1436
|
+
const summaries = [];
|
|
1437
|
+
for (const op of ops) {
|
|
1438
|
+
const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
|
|
1439
|
+
summaries.push(summary), mutation.history.push({
|
|
1440
|
+
_key: randomKey(),
|
|
1441
|
+
_type: "opApplied",
|
|
1442
|
+
at: now,
|
|
1443
|
+
stage,
|
|
1444
|
+
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
1445
|
+
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
1446
|
+
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
1447
|
+
opType: summary.opType,
|
|
1448
|
+
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
1449
|
+
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
1450
|
+
...actor !== void 0 ? { actor } : {}
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
return summaries;
|
|
1454
|
+
}
|
|
1455
|
+
function applyOp(op, ctx) {
|
|
1456
|
+
switch (op.type) {
|
|
1457
|
+
case "state.set":
|
|
1458
|
+
return applyStateSet(op, ctx);
|
|
1459
|
+
case "state.unset":
|
|
1460
|
+
return applyStateUnset(op, ctx);
|
|
1461
|
+
case "state.append":
|
|
1462
|
+
return applyStateAppend(op, ctx);
|
|
1463
|
+
case "state.updateWhere":
|
|
1464
|
+
return applyStateUpdateWhere(op, ctx);
|
|
1465
|
+
case "state.removeWhere":
|
|
1466
|
+
return applyStateRemoveWhere(op, ctx);
|
|
1467
|
+
case "status.set":
|
|
1468
|
+
return applyStatusSet(op, ctx);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function applyStateSet(op, ctx) {
|
|
1472
|
+
const value = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
|
|
1473
|
+
return validateStateValue({ entryType: entry._type, entryName: entry.name, value }), setEntryValue(entry, value), { opType: op.type, target: op.target, resolved: { value } };
|
|
1474
|
+
}
|
|
1475
|
+
const EMPTY_BY_KIND = {
|
|
1476
|
+
"doc.refs": [],
|
|
1477
|
+
checklist: [],
|
|
1478
|
+
notes: [],
|
|
1479
|
+
assignees: []
|
|
1480
|
+
};
|
|
1481
|
+
function applyStateUnset(op, ctx) {
|
|
1482
|
+
const entry = locateEntry(ctx, op.target);
|
|
1483
|
+
return setEntryValue(entry, EMPTY_BY_KIND[entry._type] ?? null), { opType: op.type, target: op.target };
|
|
1484
|
+
}
|
|
1485
|
+
function applyStateAppend(op, ctx) {
|
|
1486
|
+
const item = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
|
|
1487
|
+
validateStateAppendItem({ entryType: entry._type, entryName: entry.name, item });
|
|
1488
|
+
const current = Array.isArray(entry.value) ? entry.value : [];
|
|
1489
|
+
return setEntryValue(entry, [...current, withRowKey(item)]), { opType: op.type, target: op.target, resolved: { item } };
|
|
1490
|
+
}
|
|
1491
|
+
function withRowKey(item) {
|
|
1492
|
+
return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
|
|
1493
|
+
}
|
|
1494
|
+
function applyStateUpdateWhere(op, ctx) {
|
|
1495
|
+
const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op), merge = resolveOpSource(op.value, ctx);
|
|
1496
|
+
if (merge === null || typeof merge != "object" || Array.isArray(merge))
|
|
1497
|
+
throw new Error(
|
|
1498
|
+
`state.updateWhere value must resolve to an object of fields to merge (target "${op.target.state}")`
|
|
1499
|
+
);
|
|
1500
|
+
return setEntryValue(
|
|
1501
|
+
entry,
|
|
1502
|
+
rows.map(
|
|
1503
|
+
(row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...merge } : row
|
|
1504
|
+
)
|
|
1505
|
+
), { opType: op.type, target: op.target, resolved: { merge } };
|
|
1506
|
+
}
|
|
1507
|
+
function applyStateRemoveWhere(op, ctx) {
|
|
1508
|
+
const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op);
|
|
1509
|
+
return setEntryValue(
|
|
1510
|
+
entry,
|
|
1511
|
+
rows.filter((row) => !evalOpPredicate(op.where, row, ctx))
|
|
1512
|
+
), { opType: op.type, target: op.target };
|
|
1513
|
+
}
|
|
1514
|
+
function requireArrayValue(entry, op) {
|
|
1515
|
+
if (!Array.isArray(entry.value))
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
`${op.type} target ${op.target.scope}:"${op.target.state}" holds a non-array ${entry._type} value \u2014 where-ops operate on array entries`
|
|
1518
|
+
);
|
|
1519
|
+
return entry.value;
|
|
1520
|
+
}
|
|
1521
|
+
function applyStatusSet(op, ctx) {
|
|
1522
|
+
const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.name === op.task);
|
|
1523
|
+
if (entry === void 0)
|
|
1524
|
+
throw new Error(`status.set targets task "${op.task}" which has no entry in the open stage`);
|
|
1525
|
+
return applyTaskStatusChange({
|
|
1526
|
+
entry,
|
|
1527
|
+
history: ctx.mutation.history,
|
|
1528
|
+
stage: ctx.stage,
|
|
1529
|
+
to: op.status,
|
|
1530
|
+
at: ctx.now,
|
|
1531
|
+
...ctx.actor !== void 0 ? { actor: ctx.actor } : {}
|
|
1532
|
+
}), { opType: op.type, resolved: { task: op.task, status: op.status } };
|
|
1533
|
+
}
|
|
1534
|
+
function setEntryValue(entry, value) {
|
|
1535
|
+
entry.value = value;
|
|
1536
|
+
}
|
|
1537
|
+
function locateEntry(ctx, target) {
|
|
1538
|
+
const entry = stateHost(ctx, target.scope)?.find((s) => s.name === target.state);
|
|
1539
|
+
if (entry === void 0)
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
`Op target ${target.scope}:"${target.state}" has no resolved state entry on this instance \u2014 the deployed definition and the instance state have diverged`
|
|
1542
|
+
);
|
|
1543
|
+
return entry;
|
|
1544
|
+
}
|
|
1545
|
+
function stateHost(ctx, scope) {
|
|
1546
|
+
if (scope === "workflow") return ctx.mutation.state;
|
|
1547
|
+
const stageEntry = findOpenStageEntry(ctx.mutation);
|
|
1548
|
+
if (scope === "stage") return stageEntry?.state;
|
|
1549
|
+
const task = stageEntry?.tasks.find((t) => t.name === ctx.taskName);
|
|
1550
|
+
return task !== void 0 && task.state === void 0 && (task.state = []), task?.state;
|
|
1551
|
+
}
|
|
1552
|
+
function resolveOpSource(src, ctx) {
|
|
1553
|
+
const staticValue = resolveStaticSource(src, ctx);
|
|
1554
|
+
if (staticValue.handled) return staticValue.value;
|
|
1555
|
+
switch (src.type) {
|
|
1556
|
+
case "self":
|
|
1557
|
+
return ctx.self;
|
|
1558
|
+
case "stage":
|
|
1559
|
+
return ctx.stage;
|
|
1560
|
+
case "stateRead": {
|
|
1561
|
+
const value = readEntryFromMutation(ctx, src);
|
|
1562
|
+
return src.path !== void 0 ? getPath(value, src.path) : value;
|
|
1563
|
+
}
|
|
1564
|
+
case "object": {
|
|
1565
|
+
const out = {};
|
|
1566
|
+
for (const [field, fieldSrc] of Object.entries(src.fields))
|
|
1567
|
+
out[field] = resolveOpSource(fieldSrc, ctx);
|
|
1568
|
+
return out;
|
|
1569
|
+
}
|
|
1570
|
+
default:
|
|
1571
|
+
throw new Error(`Source type "${src.type}" is a state-entry origin, not a value`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function entryValue(entries, name) {
|
|
1575
|
+
return entries?.find((s) => s.name === name)?.value;
|
|
1576
|
+
}
|
|
1577
|
+
function readEntryFromMutation(ctx, src) {
|
|
1578
|
+
const scopes = src.scope !== void 0 ? [src.scope] : ["stage", "workflow"], stageEntry = findOpenStageEntry(ctx.mutation);
|
|
1579
|
+
for (const scope of scopes) {
|
|
1580
|
+
const host = scope === "workflow" ? ctx.mutation.state : stageEntry?.state, value = entryValue(host, src.state);
|
|
1581
|
+
if (value !== void 0) return value;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function evalOpPredicate(p, row, ctx) {
|
|
1585
|
+
switch (p.type) {
|
|
1513
1586
|
case "all":
|
|
1514
|
-
return
|
|
1587
|
+
return p.of.every((sub) => evalOpPredicate(sub, row, ctx));
|
|
1515
1588
|
case "any":
|
|
1516
|
-
return
|
|
1517
|
-
case "
|
|
1518
|
-
return
|
|
1589
|
+
return p.of.some((sub) => evalOpPredicate(sub, row, ctx));
|
|
1590
|
+
case "field":
|
|
1591
|
+
return row[p.field] === resolveOpSource(p.equals, ctx);
|
|
1519
1592
|
}
|
|
1520
1593
|
}
|
|
1521
1594
|
async function invokeTask(args) {
|
|
1522
|
-
const { client, instanceId,
|
|
1595
|
+
const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1523
1596
|
...options?.clock ? { clock: options.clock } : {}
|
|
1524
1597
|
});
|
|
1525
|
-
return commitInvoke(ctx,
|
|
1598
|
+
return commitInvoke(ctx, taskName, options?.actor);
|
|
1526
1599
|
}
|
|
1527
|
-
async function commitInvoke(ctx,
|
|
1528
|
-
const { task } = findTaskInCurrentStage(ctx,
|
|
1600
|
+
async function commitInvoke(ctx, taskName, actor) {
|
|
1601
|
+
const { task } = findTaskInCurrentStage(ctx, taskName), entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
|
|
1529
1602
|
if (entry === void 0)
|
|
1530
|
-
throw new Error(`Task "${
|
|
1603
|
+
throw new Error(`Task "${taskName}" has no status entry on instance ${ctx.instance._id}`);
|
|
1531
1604
|
if (entry.status !== "pending")
|
|
1532
|
-
return { invoked: !1,
|
|
1533
|
-
const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation,
|
|
1534
|
-
return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0,
|
|
1535
|
-
}
|
|
1536
|
-
async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
|
|
1537
|
-
const checks = [
|
|
1538
|
-
{ filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
|
|
1539
|
-
{ filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
|
|
1540
|
-
];
|
|
1541
|
-
for (const { filter, outcome, systemId } of checks)
|
|
1542
|
-
if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
|
|
1543
|
-
return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
|
|
1544
|
-
taskStatusChangedEntry({
|
|
1545
|
-
stageId: mutation.currentStageId,
|
|
1546
|
-
taskId: task.id,
|
|
1547
|
-
from: "active",
|
|
1548
|
-
to: outcome,
|
|
1549
|
-
at: now,
|
|
1550
|
-
actor: { kind: "system", id: systemId }
|
|
1551
|
-
})
|
|
1552
|
-
), !0;
|
|
1553
|
-
return !1;
|
|
1605
|
+
return { invoked: !1, task: taskName };
|
|
1606
|
+
const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName);
|
|
1607
|
+
return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, task: taskName };
|
|
1554
1608
|
}
|
|
1555
1609
|
async function activateTask(ctx, mutation, task, entry, actor) {
|
|
1556
1610
|
const now = ctx.now;
|
|
1557
1611
|
if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
|
|
1558
|
-
const stage = findStage(ctx.definition, mutation.
|
|
1559
|
-
entry.state = await
|
|
1612
|
+
const stage = findStage(ctx.definition, mutation.currentStage);
|
|
1613
|
+
entry.state = await resolveTaskStateEntries({
|
|
1560
1614
|
client: ctx.client,
|
|
1561
1615
|
instance: ctx.instance,
|
|
1562
1616
|
stage,
|
|
@@ -1564,55 +1618,73 @@ async function activateTask(ctx, mutation, task, entry, actor) {
|
|
|
1564
1618
|
now
|
|
1565
1619
|
});
|
|
1566
1620
|
}
|
|
1567
|
-
|
|
1621
|
+
runOps({
|
|
1622
|
+
ops: task.ops,
|
|
1623
|
+
mutation,
|
|
1624
|
+
stage: mutation.currentStage,
|
|
1625
|
+
origin: { task: task.name },
|
|
1626
|
+
params: {},
|
|
1627
|
+
actor,
|
|
1628
|
+
self: selfGdr(ctx.instance),
|
|
1629
|
+
now
|
|
1630
|
+
}), mutation.history.push({
|
|
1568
1631
|
_key: randomKey(),
|
|
1569
|
-
_type: "
|
|
1632
|
+
_type: "taskActivated",
|
|
1570
1633
|
at: now,
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
...actor !== void 0 ? { actor } : {}
|
|
1574
|
-
}), await queueEffects(
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
},
|
|
1582
|
-
actor
|
|
1583
|
-
), task.spawns !== void 0)) {
|
|
1584
|
-
const refs = await spawnChildren(ctx, mutation, task, task.spawns, actor);
|
|
1585
|
-
entry.spawnedInstances = refs;
|
|
1634
|
+
stage: mutation.currentStage,
|
|
1635
|
+
task: task.name,
|
|
1636
|
+
...actor !== void 0 ? { actor } : {}
|
|
1637
|
+
}), await queueEffects(ctx, mutation, task.effects, { kind: "task", name: task.name }, actor, {
|
|
1638
|
+
taskName: task.name
|
|
1639
|
+
});
|
|
1640
|
+
let pendingChildren;
|
|
1641
|
+
if (task.subworkflows !== void 0) {
|
|
1642
|
+
const createsBefore = mutation.pendingCreates.length, refs = await spawnSubworkflows(ctx, mutation, task, task.subworkflows, actor);
|
|
1643
|
+
entry.spawnedInstances = refs, pendingChildren = mutation.pendingCreates.slice(createsBefore).map((c) => ({ _id: c._id, stage: c.currentStage, status: "active" }));
|
|
1586
1644
|
for (const ref of refs)
|
|
1587
1645
|
mutation.history.push({
|
|
1588
1646
|
_key: randomKey(),
|
|
1589
|
-
_type: "
|
|
1647
|
+
_type: "spawned",
|
|
1590
1648
|
at: now,
|
|
1591
|
-
|
|
1649
|
+
task: task.name,
|
|
1592
1650
|
instanceRef: ref
|
|
1593
1651
|
});
|
|
1594
|
-
if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
|
|
1595
|
-
const previousStatus = entry.status;
|
|
1596
|
-
entry.status = "done", entry.completedAt = now, mutation.history.push(
|
|
1597
|
-
taskStatusChangedEntry({
|
|
1598
|
-
stageId: mutation.currentStageId,
|
|
1599
|
-
taskId: task.id,
|
|
1600
|
-
from: previousStatus,
|
|
1601
|
-
to: "done",
|
|
1602
|
-
at: now,
|
|
1603
|
-
...actor !== void 0 ? { actor } : {}
|
|
1604
|
-
})
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
1652
|
}
|
|
1653
|
+
await autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) || resolveMachineStep(mutation, task, entry, now);
|
|
1654
|
+
}
|
|
1655
|
+
async function autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) {
|
|
1656
|
+
if (task.failWhen === void 0 && effectiveCompleteWhen(task) === void 0) return !1;
|
|
1657
|
+
const vars = pendingChildren !== void 0 ? { subworkflows: pendingChildren } : await taskConditionVars(ctx, entry), outcome = await evaluateResolutionGate(ctx, task, vars);
|
|
1658
|
+
return outcome === void 0 ? !1 : (applyTaskStatusChange({
|
|
1659
|
+
entry,
|
|
1660
|
+
history: mutation.history,
|
|
1661
|
+
stage: mutation.currentStage,
|
|
1662
|
+
to: outcome,
|
|
1663
|
+
at: now,
|
|
1664
|
+
actor: gateActor(outcome)
|
|
1665
|
+
}), !0);
|
|
1666
|
+
}
|
|
1667
|
+
async function taskConditionVars(ctx, entry) {
|
|
1668
|
+
return entry.spawnedInstances === void 0 || entry.spawnedInstances.length === 0 ? { subworkflows: [] } : { subworkflows: await subworkflowRows(ctx, entry.spawnedInstances) };
|
|
1669
|
+
}
|
|
1670
|
+
function resolveMachineStep(mutation, task, entry, now) {
|
|
1671
|
+
const waitsForActor = task.actions !== void 0 && task.actions.length > 0, waitsForCondition = task.completeWhen !== void 0 || task.subworkflows !== void 0;
|
|
1672
|
+
waitsForActor || waitsForCondition || applyTaskStatusChange({
|
|
1673
|
+
entry,
|
|
1674
|
+
history: mutation.history,
|
|
1675
|
+
stage: mutation.currentStage,
|
|
1676
|
+
to: "done",
|
|
1677
|
+
at: now,
|
|
1678
|
+
actor: { kind: "system", id: "engine.machineStep" }
|
|
1679
|
+
});
|
|
1608
1680
|
}
|
|
1609
1681
|
async function buildStageTasks(ctx, stage) {
|
|
1610
1682
|
const entries = [];
|
|
1611
1683
|
for (const task of stage.tasks ?? []) {
|
|
1612
|
-
const inScope = await
|
|
1684
|
+
const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
|
|
1613
1685
|
entries.push({
|
|
1614
1686
|
_key: randomKey(),
|
|
1615
|
-
|
|
1687
|
+
name: task.name,
|
|
1616
1688
|
status: inScope ? "pending" : "skipped",
|
|
1617
1689
|
...task.filter !== void 0 ? {
|
|
1618
1690
|
filterEvaluation: {
|
|
@@ -1624,22 +1696,22 @@ async function buildStageTasks(ctx, stage) {
|
|
|
1624
1696
|
}
|
|
1625
1697
|
return entries;
|
|
1626
1698
|
}
|
|
1627
|
-
function
|
|
1628
|
-
return
|
|
1699
|
+
function isTerminalStage(stage) {
|
|
1700
|
+
return (stage.transitions ?? []).length === 0;
|
|
1629
1701
|
}
|
|
1630
1702
|
async function setStage(args) {
|
|
1631
|
-
const { client, instanceId,
|
|
1703
|
+
const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1632
1704
|
...options?.clock ? { clock: options.clock } : {}
|
|
1633
1705
|
});
|
|
1634
|
-
return commitSetStage(ctx,
|
|
1706
|
+
return commitSetStage(ctx, targetStage, reason, options?.actor);
|
|
1635
1707
|
}
|
|
1636
1708
|
async function enterStage(ctx, mutation, nextStage, actor, at) {
|
|
1637
|
-
mutation.
|
|
1709
|
+
mutation.currentStage = nextStage.name;
|
|
1638
1710
|
const nextStageEntry = {
|
|
1639
1711
|
_key: randomKey(),
|
|
1640
|
-
|
|
1712
|
+
name: nextStage.name,
|
|
1641
1713
|
enteredAt: at,
|
|
1642
|
-
state: await
|
|
1714
|
+
state: await resolveStageStateEntries({
|
|
1643
1715
|
client: ctx.client,
|
|
1644
1716
|
instance: ctx.instance,
|
|
1645
1717
|
stage: nextStage,
|
|
@@ -1647,55 +1719,44 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
|
|
|
1647
1719
|
}),
|
|
1648
1720
|
tasks: await buildStageTasks(ctx, nextStage)
|
|
1649
1721
|
};
|
|
1650
|
-
mutation.stages.push(nextStageEntry)
|
|
1651
|
-
ctx,
|
|
1652
|
-
mutation,
|
|
1653
|
-
nextStage.onEnter,
|
|
1654
|
-
{ kind: "stageEnter", id: nextStage.id },
|
|
1655
|
-
actor
|
|
1656
|
-
);
|
|
1722
|
+
mutation.stages.push(nextStageEntry);
|
|
1657
1723
|
for (const task of nextStage.tasks ?? []) {
|
|
1658
|
-
if (
|
|
1659
|
-
const entry = nextStageEntry.tasks.find((t) => t.
|
|
1724
|
+
if (task.activation !== "auto") continue;
|
|
1725
|
+
const entry = nextStageEntry.tasks.find((t) => t.name === task.name);
|
|
1660
1726
|
entry && entry.status === "pending" && await activateTask(ctx, mutation, task, entry, actor);
|
|
1661
1727
|
}
|
|
1662
|
-
nextStage
|
|
1728
|
+
isTerminalStage(nextStage) && (mutation.completedAt = at);
|
|
1663
1729
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
return await queueEffects(
|
|
1667
|
-
ctx,
|
|
1668
|
-
mutation,
|
|
1669
|
-
currentStage.onExit,
|
|
1670
|
-
{ kind: "stageExit", id: currentStage.id },
|
|
1671
|
-
actor
|
|
1672
|
-
), { mutation, at };
|
|
1730
|
+
function isTerminal(ctx) {
|
|
1731
|
+
return ctx.instance.completedAt !== void 0;
|
|
1673
1732
|
}
|
|
1674
|
-
async function commitSetStage(ctx,
|
|
1675
|
-
|
|
1676
|
-
if (currentStage.id === nextStage.id)
|
|
1733
|
+
async function commitSetStage(ctx, targetStage, reason, actor) {
|
|
1734
|
+
if (isTerminal(ctx))
|
|
1677
1735
|
return { fired: !1 };
|
|
1678
|
-
const
|
|
1736
|
+
const currentStage = findStage(ctx.definition, ctx.instance.currentStage), nextStage = findStage(ctx.definition, targetStage);
|
|
1737
|
+
if (currentStage.name === nextStage.name)
|
|
1738
|
+
return { fired: !1 };
|
|
1739
|
+
const mutation = startMutation(ctx.instance), at = ctx.now, transitionName = `setStage:${currentStage.name}->${nextStage.name}`;
|
|
1679
1740
|
mutation.history.push(
|
|
1680
1741
|
...stageTransitionHistory({
|
|
1681
|
-
|
|
1682
|
-
|
|
1742
|
+
fromStage: currentStage.name,
|
|
1743
|
+
toStage: nextStage.name,
|
|
1683
1744
|
at,
|
|
1684
|
-
|
|
1745
|
+
transition: transitionName,
|
|
1685
1746
|
via: "setStage",
|
|
1686
1747
|
...actor !== void 0 ? { actor } : {},
|
|
1687
1748
|
...reason !== void 0 ? { reason } : {}
|
|
1688
1749
|
})
|
|
1689
1750
|
);
|
|
1690
1751
|
const priorEntry = findOpenStageEntry(mutation);
|
|
1691
|
-
return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.
|
|
1752
|
+
return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
|
|
1692
1753
|
fired: !0,
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1754
|
+
fromStage: currentStage.name,
|
|
1755
|
+
toStage: nextStage.name,
|
|
1756
|
+
transition: transitionName
|
|
1696
1757
|
};
|
|
1697
1758
|
}
|
|
1698
|
-
async function persistStageMove(ctx, mutation,
|
|
1759
|
+
async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
|
|
1699
1760
|
await persistThenDeploy(
|
|
1700
1761
|
ctx,
|
|
1701
1762
|
mutation,
|
|
@@ -1704,7 +1765,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
|
|
|
1704
1765
|
clientForGdr: ctx.clientForGdr,
|
|
1705
1766
|
instance: materializeInstance(ctx.instance, mutation),
|
|
1706
1767
|
definition: ctx.definition,
|
|
1707
|
-
|
|
1768
|
+
stageName: enteredStage,
|
|
1708
1769
|
now: ctx.now
|
|
1709
1770
|
})
|
|
1710
1771
|
), await retractStageGuards({
|
|
@@ -1712,7 +1773,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
|
|
|
1712
1773
|
clientForGdr: ctx.clientForGdr,
|
|
1713
1774
|
instance: ctx.instance,
|
|
1714
1775
|
definition: ctx.definition,
|
|
1715
|
-
|
|
1776
|
+
stageName: exitedStage,
|
|
1716
1777
|
now: ctx.now
|
|
1717
1778
|
});
|
|
1718
1779
|
}
|
|
@@ -1728,53 +1789,63 @@ async function persistThenDeploy(ctx, mutation, deploy) {
|
|
|
1728
1789
|
deploy
|
|
1729
1790
|
});
|
|
1730
1791
|
}
|
|
1731
|
-
async function refreshStageGuards(ctx, mutation,
|
|
1792
|
+
async function refreshStageGuards(ctx, mutation, stageName) {
|
|
1732
1793
|
await deployStageGuards({
|
|
1733
1794
|
client: ctx.client,
|
|
1734
1795
|
clientForGdr: ctx.clientForGdr,
|
|
1735
1796
|
instance: materializeInstance(ctx.instance, mutation),
|
|
1736
1797
|
definition: ctx.definition,
|
|
1737
|
-
|
|
1798
|
+
stageName,
|
|
1738
1799
|
now: ctx.now
|
|
1739
1800
|
});
|
|
1740
1801
|
}
|
|
1741
|
-
async function commitTransition(ctx,
|
|
1742
|
-
|
|
1802
|
+
async function commitTransition(ctx, actor) {
|
|
1803
|
+
if (isTerminal(ctx))
|
|
1804
|
+
return { fired: !1 };
|
|
1805
|
+
const currentStage = findStage(ctx.definition, ctx.instance.currentStage), transition = await pickTransition(ctx, currentStage);
|
|
1743
1806
|
if (transition === void 0)
|
|
1744
1807
|
return { fired: !1 };
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1808
|
+
const mutation = startMutation(ctx.instance), at = ctx.now;
|
|
1809
|
+
runOps({
|
|
1810
|
+
ops: transition.ops,
|
|
1811
|
+
mutation,
|
|
1812
|
+
stage: currentStage.name,
|
|
1813
|
+
origin: { transition: transition.name },
|
|
1814
|
+
params: {},
|
|
1815
|
+
actor,
|
|
1816
|
+
self: selfGdr(ctx.instance),
|
|
1817
|
+
now: at
|
|
1818
|
+
}), await queueEffects(
|
|
1747
1819
|
ctx,
|
|
1748
1820
|
mutation,
|
|
1749
1821
|
transition.effects,
|
|
1750
1822
|
{
|
|
1751
1823
|
kind: "transition",
|
|
1752
|
-
|
|
1824
|
+
name: transition.name
|
|
1753
1825
|
},
|
|
1754
1826
|
actor
|
|
1755
1827
|
), mutation.history.push(
|
|
1756
1828
|
...stageTransitionHistory({
|
|
1757
|
-
|
|
1758
|
-
|
|
1829
|
+
fromStage: currentStage.name,
|
|
1830
|
+
toStage: transition.to,
|
|
1759
1831
|
at,
|
|
1760
|
-
|
|
1832
|
+
transition: transition.name,
|
|
1761
1833
|
...actor !== void 0 ? { actor } : {}
|
|
1762
1834
|
})
|
|
1763
1835
|
);
|
|
1764
1836
|
const priorEntry = findOpenStageEntry(mutation);
|
|
1765
1837
|
priorEntry !== void 0 && (priorEntry.exitedAt = at);
|
|
1766
1838
|
const nextStage = findStage(ctx.definition, transition.to);
|
|
1767
|
-
return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.
|
|
1839
|
+
return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
|
|
1768
1840
|
fired: !0,
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1841
|
+
fromStage: currentStage.name,
|
|
1842
|
+
toStage: transition.to,
|
|
1843
|
+
transition: transition.name
|
|
1772
1844
|
};
|
|
1773
1845
|
}
|
|
1774
|
-
async function pickTransition(ctx, stage
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
|
|
1846
|
+
async function pickTransition(ctx, stage) {
|
|
1847
|
+
for (const candidate of stage.transitions ?? [])
|
|
1848
|
+
if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
|
|
1778
1849
|
}
|
|
1779
1850
|
async function evaluateAutoTransitions(args) {
|
|
1780
1851
|
const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
|
|
@@ -1782,58 +1853,27 @@ async function evaluateAutoTransitions(args) {
|
|
|
1782
1853
|
...clock ? { clock } : {},
|
|
1783
1854
|
...overlay ? { overlay } : {}
|
|
1784
1855
|
});
|
|
1785
|
-
return commitTransition(ctx,
|
|
1856
|
+
return commitTransition(ctx, options?.actor);
|
|
1786
1857
|
}
|
|
1787
1858
|
async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
|
|
1788
1859
|
const instance = await client.getDocument(instanceId);
|
|
1789
|
-
if (!instance || instance.stages.length > 0
|
|
1790
|
-
const definition =
|
|
1860
|
+
if (!instance || instance.stages.length > 0) return;
|
|
1861
|
+
const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.name === instance.currentStage);
|
|
1791
1862
|
if (stage === void 0) return;
|
|
1792
|
-
const
|
|
1863
|
+
const ctx = await buildEngineContext({
|
|
1793
1864
|
client,
|
|
1794
1865
|
clientForGdr: clientForGdr ?? (() => client),
|
|
1795
|
-
instance
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
filter: task.filter,
|
|
1800
|
-
definition,
|
|
1801
|
-
snapshot,
|
|
1802
|
-
params: buildParams(instance, now)
|
|
1803
|
-
});
|
|
1804
|
-
tasks.push({
|
|
1805
|
-
_key: randomKey(),
|
|
1806
|
-
id: task.id,
|
|
1807
|
-
status: inScope ? "pending" : "skipped"
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
const initialStageEntry = {
|
|
1866
|
+
instance,
|
|
1867
|
+
definition,
|
|
1868
|
+
...clock ? { clock } : {}
|
|
1869
|
+
}), now = ctx.now, initialStageEntry = {
|
|
1811
1870
|
_key: randomKey(),
|
|
1812
|
-
|
|
1871
|
+
name: stage.name,
|
|
1813
1872
|
enteredAt: now,
|
|
1814
|
-
state: await
|
|
1815
|
-
tasks
|
|
1816
|
-
},
|
|
1817
|
-
for (const effect of stage.onEnter ?? []) {
|
|
1818
|
-
const params = await resolveBindings({
|
|
1819
|
-
bindings: effect.bindings,
|
|
1820
|
-
staticInput: effect.input,
|
|
1821
|
-
instance,
|
|
1822
|
-
now,
|
|
1823
|
-
...actor !== void 0 ? { actor } : {}
|
|
1824
|
-
}), { pending, history } = buildQueuedEffect(
|
|
1825
|
-
effect,
|
|
1826
|
-
{ kind: "stageEnter", id: stage.id },
|
|
1827
|
-
params,
|
|
1828
|
-
actor,
|
|
1829
|
-
now
|
|
1830
|
-
);
|
|
1831
|
-
pendingEffects.push(pending), newHistory.push(history);
|
|
1832
|
-
}
|
|
1833
|
-
const committed = await client.patch(instance._id).set({
|
|
1873
|
+
state: await resolveStageStateEntries({ client, instance, stage, now }),
|
|
1874
|
+
tasks: await buildStageTasks(ctx, stage)
|
|
1875
|
+
}, committed = await client.patch(instance._id).set({
|
|
1834
1876
|
stages: [initialStageEntry],
|
|
1835
|
-
pendingEffects,
|
|
1836
|
-
history: newHistory,
|
|
1837
1877
|
lastChangedAt: now
|
|
1838
1878
|
}).ifRevisionId(instance._rev).commit();
|
|
1839
1879
|
await deployOrRollback({
|
|
@@ -1842,7 +1882,6 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
1842
1882
|
committedRev: committed._rev,
|
|
1843
1883
|
restore: {
|
|
1844
1884
|
stages: instance.stages,
|
|
1845
|
-
pendingEffects: instance.pendingEffects,
|
|
1846
1885
|
history: instance.history
|
|
1847
1886
|
},
|
|
1848
1887
|
reversible: !0,
|
|
@@ -1851,7 +1890,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
1851
1890
|
clientForGdr: clientForGdr ?? (() => client),
|
|
1852
1891
|
instance,
|
|
1853
1892
|
definition,
|
|
1854
|
-
|
|
1893
|
+
stageName: stage.name,
|
|
1855
1894
|
now
|
|
1856
1895
|
})
|
|
1857
1896
|
}), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
|
|
@@ -1859,7 +1898,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
1859
1898
|
async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
|
|
1860
1899
|
const resolvedClientForGdr = clientForGdr ?? (() => client);
|
|
1861
1900
|
for (const task of stage.tasks ?? []) {
|
|
1862
|
-
if (
|
|
1901
|
+
if (task.activation !== "auto") continue;
|
|
1863
1902
|
const updated = await client.getDocument(instanceId);
|
|
1864
1903
|
if (!updated) continue;
|
|
1865
1904
|
const updatedCtx = await buildEngineContext({
|
|
@@ -1868,21 +1907,19 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
|
|
|
1868
1907
|
instance: updated,
|
|
1869
1908
|
definition,
|
|
1870
1909
|
...clock ? { clock } : {}
|
|
1871
|
-
}), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.
|
|
1910
|
+
}), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
|
|
1872
1911
|
mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
|
|
1873
1912
|
}
|
|
1874
1913
|
}
|
|
1875
1914
|
const CASCADE_LIMIT = 100;
|
|
1876
1915
|
async function gatherAutoResolveCandidates(ctx, tasks) {
|
|
1877
1916
|
const currentTasksList = findCurrentTasks(ctx.instance), candidates = [];
|
|
1878
|
-
for (const task of tasks)
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
task.completeWhen !== void 0 && await ctxEvaluateFilter(ctx, task.completeWhen) && candidates.push({ task, outcome: "done" });
|
|
1885
|
-
}
|
|
1917
|
+
for (const task of tasks) {
|
|
1918
|
+
const entry = currentTasksList.find((e) => e.name === task.name);
|
|
1919
|
+
if (entry?.status !== "active") continue;
|
|
1920
|
+
const outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
1921
|
+
outcome !== void 0 && candidates.push({ task, outcome });
|
|
1922
|
+
}
|
|
1886
1923
|
return candidates;
|
|
1887
1924
|
}
|
|
1888
1925
|
async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
|
|
@@ -1890,31 +1927,24 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, over
|
|
|
1890
1927
|
...clientForGdr ? { clientForGdr } : {},
|
|
1891
1928
|
...clock ? { clock } : {},
|
|
1892
1929
|
...overlay ? { overlay } : {}
|
|
1893
|
-
}), stage = findStage(ctx.definition, ctx.instance.
|
|
1894
|
-
(t) => t
|
|
1930
|
+
}), stage = findStage(ctx.definition, ctx.instance.currentStage), gatedTasks = (stage.tasks ?? []).filter(
|
|
1931
|
+
(t) => effectiveCompleteWhen(t) !== void 0 || t.failWhen !== void 0
|
|
1895
1932
|
);
|
|
1896
|
-
if (
|
|
1897
|
-
const candidates = await gatherAutoResolveCandidates(ctx,
|
|
1933
|
+
if (gatedTasks.length === 0) return 0;
|
|
1934
|
+
const candidates = await gatherAutoResolveCandidates(ctx, gatedTasks);
|
|
1898
1935
|
if (candidates.length === 0) return 0;
|
|
1899
1936
|
const mutation = startMutation(ctx.instance);
|
|
1900
1937
|
let count = 0;
|
|
1901
1938
|
for (const { task, outcome } of candidates) {
|
|
1902
|
-
const mutEntry = currentTasks(mutation).find((t) => t.
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
taskId: task.id,
|
|
1912
|
-
from: previousStatus,
|
|
1913
|
-
to: outcome,
|
|
1914
|
-
at: now,
|
|
1915
|
-
actor
|
|
1916
|
-
})
|
|
1917
|
-
), count++;
|
|
1939
|
+
const mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
|
|
1940
|
+
mutEntry === void 0 || mutEntry.status !== "active" || (applyTaskStatusChange({
|
|
1941
|
+
entry: mutEntry,
|
|
1942
|
+
history: mutation.history,
|
|
1943
|
+
stage: stage.name,
|
|
1944
|
+
to: outcome,
|
|
1945
|
+
at: ctx.now,
|
|
1946
|
+
actor: gateActor(outcome)
|
|
1947
|
+
}), count++);
|
|
1918
1948
|
}
|
|
1919
1949
|
return count > 0 && await persist(ctx, mutation), count;
|
|
1920
1950
|
}
|
|
@@ -1933,43 +1963,276 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
|
|
|
1933
1963
|
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
1934
1964
|
}
|
|
1935
1965
|
}
|
|
1936
|
-
async function
|
|
1966
|
+
async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
|
|
1937
1967
|
const parentRef = child.ancestors.at(-1);
|
|
1938
1968
|
if (parentRef === void 0) return;
|
|
1939
1969
|
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
1940
1970
|
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
1941
|
-
const definition =
|
|
1971
|
+
const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
|
|
1942
1972
|
if (stage === void 0) return;
|
|
1943
|
-
const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.
|
|
1944
|
-
if (task?.
|
|
1945
|
-
const entry = parentCurrentTasks.find((e) => e.
|
|
1946
|
-
if (
|
|
1947
|
-
|
|
1973
|
+
const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
|
|
1974
|
+
if (task?.subworkflows === void 0) return;
|
|
1975
|
+
const entry = parentCurrentTasks.find((e) => e.name === task.name);
|
|
1976
|
+
if (entry === void 0 || entry.status !== "active") return;
|
|
1977
|
+
const ctx = await buildEngineContext({
|
|
1978
|
+
client,
|
|
1979
|
+
clientForGdr,
|
|
1980
|
+
instance: parent,
|
|
1981
|
+
definition,
|
|
1982
|
+
...clock ? { clock } : {}
|
|
1983
|
+
}), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
|
|
1984
|
+
if (outcome !== void 0)
|
|
1985
|
+
return { parent, definition, stage, task, outcome };
|
|
1948
1986
|
}
|
|
1949
1987
|
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
1950
1988
|
const instance = await client.getDocument(instanceId);
|
|
1951
1989
|
if (!instance) return;
|
|
1952
|
-
const found = await
|
|
1990
|
+
const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
|
|
1953
1991
|
if (found === void 0) return;
|
|
1954
|
-
const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
|
|
1992
|
+
const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
|
|
1955
1993
|
client,
|
|
1956
|
-
clientForGdr:
|
|
1994
|
+
clientForGdr: resolvedClientForGdr,
|
|
1957
1995
|
instance: parent,
|
|
1958
1996
|
definition,
|
|
1959
1997
|
...clock ? { clock } : {}
|
|
1960
|
-
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1998
|
+
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
|
|
1999
|
+
mutEntry !== void 0 && (applyTaskStatusChange({
|
|
2000
|
+
entry: mutEntry,
|
|
2001
|
+
history: mutation.history,
|
|
2002
|
+
stage: stage.name,
|
|
2003
|
+
to: outcome,
|
|
2004
|
+
at: ctx.now,
|
|
2005
|
+
actor: gateActor(outcome)
|
|
2006
|
+
}), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
|
|
2007
|
+
}
|
|
2008
|
+
function startMutation(instance) {
|
|
2009
|
+
return {
|
|
2010
|
+
currentStage: instance.currentStage,
|
|
2011
|
+
// Deep-ish copy. Per-scope state entry arrays need their own copies
|
|
2012
|
+
// so ops can mutate without leaking into the source.
|
|
2013
|
+
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
2014
|
+
stages: instance.stages.map((s) => ({
|
|
2015
|
+
...s,
|
|
2016
|
+
state: (s.state ?? []).map((entry) => ({ ...entry })),
|
|
2017
|
+
tasks: s.tasks.map((t) => ({
|
|
2018
|
+
...t,
|
|
2019
|
+
...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
|
|
2020
|
+
}))
|
|
2021
|
+
})),
|
|
2022
|
+
pendingEffects: [...instance.pendingEffects],
|
|
2023
|
+
effectHistory: [...instance.effectHistory],
|
|
2024
|
+
effectsContext: [...instance.effectsContext],
|
|
2025
|
+
history: [...instance.history],
|
|
2026
|
+
lastChangedAt: instance.lastChangedAt,
|
|
2027
|
+
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
2028
|
+
...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
|
|
2029
|
+
pendingCreates: []
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function instanceStateFields(src) {
|
|
2033
|
+
const fields = {
|
|
2034
|
+
currentStage: src.currentStage,
|
|
2035
|
+
state: src.state,
|
|
2036
|
+
stages: src.stages,
|
|
2037
|
+
pendingEffects: src.pendingEffects,
|
|
2038
|
+
effectHistory: src.effectHistory,
|
|
2039
|
+
effectsContext: src.effectsContext,
|
|
2040
|
+
history: src.history
|
|
2041
|
+
};
|
|
2042
|
+
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
|
|
2043
|
+
}
|
|
2044
|
+
function materializeInstance(base, mutation) {
|
|
2045
|
+
return {
|
|
2046
|
+
...base,
|
|
2047
|
+
currentStage: mutation.currentStage,
|
|
2048
|
+
state: mutation.state,
|
|
2049
|
+
stages: mutation.stages
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
async function persist(ctx, mutation) {
|
|
2053
|
+
const set = {
|
|
2054
|
+
...instanceStateFields(mutation),
|
|
2055
|
+
lastChangedAt: ctx.now
|
|
2056
|
+
}, pendingCreates = mutation.pendingCreates;
|
|
2057
|
+
if (pendingCreates.length === 0)
|
|
2058
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
2059
|
+
const tx = ctx.client.transaction();
|
|
2060
|
+
for (const body of pendingCreates) tx.create(body);
|
|
2061
|
+
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
2062
|
+
const actorForPriming = void 0;
|
|
2063
|
+
for (const body of pendingCreates)
|
|
2064
|
+
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
2065
|
+
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
2066
|
+
if (!reloaded)
|
|
2067
|
+
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
2068
|
+
return reloaded;
|
|
2069
|
+
}
|
|
2070
|
+
function currentStageEntry(mutation) {
|
|
2071
|
+
const entry = findOpenStageEntry(mutation);
|
|
2072
|
+
if (entry === void 0)
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
|
|
2075
|
+
);
|
|
2076
|
+
return entry;
|
|
2077
|
+
}
|
|
2078
|
+
function currentTasks(mutation) {
|
|
2079
|
+
return currentStageEntry(mutation).tasks;
|
|
2080
|
+
}
|
|
2081
|
+
function findTaskInCurrentStage(ctx, taskName) {
|
|
2082
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
|
|
2083
|
+
if (task === void 0)
|
|
2084
|
+
throw new Error(
|
|
2085
|
+
`Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
2086
|
+
);
|
|
2087
|
+
return { stage, task };
|
|
2088
|
+
}
|
|
2089
|
+
function requireMutationTaskEntry(mutation, task) {
|
|
2090
|
+
const mutEntry = currentTasks(mutation).find((t) => t.name === task);
|
|
2091
|
+
if (mutEntry === void 0)
|
|
2092
|
+
throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
|
|
2093
|
+
return mutEntry;
|
|
2094
|
+
}
|
|
2095
|
+
function findCurrentStageEntry(instance) {
|
|
2096
|
+
return findOpenStageEntry(instance);
|
|
2097
|
+
}
|
|
2098
|
+
function findCurrentTasks(instance) {
|
|
2099
|
+
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
2100
|
+
}
|
|
2101
|
+
async function completeEffect(args) {
|
|
2102
|
+
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2103
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
2104
|
+
});
|
|
2105
|
+
return commitCompleteEffect(
|
|
2106
|
+
ctx,
|
|
2107
|
+
effectKey,
|
|
2108
|
+
status,
|
|
2109
|
+
outputs,
|
|
2110
|
+
detail,
|
|
2111
|
+
error,
|
|
2112
|
+
durationMs,
|
|
2113
|
+
options?.actor
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
function buildEffectHistoryEntry(pending, outcome) {
|
|
2117
|
+
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
2118
|
+
return {
|
|
2119
|
+
_key: pending._key,
|
|
2120
|
+
name: pending.name,
|
|
2121
|
+
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
2122
|
+
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
2123
|
+
params: pending.params,
|
|
2124
|
+
origin: pending.origin,
|
|
2125
|
+
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
2126
|
+
ranAt,
|
|
2127
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
2128
|
+
status,
|
|
2129
|
+
...detail !== void 0 ? { detail } : {},
|
|
2130
|
+
...error !== void 0 ? { error } : {},
|
|
2131
|
+
...outputs !== void 0 ? { outputs } : {}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function upsertEffectsContext(mutation, effectName, outputs) {
|
|
2135
|
+
const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
|
|
2136
|
+
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
2137
|
+
}
|
|
2138
|
+
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
2139
|
+
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
2140
|
+
if (pending === void 0)
|
|
2141
|
+
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
2142
|
+
const mutation = startMutation(ctx.instance);
|
|
2143
|
+
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
2144
|
+
const ranAt = ctx.now;
|
|
2145
|
+
return mutation.effectHistory.push(
|
|
2146
|
+
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
2147
|
+
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
|
|
2148
|
+
_key: randomKey(),
|
|
2149
|
+
_type: "effectCompleted",
|
|
2150
|
+
at: ranAt,
|
|
2151
|
+
effectKey,
|
|
2152
|
+
effect: pending.name,
|
|
2153
|
+
status,
|
|
2154
|
+
...outputs !== void 0 ? { outputs } : {},
|
|
2155
|
+
...detail !== void 0 ? { detail } : {},
|
|
2156
|
+
...actor !== void 0 ? { actor } : {}
|
|
2157
|
+
}), await persist(ctx, mutation), { effectKey, status };
|
|
2158
|
+
}
|
|
2159
|
+
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
2160
|
+
const key = randomKey(), pending = {
|
|
2161
|
+
_key: key,
|
|
2162
|
+
_type: "pendingEffect",
|
|
2163
|
+
name: effect.name,
|
|
2164
|
+
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
2165
|
+
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
2166
|
+
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
2167
|
+
params,
|
|
2168
|
+
origin,
|
|
2169
|
+
...actor !== void 0 ? { actor } : {},
|
|
2170
|
+
queuedAt: now
|
|
2171
|
+
}, history = {
|
|
2172
|
+
_key: randomKey(),
|
|
2173
|
+
_type: "effectQueued",
|
|
2174
|
+
at: now,
|
|
2175
|
+
effectKey: key,
|
|
2176
|
+
effect: effect.name,
|
|
2177
|
+
origin
|
|
2178
|
+
};
|
|
2179
|
+
return { pending, history };
|
|
2180
|
+
}
|
|
2181
|
+
async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
|
|
2182
|
+
if (!effects || effects.length === 0) return;
|
|
2183
|
+
const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
|
|
2184
|
+
...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
|
|
2185
|
+
...actor !== void 0 ? { actor } : {},
|
|
2186
|
+
// Caller-supplied action params, readable in bindings as `$params.<name>`.
|
|
2187
|
+
vars: { params: opts?.callerParams ?? {} }
|
|
2188
|
+
});
|
|
2189
|
+
for (const effect of effects) {
|
|
2190
|
+
const resolved = await resolveBindings({
|
|
2191
|
+
bindings: effect.bindings,
|
|
2192
|
+
staticInput: effect.input,
|
|
2193
|
+
snapshot: ctx.snapshot,
|
|
2194
|
+
params
|
|
2195
|
+
}), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
|
|
2196
|
+
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
async function abortInstance(args) {
|
|
2200
|
+
const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
2201
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
2202
|
+
});
|
|
2203
|
+
return commitAbort(ctx, reason, options?.actor);
|
|
2204
|
+
}
|
|
2205
|
+
async function commitAbort(ctx, reason, actor) {
|
|
2206
|
+
if (ctx.instance.completedAt !== void 0)
|
|
2207
|
+
return { fired: !1 };
|
|
2208
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
|
|
2209
|
+
return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
|
|
2210
|
+
...mutation.pendingEffects.map(
|
|
2211
|
+
(pending) => buildEffectHistoryEntry(pending, {
|
|
2212
|
+
status: "failed",
|
|
2213
|
+
ranAt: at,
|
|
2214
|
+
actor,
|
|
2215
|
+
detail: "instance aborted before dispatch",
|
|
2216
|
+
error: void 0,
|
|
2217
|
+
durationMs: void 0,
|
|
2218
|
+
outputs: void 0
|
|
2219
|
+
})
|
|
2220
|
+
)
|
|
2221
|
+
), mutation.pendingEffects = [], mutation.history.push({
|
|
2222
|
+
_key: randomKey(),
|
|
2223
|
+
_type: "aborted",
|
|
2224
|
+
at,
|
|
2225
|
+
stage: stage.name,
|
|
2226
|
+
...reason !== void 0 ? { reason } : {},
|
|
2227
|
+
...actor !== void 0 ? { actor } : {}
|
|
2228
|
+
}), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
|
|
2229
|
+
client: ctx.client,
|
|
2230
|
+
clientForGdr: ctx.clientForGdr,
|
|
2231
|
+
instance: ctx.instance,
|
|
2232
|
+
definition: ctx.definition,
|
|
2233
|
+
stageName: stage.name,
|
|
2234
|
+
now: ctx.now
|
|
2235
|
+
}), { fired: !0, stage: stage.name };
|
|
1973
2236
|
}
|
|
1974
2237
|
const parsedFilters = /* @__PURE__ */ new Map();
|
|
1975
2238
|
async function matchesFilter(args) {
|
|
@@ -2055,17 +2318,103 @@ async function fetchActor(requestFn) {
|
|
|
2055
2318
|
...roleNames.length > 0 ? { roles: roleNames } : {}
|
|
2056
2319
|
};
|
|
2057
2320
|
}
|
|
2058
|
-
async function fetchGrantsCached(requestFn, resourcePath) {
|
|
2321
|
+
async function fetchGrantsCached(requestFn, resourcePath) {
|
|
2322
|
+
try {
|
|
2323
|
+
return await fetchGrants({ client: { request: requestFn }, resourcePath });
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
console.warn(
|
|
2326
|
+
`workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
2327
|
+
);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
2332
|
+
function guardsForResource(client) {
|
|
2333
|
+
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
2334
|
+
}
|
|
2335
|
+
function instanceGuardQuery(instanceId) {
|
|
2336
|
+
return {
|
|
2337
|
+
query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
|
|
2338
|
+
params: { guardType: GUARD_DOC_TYPE, instanceId }
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function verdictGuardsForInstance(client, instanceId) {
|
|
2342
|
+
const { query, params } = instanceGuardQuery(instanceId);
|
|
2343
|
+
return client.fetch(query, params);
|
|
2344
|
+
}
|
|
2345
|
+
async function guardsForInstance(args) {
|
|
2346
|
+
const { client, clientForGdr, instance } = args, stateUris = [
|
|
2347
|
+
...collectEntryDocUris(instance.state),
|
|
2348
|
+
...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
|
|
2349
|
+
], clients = resourceClientMap(
|
|
2350
|
+
instance.workflowResource,
|
|
2351
|
+
client,
|
|
2352
|
+
clientForGdr,
|
|
2353
|
+
stateUris.map((uri) => tryParseGdr(uri))
|
|
2354
|
+
), { query, params } = instanceGuardQuery(instance._id);
|
|
2355
|
+
return queryGuardsAcross(clients.values(), query, params);
|
|
2356
|
+
}
|
|
2357
|
+
async function guardsForDefinition(args) {
|
|
2358
|
+
const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
|
|
2359
|
+
(def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
|
|
2360
|
+
), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs);
|
|
2361
|
+
return queryGuardsAcross(clients.values(), "*[_type == $t && sourceDefinition == $definition]", {
|
|
2362
|
+
t: GUARD_DOC_TYPE,
|
|
2363
|
+
definition
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
function guardIdRefs(definition) {
|
|
2367
|
+
return definition.stages.flatMap(
|
|
2368
|
+
(stage) => (stage.guards ?? []).flatMap(
|
|
2369
|
+
(guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
|
|
2370
|
+
)
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
function staticIdRefGdr(idRef, stage, definition) {
|
|
2374
|
+
const read = /^\$state\.([\w-]+)/.exec(idRef);
|
|
2375
|
+
if (read === null) return;
|
|
2376
|
+
const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
|
|
2377
|
+
return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
function entriesInScope(stage, definition) {
|
|
2380
|
+
return [
|
|
2381
|
+
...definition.state ?? [],
|
|
2382
|
+
...stage.state ?? [],
|
|
2383
|
+
...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
|
|
2384
|
+
];
|
|
2385
|
+
}
|
|
2386
|
+
function gdrFromValue(value) {
|
|
2387
|
+
if (typeof value == "string") return tryParseGdr(value);
|
|
2388
|
+
if (typeof value == "object" && value !== null && "id" in value) {
|
|
2389
|
+
const id = value.id;
|
|
2390
|
+
return typeof id == "string" ? tryParseGdr(id) : void 0;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
function tryParseGdr(uri) {
|
|
2059
2394
|
try {
|
|
2060
|
-
return
|
|
2061
|
-
} catch
|
|
2062
|
-
console.warn(
|
|
2063
|
-
`workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
2064
|
-
);
|
|
2395
|
+
return parseGdr(uri);
|
|
2396
|
+
} catch {
|
|
2065
2397
|
return;
|
|
2066
2398
|
}
|
|
2067
2399
|
}
|
|
2068
|
-
|
|
2400
|
+
function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
|
|
2401
|
+
const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
|
|
2402
|
+
for (const parsed of gdrs) {
|
|
2403
|
+
if (parsed === void 0) continue;
|
|
2404
|
+
const key = resourceKey(resourceFromParsed(parsed));
|
|
2405
|
+
map.has(key) || map.set(key, clientForGdr(parsed));
|
|
2406
|
+
}
|
|
2407
|
+
return map;
|
|
2408
|
+
}
|
|
2409
|
+
async function queryGuardsAcross(clients, query, params) {
|
|
2410
|
+
const perResource = await Promise.all(
|
|
2411
|
+
[...clients].map((c) => c.fetch(query, params))
|
|
2412
|
+
);
|
|
2413
|
+
return dedupById(perResource.flat());
|
|
2414
|
+
}
|
|
2415
|
+
function dedupById(guards) {
|
|
2416
|
+
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
2417
|
+
}
|
|
2069
2418
|
async function evaluateInstance(args) {
|
|
2070
2419
|
const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2071
2420
|
validateTags(tags);
|
|
@@ -2077,45 +2426,45 @@ async function evaluateInstance(args) {
|
|
|
2077
2426
|
throw new Error(`Workflow instance "${instanceId}" not found`);
|
|
2078
2427
|
if (!tags.some((t) => instance.tags?.includes(t)))
|
|
2079
2428
|
throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
|
|
2080
|
-
const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance });
|
|
2429
|
+
const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
|
|
2081
2430
|
return evaluateFromSnapshot({
|
|
2082
2431
|
instance,
|
|
2083
2432
|
definition,
|
|
2084
2433
|
actor,
|
|
2085
2434
|
snapshot,
|
|
2435
|
+
guards,
|
|
2086
2436
|
now,
|
|
2087
2437
|
...grants !== void 0 ? { grants } : {}
|
|
2088
2438
|
});
|
|
2089
2439
|
}
|
|
2090
2440
|
async function evaluateFromSnapshot(args) {
|
|
2091
|
-
const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.
|
|
2441
|
+
const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.name === instance.currentStage);
|
|
2092
2442
|
if (stage === void 0)
|
|
2093
2443
|
throw new Error(
|
|
2094
|
-
`Instance "${instance._id}"
|
|
2444
|
+
`Instance "${instance._id}" currentStage "${instance.currentStage}" not in definition`
|
|
2095
2445
|
);
|
|
2096
|
-
const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
|
|
2446
|
+
const scope = await renderScope({ instance, definition, actor, grants, snapshot, now }), currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], guardDenial = await instanceGuardReason(instance, actor, args.guards), taskEvaluations = [];
|
|
2097
2447
|
for (const task of stage.tasks ?? [])
|
|
2098
2448
|
taskEvaluations.push(
|
|
2099
2449
|
await evaluateTask({
|
|
2100
2450
|
task,
|
|
2101
|
-
statusEntry: currentTaskEntries.find((t) => t.
|
|
2451
|
+
statusEntry: currentTaskEntries.find((t) => t.name === task.name),
|
|
2102
2452
|
instance,
|
|
2103
|
-
definition,
|
|
2104
2453
|
actor,
|
|
2105
|
-
grants,
|
|
2106
2454
|
snapshot,
|
|
2107
|
-
|
|
2455
|
+
scope,
|
|
2456
|
+
stageHasExits: !isTerminalStage(stage),
|
|
2457
|
+
guardDenial
|
|
2108
2458
|
})
|
|
2109
2459
|
);
|
|
2110
2460
|
const transitionEvaluations = [];
|
|
2111
2461
|
for (const transition of stage.transitions ?? [])
|
|
2112
2462
|
transitionEvaluations.push({
|
|
2113
2463
|
transition,
|
|
2114
|
-
filterSatisfied: await
|
|
2115
|
-
|
|
2116
|
-
definition,
|
|
2464
|
+
filterSatisfied: await evaluateCondition({
|
|
2465
|
+
condition: transition.filter,
|
|
2117
2466
|
snapshot,
|
|
2118
|
-
params:
|
|
2467
|
+
params: scope
|
|
2119
2468
|
})
|
|
2120
2469
|
});
|
|
2121
2470
|
const currentStage = {
|
|
@@ -2132,142 +2481,102 @@ async function evaluateFromSnapshot(args) {
|
|
|
2132
2481
|
canInteract
|
|
2133
2482
|
};
|
|
2134
2483
|
}
|
|
2484
|
+
async function renderScope(args) {
|
|
2485
|
+
const { instance, definition, actor, grants, snapshot, now } = args, params = {
|
|
2486
|
+
...buildParams({ instance, now, snapshot }),
|
|
2487
|
+
actor,
|
|
2488
|
+
assigned: !1,
|
|
2489
|
+
can: await advisoryCan(instance, actor, grants)
|
|
2490
|
+
};
|
|
2491
|
+
return { ...await evaluatePredicates({
|
|
2492
|
+
predicates: definition.predicates,
|
|
2493
|
+
snapshot,
|
|
2494
|
+
params
|
|
2495
|
+
}), ...params };
|
|
2496
|
+
}
|
|
2497
|
+
async function advisoryCan(instance, actor, grants) {
|
|
2498
|
+
if (grants === void 0) return;
|
|
2499
|
+
const can = {};
|
|
2500
|
+
for (const permission of DOCUMENT_VALUE_PERMISSIONS)
|
|
2501
|
+
can[permission] = await grantsPermissionOn({
|
|
2502
|
+
document: instance,
|
|
2503
|
+
grants,
|
|
2504
|
+
permission,
|
|
2505
|
+
userId: actor.id
|
|
2506
|
+
});
|
|
2507
|
+
return can;
|
|
2508
|
+
}
|
|
2135
2509
|
async function evaluateTask(args) {
|
|
2136
|
-
const { task, statusEntry, instance,
|
|
2510
|
+
const { task, statusEntry, instance, actor, snapshot, scope, stageHasExits, guardDenial } = args, status = statusEntry?.status ?? "pending", assigned = assignedFor(instance, task.name, actor), taskScope = {
|
|
2511
|
+
...scope,
|
|
2512
|
+
state: {
|
|
2513
|
+
...scope.state,
|
|
2514
|
+
...scopedStateOverlay(instance, snapshot, task.name)
|
|
2515
|
+
},
|
|
2516
|
+
assigned
|
|
2517
|
+
}, actions = [];
|
|
2137
2518
|
for (const action of task.actions ?? [])
|
|
2138
2519
|
actions.push(
|
|
2139
2520
|
await evaluateAction({
|
|
2140
2521
|
action,
|
|
2141
|
-
task,
|
|
2142
|
-
...statusEntry !== void 0 ? { statusEntry } : {},
|
|
2143
2522
|
status,
|
|
2144
2523
|
instance,
|
|
2145
|
-
definition,
|
|
2146
|
-
actor,
|
|
2147
|
-
...grants !== void 0 ? { grants } : {},
|
|
2148
2524
|
snapshot,
|
|
2149
|
-
|
|
2525
|
+
taskScope,
|
|
2526
|
+
stageHasExits,
|
|
2527
|
+
guardDenial
|
|
2150
2528
|
})
|
|
2151
2529
|
);
|
|
2152
2530
|
return {
|
|
2153
2531
|
task,
|
|
2154
2532
|
status,
|
|
2155
2533
|
// "pending on actor" treats both `pending` and `active` as waiting on
|
|
2156
|
-
// the assignee — pending tasks just haven't been
|
|
2157
|
-
// they're still inbox items.
|
|
2158
|
-
|
|
2534
|
+
// the assignee — pending tasks just haven't been activated yet, but
|
|
2535
|
+
// they're still inbox items. The inbox reads the assignees-kind state
|
|
2536
|
+
// entry BY KIND (who-data is state).
|
|
2537
|
+
pendingOnActor: (status === "active" || status === "pending") && assigned,
|
|
2159
2538
|
actions
|
|
2160
2539
|
};
|
|
2161
2540
|
}
|
|
2162
2541
|
async function evaluateAction(args) {
|
|
2163
|
-
const { action,
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2542
|
+
const { action, status, instance, snapshot, taskScope, stageHasExits, guardDenial } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
|
|
2543
|
+
return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : action.filter !== void 0 && !await evaluateCondition({
|
|
2544
|
+
condition: action.filter,
|
|
2545
|
+
snapshot,
|
|
2546
|
+
params: taskScope
|
|
2547
|
+
}) ? disabled(action, { kind: "filter-failed", filter: action.filter }) : { action, allowed: !0 };
|
|
2167
2548
|
}
|
|
2168
|
-
function
|
|
2549
|
+
async function instanceGuardReason(instance, actor, guards) {
|
|
2550
|
+
if (guards === void 0 || guards.length === 0) return;
|
|
2551
|
+
const denied = await instanceWriteDenials({ instance, guards, identity: actor.id });
|
|
2552
|
+
if (denied.length !== 0)
|
|
2553
|
+
return {
|
|
2554
|
+
kind: "mutation-guard-denied",
|
|
2555
|
+
guardIds: denied.map((g) => g._id),
|
|
2556
|
+
detail: denied.map((g) => g.name ?? g._id).join(", ")
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function lifecycleReason(instance, status, stageHasExits) {
|
|
2169
2560
|
if (instance.completedAt !== void 0)
|
|
2170
2561
|
return { kind: "instance-completed", completedAt: instance.completedAt };
|
|
2171
|
-
if (
|
|
2172
|
-
return { kind: "stage-terminal",
|
|
2173
|
-
if (status
|
|
2562
|
+
if (!stageHasExits)
|
|
2563
|
+
return { kind: "stage-terminal", stage: instance.currentStage };
|
|
2564
|
+
if (isTerminalTaskStatus(status))
|
|
2174
2565
|
return { kind: "task-not-active", status };
|
|
2175
2566
|
}
|
|
2176
|
-
function actorReason(action, task, actor) {
|
|
2177
|
-
const actorRoles = actor.roles ?? [];
|
|
2178
|
-
if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
|
|
2179
|
-
return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
|
|
2180
|
-
if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
|
|
2181
|
-
return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
|
|
2182
|
-
}
|
|
2183
|
-
async function permissionReason(action, instance, actor, grants) {
|
|
2184
|
-
if (grants === void 0) return;
|
|
2185
|
-
const permission = action.requiredPermission ?? "update";
|
|
2186
|
-
if (!await grantsPermissionOn({
|
|
2187
|
-
document: instance,
|
|
2188
|
-
grants,
|
|
2189
|
-
permission,
|
|
2190
|
-
userId: actor.id
|
|
2191
|
-
}))
|
|
2192
|
-
return { kind: "permission-denied", permission, matchingGrants: grants.length };
|
|
2193
|
-
}
|
|
2194
|
-
async function filterReason(action, instance, definition, snapshot, now) {
|
|
2195
|
-
if (!(action.filter === void 0 || await evaluateFilter({
|
|
2196
|
-
filter: action.filter,
|
|
2197
|
-
definition,
|
|
2198
|
-
snapshot,
|
|
2199
|
-
params: buildParams(instance, now)
|
|
2200
|
-
})))
|
|
2201
|
-
return { kind: "filter-failed", filter: action.filter };
|
|
2202
|
-
}
|
|
2203
2567
|
function disabled(action, reason) {
|
|
2204
2568
|
return { action, allowed: !1, disabledReason: reason };
|
|
2205
2569
|
}
|
|
2206
|
-
function
|
|
2207
|
-
|
|
2208
|
-
const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
|
|
2209
|
-
for (const assignee of assignees)
|
|
2210
|
-
if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
|
|
2211
|
-
return !0;
|
|
2212
|
-
return !1;
|
|
2213
|
-
}
|
|
2214
|
-
function parseDefinitionSnapshot(instance) {
|
|
2215
|
-
try {
|
|
2216
|
-
return JSON.parse(instance.definitionSnapshot);
|
|
2217
|
-
} catch (err) {
|
|
2218
|
-
throw new Error(
|
|
2219
|
-
`Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2220
|
-
{ cause: err }
|
|
2221
|
-
);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
2225
|
-
function guardsForResource(client) {
|
|
2226
|
-
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
2227
|
-
}
|
|
2228
|
-
async function guardsForInstance(args) {
|
|
2229
|
-
const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
|
|
2230
|
-
[resourceKey(instance.workflowResource), client]
|
|
2231
|
-
]), stateUris = [
|
|
2232
|
-
...collectSlotDocUris(instance.state),
|
|
2233
|
-
...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
|
|
2234
|
-
];
|
|
2235
|
-
for (const uri of stateUris) {
|
|
2236
|
-
const parsed = tryParseGdr(uri);
|
|
2237
|
-
if (parsed === void 0) continue;
|
|
2238
|
-
const key = resourceKey(resourceFromParsed(parsed));
|
|
2239
|
-
clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
|
|
2240
|
-
}
|
|
2241
|
-
const perResource = await Promise.all(
|
|
2242
|
-
[...clientsByResource.values()].map(
|
|
2243
|
-
(c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
|
|
2244
|
-
t: GUARD_DOC_TYPE,
|
|
2245
|
-
id: instance._id
|
|
2246
|
-
})
|
|
2247
|
-
)
|
|
2248
|
-
);
|
|
2249
|
-
return dedupById(perResource.flat());
|
|
2250
|
-
}
|
|
2251
|
-
function tryParseGdr(uri) {
|
|
2252
|
-
try {
|
|
2253
|
-
return parseGdr(uri);
|
|
2254
|
-
} catch {
|
|
2255
|
-
return;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
function dedupById(guards) {
|
|
2259
|
-
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
2260
|
-
}
|
|
2261
|
-
function definitionDocId(_workflowResource, prefix, workflowId, version) {
|
|
2262
|
-
return `${prefix}.${workflowId}.v${version}`;
|
|
2570
|
+
function definitionDocId(_workflowResource, prefix, definition, version) {
|
|
2571
|
+
return `${prefix}.${definition}.v${version}`;
|
|
2263
2572
|
}
|
|
2264
2573
|
async function sortByDependencies(client, definitions, tags, workflowResource) {
|
|
2265
|
-
const prefix = canonicalTag(tags),
|
|
2574
|
+
const prefix = canonicalTag(tags), byName = /* @__PURE__ */ new Map();
|
|
2266
2575
|
for (const def of definitions) {
|
|
2267
|
-
const list =
|
|
2268
|
-
list.push(def),
|
|
2576
|
+
const list = byName.get(def.name) ?? [];
|
|
2577
|
+
list.push(def), byName.set(def.name, list);
|
|
2269
2578
|
}
|
|
2270
|
-
const findInBatch = (ref) => findRefInBatch(
|
|
2579
|
+
const findInBatch = (ref) => findRefInBatch(byName, ref);
|
|
2271
2580
|
return await assertCrossBatchRefsDeployed(client, definitions, {
|
|
2272
2581
|
tags,
|
|
2273
2582
|
workflowResource,
|
|
@@ -2275,8 +2584,8 @@ async function sortByDependencies(client, definitions, tags, workflowResource) {
|
|
|
2275
2584
|
findInBatch
|
|
2276
2585
|
}), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
|
|
2277
2586
|
}
|
|
2278
|
-
function findRefInBatch(
|
|
2279
|
-
const candidates =
|
|
2587
|
+
function findRefInBatch(byName, ref) {
|
|
2588
|
+
const candidates = byName.get(ref.name);
|
|
2280
2589
|
if (!(candidates === void 0 || candidates.length === 0))
|
|
2281
2590
|
return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
|
|
2282
2591
|
(best, c) => best === void 0 || c.version > best.version ? c : best,
|
|
@@ -2286,7 +2595,7 @@ function findRefInBatch(byWorkflowId, ref) {
|
|
|
2286
2595
|
async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
2287
2596
|
const missing = [];
|
|
2288
2597
|
for (const def of definitions) {
|
|
2289
|
-
const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.
|
|
2598
|
+
const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
|
|
2290
2599
|
for (const ref of refsOf(def)) {
|
|
2291
2600
|
if (ctx.findInBatch(ref) !== void 0) continue;
|
|
2292
2601
|
const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
|
|
@@ -2302,16 +2611,16 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
|
2302
2611
|
);
|
|
2303
2612
|
}
|
|
2304
2613
|
async function resolveDeployedRefLabel(client, ref, tags) {
|
|
2305
|
-
const wantsExplicit = typeof ref.version == "number", params = {
|
|
2614
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
|
|
2306
2615
|
if (wantsExplicit && (params.version = ref.version), !await client.fetch(
|
|
2307
2616
|
definitionLookupGroq(wantsExplicit),
|
|
2308
2617
|
params
|
|
2309
2618
|
))
|
|
2310
|
-
return wantsExplicit ? `${ref.
|
|
2619
|
+
return wantsExplicit ? `${ref.name} v${ref.version}` : ref.name;
|
|
2311
2620
|
}
|
|
2312
2621
|
function topoSortDefinitions(definitions, ctx) {
|
|
2313
2622
|
const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
|
|
2314
|
-
const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.
|
|
2623
|
+
const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
|
|
2315
2624
|
if (!visited.has(id)) {
|
|
2316
2625
|
if (visiting.has(id))
|
|
2317
2626
|
throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
|
|
@@ -2330,9 +2639,9 @@ function refsOf(def) {
|
|
|
2330
2639
|
const out = [];
|
|
2331
2640
|
for (const stage of def.stages)
|
|
2332
2641
|
for (const task of stage.tasks ?? []) {
|
|
2333
|
-
const ref = task.
|
|
2642
|
+
const ref = task.subworkflows?.definition;
|
|
2334
2643
|
ref !== void 0 && out.push({
|
|
2335
|
-
|
|
2644
|
+
name: ref.name,
|
|
2336
2645
|
...ref.version !== void 0 ? { version: ref.version } : {}
|
|
2337
2646
|
});
|
|
2338
2647
|
}
|
|
@@ -2352,324 +2661,105 @@ function stableStringify(value) {
|
|
|
2352
2661
|
([a], [b]) => a.localeCompare(b)
|
|
2353
2662
|
).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
|
|
2354
2663
|
}
|
|
2355
|
-
async function loadDefinition(client,
|
|
2664
|
+
async function loadDefinition(client, definition, version, tags) {
|
|
2356
2665
|
if (version !== void 0) {
|
|
2357
2666
|
const doc = await client.fetch(
|
|
2358
2667
|
definitionLookupGroq(!0),
|
|
2359
|
-
{
|
|
2668
|
+
{ definition, version, engineTags: tags }
|
|
2360
2669
|
);
|
|
2361
2670
|
if (!doc)
|
|
2362
|
-
throw new Error(`Workflow definition ${
|
|
2671
|
+
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
2363
2672
|
return doc;
|
|
2364
2673
|
}
|
|
2365
2674
|
const latest = await client.fetch(definitionLookupGroq(!1), {
|
|
2366
|
-
|
|
2675
|
+
definition,
|
|
2367
2676
|
engineTags: tags
|
|
2368
2677
|
});
|
|
2369
2678
|
if (!latest)
|
|
2370
|
-
throw new Error(`No deployed definition for workflow ${
|
|
2679
|
+
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2371
2680
|
return latest;
|
|
2372
2681
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
"workflow.op.state.removeWhere"
|
|
2378
|
-
];
|
|
2379
|
-
function isStateOp(summary) {
|
|
2380
|
-
return STATE_OP_TYPES.includes(summary.opType);
|
|
2381
|
-
}
|
|
2382
|
-
function validateActionParams(action, taskId, callerParams) {
|
|
2383
|
-
const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
|
|
2384
|
-
for (const decl of declared) {
|
|
2385
|
-
const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
|
|
2386
|
-
if (decl.required === !0 && !present) {
|
|
2387
|
-
issues.push({ paramId: decl.id, reason: "required but missing" });
|
|
2388
|
-
continue;
|
|
2389
|
-
}
|
|
2390
|
-
present && (checkParamType(value, decl) || issues.push({
|
|
2391
|
-
paramId: decl.id,
|
|
2392
|
-
reason: `expected paramType=${decl.paramType}, got ${typeof value}`
|
|
2393
|
-
}));
|
|
2394
|
-
}
|
|
2395
|
-
if (issues.length > 0)
|
|
2396
|
-
throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
|
|
2397
|
-
return params;
|
|
2398
|
-
}
|
|
2399
|
-
function hasStringField(value, field) {
|
|
2400
|
-
return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
|
|
2401
|
-
}
|
|
2402
|
-
function checkParamType(value, decl) {
|
|
2403
|
-
switch (decl.paramType) {
|
|
2404
|
-
case "string":
|
|
2405
|
-
case "url":
|
|
2406
|
-
case "dateTime":
|
|
2407
|
-
return typeof value == "string";
|
|
2408
|
-
case "number":
|
|
2409
|
-
return typeof value == "number" && Number.isFinite(value);
|
|
2410
|
-
case "boolean":
|
|
2411
|
-
return typeof value == "boolean";
|
|
2412
|
-
case "actor":
|
|
2413
|
-
return hasStringField(value, "kind");
|
|
2414
|
-
case "doc.ref":
|
|
2415
|
-
return hasStringField(value, "id");
|
|
2416
|
-
case "doc.refs":
|
|
2417
|
-
return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
|
|
2418
|
-
case "json":
|
|
2419
|
-
return !0;
|
|
2420
|
-
default:
|
|
2421
|
-
return !1;
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
async function runActionOps(args) {
|
|
2425
|
-
const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
|
|
2426
|
-
if (ops === void 0 || ops.length === 0) return [];
|
|
2427
|
-
const summaries = [];
|
|
2428
|
-
for (const op of ops) {
|
|
2429
|
-
const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
|
|
2430
|
-
summaries.push(summary), mutation.history.push({
|
|
2431
|
-
_key: randomKey(),
|
|
2432
|
-
_type: "workflow.history.opApplied",
|
|
2433
|
-
at: now,
|
|
2434
|
-
stageId,
|
|
2435
|
-
taskId,
|
|
2436
|
-
actionId: actionName,
|
|
2437
|
-
opType: summary.opType,
|
|
2438
|
-
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2439
|
-
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2440
|
-
...actor !== void 0 ? { actor } : {}
|
|
2441
|
-
});
|
|
2442
|
-
}
|
|
2443
|
-
return summaries;
|
|
2444
|
-
}
|
|
2445
|
-
function applyOp(op, ctx) {
|
|
2446
|
-
switch (op.type) {
|
|
2447
|
-
case "workflow.op.state.set":
|
|
2448
|
-
return applyStateSet(op, ctx);
|
|
2449
|
-
case "workflow.op.state.append":
|
|
2450
|
-
return applyStateAppend(op, ctx);
|
|
2451
|
-
case "workflow.op.state.updateWhere":
|
|
2452
|
-
return applyStateUpdateWhere(op, ctx);
|
|
2453
|
-
case "workflow.op.state.removeWhere":
|
|
2454
|
-
return applyStateRemoveWhere(op, ctx);
|
|
2455
|
-
case "workflow.op.status.set":
|
|
2456
|
-
return applyStatusSet(op, ctx);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
function applyStateSet(op, ctx) {
|
|
2460
|
-
const value = resolveOpSource(op.value, ctx), slot = locateSlot(
|
|
2461
|
-
ctx,
|
|
2462
|
-
op.target,
|
|
2463
|
-
/*createIfMissing*/
|
|
2464
|
-
!0
|
|
2465
|
-
);
|
|
2466
|
-
return slot !== void 0 && (validateSlotValue({ slotType: slot._type, slotId: slot.id, value }), slot.value = value), { opType: op.type, target: op.target, resolved: { value } };
|
|
2467
|
-
}
|
|
2468
|
-
function applyStateAppend(op, ctx) {
|
|
2469
|
-
const item = resolveOpSource(op.item, ctx), slot = locateSlot(
|
|
2470
|
-
ctx,
|
|
2471
|
-
op.target,
|
|
2472
|
-
/*createIfMissing*/
|
|
2473
|
-
!0
|
|
2474
|
-
);
|
|
2475
|
-
if (slot !== void 0) {
|
|
2476
|
-
validateSlotAppendItem({ slotType: slot._type, slotId: slot.id, item });
|
|
2477
|
-
const current = Array.isArray(slot.value) ? slot.value : [];
|
|
2478
|
-
slot.value = [...current, withRowKey(item)];
|
|
2479
|
-
}
|
|
2480
|
-
return { opType: op.type, target: op.target, resolved: { item } };
|
|
2481
|
-
}
|
|
2482
|
-
function withRowKey(item) {
|
|
2483
|
-
return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
|
|
2484
|
-
}
|
|
2485
|
-
function applyStateUpdateWhere(op, ctx) {
|
|
2486
|
-
const slot = locateSlot(
|
|
2487
|
-
ctx,
|
|
2488
|
-
op.target,
|
|
2489
|
-
/*createIfMissing*/
|
|
2490
|
-
!1
|
|
2491
|
-
), resolvedMerge = {};
|
|
2492
|
-
for (const [k, v2] of Object.entries(op.merge))
|
|
2493
|
-
resolvedMerge[k] = resolveOpSource(v2, ctx);
|
|
2494
|
-
return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
|
|
2495
|
-
(row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
|
|
2496
|
-
)), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
|
|
2497
|
-
}
|
|
2498
|
-
function applyStateRemoveWhere(op, ctx) {
|
|
2499
|
-
const slot = locateSlot(
|
|
2500
|
-
ctx,
|
|
2501
|
-
op.target,
|
|
2502
|
-
/*createIfMissing*/
|
|
2503
|
-
!1
|
|
2682
|
+
async function loadDefinitionVersions(client, definition, tags) {
|
|
2683
|
+
return client.fetch(
|
|
2684
|
+
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && count(tags[@ in $engineTags]) > 0] | order(version desc)`,
|
|
2685
|
+
{ definition, engineTags: tags }
|
|
2504
2686
|
);
|
|
2505
|
-
return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.filter(
|
|
2506
|
-
(row) => !evalOpPredicate(op.where, row, ctx)
|
|
2507
|
-
)), { opType: op.type, target: op.target };
|
|
2508
|
-
}
|
|
2509
|
-
function applyStatusSet(op, ctx) {
|
|
2510
|
-
const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === op.taskId);
|
|
2511
|
-
return entry !== void 0 && (entry.status = op.status), { opType: op.type, resolved: { taskId: op.taskId, status: op.status } };
|
|
2512
|
-
}
|
|
2513
|
-
function locateSlot(ctx, target, createIfMissing) {
|
|
2514
|
-
let host;
|
|
2515
|
-
if (target.scope === "workflow")
|
|
2516
|
-
host = ctx.mutation.state;
|
|
2517
|
-
else if (target.scope === "stage")
|
|
2518
|
-
host = findOpenStageEntry(ctx.mutation)?.state;
|
|
2519
|
-
else {
|
|
2520
|
-
const task = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === ctx.taskId);
|
|
2521
|
-
host = task?.state, host === void 0 && task !== void 0 && createIfMissing && (task.state = [], host = task.state);
|
|
2522
|
-
}
|
|
2523
|
-
if (host === void 0)
|
|
2524
|
-
throw new Error(
|
|
2525
|
-
`Op target scope "${target.scope}" has no state container on this instance \u2014 declare the slot or check stage/task identity`
|
|
2526
|
-
);
|
|
2527
|
-
let slot = host.find((s) => s.id === target.slotId);
|
|
2528
|
-
return slot === void 0 && createIfMissing && (slot = {
|
|
2529
|
-
_key: randomKey(),
|
|
2530
|
-
_type: "workflow.state.value.string",
|
|
2531
|
-
id: target.slotId,
|
|
2532
|
-
value: null
|
|
2533
|
-
}, host.push(slot)), slot;
|
|
2534
|
-
}
|
|
2535
|
-
function resolveOpSource(src, ctx) {
|
|
2536
|
-
const staticValue = resolveStaticSource(src, ctx);
|
|
2537
|
-
if (staticValue.handled) return staticValue.value;
|
|
2538
|
-
switch (src.source) {
|
|
2539
|
-
case "self":
|
|
2540
|
-
return ctx.self;
|
|
2541
|
-
case "stageId":
|
|
2542
|
-
return ctx.stageId;
|
|
2543
|
-
case "stateRead": {
|
|
2544
|
-
const value = readSlotFromMutation(ctx, src.scope, src.slotId);
|
|
2545
|
-
return src.path !== void 0 ? getPath(value, src.path) : value;
|
|
2546
|
-
}
|
|
2547
|
-
case "effectOutput": {
|
|
2548
|
-
const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
|
|
2549
|
-
if (entry === void 0) return null;
|
|
2550
|
-
const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
|
|
2551
|
-
return v2 === void 0 ? null : v2;
|
|
2552
|
-
}
|
|
2553
|
-
case "object": {
|
|
2554
|
-
const out = {};
|
|
2555
|
-
for (const [field, fieldSrc] of Object.entries(src.fields))
|
|
2556
|
-
out[field] = resolveOpSource(fieldSrc, ctx);
|
|
2557
|
-
return out;
|
|
2558
|
-
}
|
|
2559
|
-
case "row":
|
|
2560
|
-
case "parentState":
|
|
2561
|
-
return;
|
|
2562
|
-
default:
|
|
2563
|
-
return;
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
function slotValue(slots, slotId) {
|
|
2567
|
-
return slots?.find((s) => s.id === slotId)?.value;
|
|
2568
|
-
}
|
|
2569
|
-
function readSlotFromMutation(ctx, scope, slotId) {
|
|
2570
|
-
if (scope === "workflow") return slotValue(ctx.mutation.state, slotId);
|
|
2571
|
-
const stageEntry = findOpenStageEntry(ctx.mutation);
|
|
2572
|
-
if (scope === "stage") return slotValue(stageEntry?.state, slotId);
|
|
2573
|
-
const task = stageEntry?.tasks.find((t) => t.id === ctx.taskId);
|
|
2574
|
-
return slotValue(task?.state, slotId);
|
|
2575
|
-
}
|
|
2576
|
-
function evalOpPredicate(p, row, ctx) {
|
|
2577
|
-
if ("all" in p) return p.all.every((sub) => evalOpPredicate(sub, row, ctx));
|
|
2578
|
-
if ("any" in p) return p.any.some((sub) => evalOpPredicate(sub, row, ctx));
|
|
2579
|
-
const target = resolveOpSource(p.equals, ctx);
|
|
2580
|
-
return row[p.field] === target;
|
|
2581
2687
|
}
|
|
2582
2688
|
async function fireAction(args) {
|
|
2583
|
-
const { client, instanceId,
|
|
2689
|
+
const { client, instanceId, task, action, params, options } = args;
|
|
2584
2690
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2585
2691
|
const ctx = await loadContext(client, instanceId, {
|
|
2586
2692
|
...options?.clock ? { clock: options.clock } : {}
|
|
2587
2693
|
});
|
|
2588
2694
|
try {
|
|
2589
|
-
return await commitAction(ctx,
|
|
2695
|
+
return await commitAction(ctx, task, action, params, options);
|
|
2590
2696
|
} catch (error) {
|
|
2591
2697
|
if (!isRevisionConflict(error)) throw error;
|
|
2592
2698
|
}
|
|
2593
2699
|
}
|
|
2594
2700
|
throw new ConcurrentFireActionError({
|
|
2595
2701
|
instanceId,
|
|
2596
|
-
|
|
2702
|
+
task,
|
|
2597
2703
|
action,
|
|
2598
2704
|
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
2599
2705
|
});
|
|
2600
2706
|
}
|
|
2601
|
-
async function resolveActionCommit(ctx,
|
|
2602
|
-
const { stage, task } = findTaskInCurrentStage(ctx,
|
|
2707
|
+
async function resolveActionCommit(ctx, taskName, actionName, callerParams, options) {
|
|
2708
|
+
const actor = options?.actor, { stage, task } = findTaskInCurrentStage(ctx, taskName), action = (task.actions ?? []).find((a) => a.name === actionName);
|
|
2603
2709
|
if (action === void 0)
|
|
2604
|
-
throw new Error(`Action "${actionName}" not declared on task "${
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2710
|
+
throw new Error(`Action "${actionName}" not declared on task "${taskName}"`);
|
|
2711
|
+
const can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0;
|
|
2712
|
+
if (!await ctxEvaluateCondition(ctx, action.filter, {
|
|
2713
|
+
taskName,
|
|
2714
|
+
...actor !== void 0 ? { actor } : {},
|
|
2715
|
+
...can !== void 0 ? { vars: { can } } : {}
|
|
2716
|
+
}))
|
|
2717
|
+
throw new Error(`Action "${actionName}" filter rejected on task "${taskName}"`);
|
|
2718
|
+
const entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
|
|
2608
2719
|
if (entry === void 0 || entry.status !== "active")
|
|
2609
2720
|
throw new Error(
|
|
2610
|
-
`Task "${
|
|
2721
|
+
`Task "${taskName}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
|
|
2611
2722
|
);
|
|
2612
|
-
const params = validateActionParams(action,
|
|
2723
|
+
const params = validateActionParams(action, taskName, callerParams);
|
|
2613
2724
|
return { stage, task, action, params };
|
|
2614
2725
|
}
|
|
2615
|
-
function
|
|
2616
|
-
|
|
2617
|
-
const initialStatus = mutEntry.status, newStatus = action.setStatus;
|
|
2618
|
-
return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
|
|
2619
|
-
taskStatusChangedEntry({
|
|
2620
|
-
stageId,
|
|
2621
|
-
taskId,
|
|
2622
|
-
from: initialStatus,
|
|
2623
|
-
to: newStatus,
|
|
2624
|
-
at: now,
|
|
2625
|
-
...actor !== void 0 ? { actor } : {}
|
|
2626
|
-
})
|
|
2627
|
-
), newStatus;
|
|
2628
|
-
}
|
|
2629
|
-
async function commitAction(ctx, taskId, actionName, callerParams, actor) {
|
|
2630
|
-
const { stage, task, action, params } = await resolveActionCommit(
|
|
2726
|
+
async function commitAction(ctx, taskName, actionName, callerParams, options) {
|
|
2727
|
+
const actor = options?.actor, { stage, action, params } = await resolveActionCommit(
|
|
2631
2728
|
ctx,
|
|
2632
|
-
|
|
2729
|
+
taskName,
|
|
2633
2730
|
actionName,
|
|
2634
|
-
callerParams
|
|
2635
|
-
|
|
2731
|
+
callerParams,
|
|
2732
|
+
options
|
|
2733
|
+
), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName), now = ctx.now;
|
|
2636
2734
|
mutation.history.push({
|
|
2637
2735
|
_key: randomKey(),
|
|
2638
|
-
_type: "
|
|
2736
|
+
_type: "actionFired",
|
|
2639
2737
|
at: now,
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2738
|
+
stage: stage.name,
|
|
2739
|
+
task: taskName,
|
|
2740
|
+
action: actionName,
|
|
2643
2741
|
...actor !== void 0 ? { actor } : {}
|
|
2644
2742
|
});
|
|
2645
|
-
const ranOps = await
|
|
2743
|
+
const statusBefore = mutEntry.status, ranOps = await runOps({
|
|
2646
2744
|
ops: action.ops,
|
|
2647
2745
|
mutation,
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
actionName,
|
|
2746
|
+
stage: stage.name,
|
|
2747
|
+
origin: { task: taskName, action: actionName },
|
|
2651
2748
|
params,
|
|
2652
2749
|
actor,
|
|
2653
2750
|
self: selfGdr(ctx.instance),
|
|
2654
2751
|
now
|
|
2655
|
-
}), newStatus =
|
|
2656
|
-
return await queueEffects(
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
{
|
|
2661
|
-
kind: "action",
|
|
2662
|
-
id: `${task.id}:${actionName}`
|
|
2663
|
-
},
|
|
2664
|
-
actor,
|
|
2665
|
-
params
|
|
2666
|
-
), await persistThenDeploy(
|
|
2752
|
+
}), newStatus = mutEntry.status !== statusBefore ? mutEntry.status : void 0;
|
|
2753
|
+
return await queueEffects(ctx, mutation, action.effects, { kind: "action", name: actionName }, actor, {
|
|
2754
|
+
callerParams: params,
|
|
2755
|
+
taskName
|
|
2756
|
+
}), await persistThenDeploy(
|
|
2667
2757
|
ctx,
|
|
2668
2758
|
mutation,
|
|
2669
|
-
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.
|
|
2759
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
|
|
2670
2760
|
), {
|
|
2671
2761
|
fired: !0,
|
|
2672
|
-
|
|
2762
|
+
task: taskName,
|
|
2673
2763
|
action: actionName,
|
|
2674
2764
|
...newStatus !== void 0 ? { newStatus } : {},
|
|
2675
2765
|
...ranOps.length > 0 ? { ranOps } : {}
|
|
@@ -2677,25 +2767,22 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
|
|
|
2677
2767
|
}
|
|
2678
2768
|
class ActionDisabledError extends Error {
|
|
2679
2769
|
reason;
|
|
2680
|
-
|
|
2770
|
+
task;
|
|
2681
2771
|
action;
|
|
2682
2772
|
constructor(args) {
|
|
2683
|
-
super(formatDisabledReason(args.
|
|
2773
|
+
super(formatDisabledReason(args.task, args.action, args.reason)), this.name = "ActionDisabledError", this.reason = args.reason, this.task = args.task, this.action = args.action;
|
|
2684
2774
|
}
|
|
2685
2775
|
}
|
|
2686
2776
|
const disabledReasonDetail = {
|
|
2687
|
-
"role-required": (r) => `requires one of [${r.required.join(", ")}]; actor has [${r.actorRoles.join(", ") || "(none)"}]`,
|
|
2688
|
-
"not-assigned": (r) => `actor "${r.actorId}" is not in the task assignees`,
|
|
2689
|
-
"permission-denied": (r) => `actor lacks "${r.permission}" permission on the workflow instance`,
|
|
2690
2777
|
"filter-failed": (r) => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
|
|
2691
2778
|
"task-not-active": (r) => `task status is "${r.status}"`,
|
|
2692
|
-
"stage-terminal": (r) => `stage "${r.
|
|
2779
|
+
"stage-terminal": (r) => `stage "${r.stage}" is terminal`,
|
|
2693
2780
|
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
2694
2781
|
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
|
|
2695
2782
|
};
|
|
2696
|
-
function formatDisabledReason(
|
|
2783
|
+
function formatDisabledReason(task, action, reason) {
|
|
2697
2784
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
2698
|
-
return `Action "${
|
|
2785
|
+
return `Action "${task}:${action}" is not allowed: ${detail}`;
|
|
2699
2786
|
}
|
|
2700
2787
|
function bareIdFromSpawnRef(uri) {
|
|
2701
2788
|
if (!uri.includes(":")) return [uri];
|
|
@@ -2744,17 +2831,21 @@ function intersectsTags(docTags, engineTags) {
|
|
|
2744
2831
|
function toEffectsContextEntries(ctx) {
|
|
2745
2832
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
2746
2833
|
}
|
|
2747
|
-
function assertActionAllowed(evaluation,
|
|
2748
|
-
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.
|
|
2834
|
+
function assertActionAllowed(evaluation, task, action) {
|
|
2835
|
+
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
|
|
2749
2836
|
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
2750
|
-
throw new ActionDisabledError({
|
|
2837
|
+
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
2751
2838
|
}
|
|
2752
2839
|
async function applyAction(args) {
|
|
2753
|
-
const { client, instance,
|
|
2754
|
-
|
|
2840
|
+
const { client, instance, task, action, params, actor, grants, clock } = args, options = {
|
|
2841
|
+
actor,
|
|
2842
|
+
...grants !== void 0 ? { grants } : {},
|
|
2843
|
+
...clock !== void 0 ? { clock } : {}
|
|
2844
|
+
};
|
|
2845
|
+
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
2755
2846
|
client,
|
|
2756
2847
|
instanceId: instance._id,
|
|
2757
|
-
|
|
2848
|
+
task,
|
|
2758
2849
|
action,
|
|
2759
2850
|
...params !== void 0 ? { params } : {},
|
|
2760
2851
|
options
|
|
@@ -2762,18 +2853,14 @@ async function applyAction(args) {
|
|
|
2762
2853
|
}
|
|
2763
2854
|
const workflow = {
|
|
2764
2855
|
/**
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2767
|
-
* `
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
*
|
|
2773
|
-
* out the dependency order itself (children before parents that spawn
|
|
2774
|
-
* them via `task.spawns.definitionRef`), idempotently writes any
|
|
2775
|
-
* definition whose content has changed, and reports a per-definition
|
|
2776
|
-
* outcome (`created` / `updated` / `unchanged`).
|
|
2856
|
+
* Deploy a set of workflow definitions as one call. Each lands as a
|
|
2857
|
+
* {@link WORKFLOW_DEFINITION_TYPE} document; subsequent versions are
|
|
2858
|
+
* stored alongside earlier ones — `startInstance` picks the highest
|
|
2859
|
+
* version by default. The SDK figures out the dependency order itself
|
|
2860
|
+
* (children before parents that spawn them via
|
|
2861
|
+
* `subworkflows.definition`), idempotently writes any definition
|
|
2862
|
+
* whose content has changed, and reports a per-definition outcome
|
|
2863
|
+
* (`created` / `updated` / `unchanged`).
|
|
2777
2864
|
*
|
|
2778
2865
|
* Refs may point inside the batch OR at already-deployed definitions
|
|
2779
2866
|
* in the lake — both are valid. A ref pointing at neither errors with
|
|
@@ -2789,14 +2876,14 @@ const workflow = {
|
|
|
2789
2876
|
const ordered = await sortByDependencies(client, definitions, tags, workflowResource), results = [], prefix = canonicalTag(tags), tx = client.transaction();
|
|
2790
2877
|
let hasWrites = !1;
|
|
2791
2878
|
for (const def of ordered) {
|
|
2792
|
-
const id = definitionDocId(workflowResource, prefix, def.
|
|
2879
|
+
const id = definitionDocId(workflowResource, prefix, def.name, def.version), expected = {
|
|
2793
2880
|
...def,
|
|
2794
2881
|
_id: id,
|
|
2795
2882
|
_type: WORKFLOW_DEFINITION_TYPE,
|
|
2796
2883
|
tags
|
|
2797
2884
|
}, existing = await client.getDocument(id);
|
|
2798
2885
|
if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
|
|
2799
|
-
results.push({
|
|
2886
|
+
results.push({ definition: def.name, version: def.version, status: "unchanged" });
|
|
2800
2887
|
continue;
|
|
2801
2888
|
}
|
|
2802
2889
|
if (existing) {
|
|
@@ -2807,7 +2894,7 @@ const workflow = {
|
|
|
2807
2894
|
} else
|
|
2808
2895
|
tx.create(expected);
|
|
2809
2896
|
hasWrites = !0, results.push({
|
|
2810
|
-
|
|
2897
|
+
definition: def.name,
|
|
2811
2898
|
version: def.version,
|
|
2812
2899
|
status: existing ? "updated" : "created"
|
|
2813
2900
|
});
|
|
@@ -2827,20 +2914,20 @@ const workflow = {
|
|
|
2827
2914
|
client,
|
|
2828
2915
|
tags,
|
|
2829
2916
|
workflowResource,
|
|
2830
|
-
|
|
2917
|
+
definition: definitionName,
|
|
2831
2918
|
version,
|
|
2832
2919
|
initialState,
|
|
2833
2920
|
ancestors,
|
|
2834
2921
|
effectsContext,
|
|
2835
2922
|
instanceId,
|
|
2836
2923
|
perspective
|
|
2837
|
-
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client,
|
|
2838
|
-
if (definition.stages.find((s) => s.
|
|
2924
|
+
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
|
|
2925
|
+
if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
|
|
2839
2926
|
throw new Error(
|
|
2840
|
-
`Initial stage "${definition.
|
|
2927
|
+
`Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
|
|
2841
2928
|
);
|
|
2842
2929
|
const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
|
|
2843
|
-
|
|
2930
|
+
entryDefs: definition.state ?? [],
|
|
2844
2931
|
initialState: initialState ?? [],
|
|
2845
2932
|
ctx: {
|
|
2846
2933
|
client,
|
|
@@ -2856,14 +2943,14 @@ const workflow = {
|
|
|
2856
2943
|
now,
|
|
2857
2944
|
tags,
|
|
2858
2945
|
workflowResource,
|
|
2859
|
-
|
|
2946
|
+
definitionName: definition.name,
|
|
2860
2947
|
pinnedVersion: definition.version,
|
|
2861
2948
|
definition,
|
|
2862
2949
|
state: resolvedState,
|
|
2863
2950
|
effectsContext: effectsContextEntries,
|
|
2864
2951
|
ancestors: ancestors ?? [],
|
|
2865
2952
|
perspective: effectivePerspective,
|
|
2866
|
-
|
|
2953
|
+
initialStage: definition.initialStage,
|
|
2867
2954
|
actor
|
|
2868
2955
|
});
|
|
2869
2956
|
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
|
|
@@ -2883,13 +2970,13 @@ const workflow = {
|
|
|
2883
2970
|
client,
|
|
2884
2971
|
tags,
|
|
2885
2972
|
instanceId,
|
|
2886
|
-
|
|
2973
|
+
task,
|
|
2887
2974
|
action,
|
|
2888
2975
|
params,
|
|
2889
2976
|
idempotent,
|
|
2890
2977
|
resourceClients
|
|
2891
2978
|
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
|
|
2892
|
-
if (findOpenStageEntry(before)?.tasks.find((t) => t.
|
|
2979
|
+
if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
|
|
2893
2980
|
return { instance: before, cascaded: 0, fired: !1 };
|
|
2894
2981
|
const evaluation = await evaluateInstance({
|
|
2895
2982
|
client,
|
|
@@ -2899,13 +2986,14 @@ const workflow = {
|
|
|
2899
2986
|
clock,
|
|
2900
2987
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
2901
2988
|
});
|
|
2902
|
-
assertActionAllowed(evaluation,
|
|
2989
|
+
assertActionAllowed(evaluation, task, action);
|
|
2903
2990
|
const ranOps = await applyAction({
|
|
2904
2991
|
client,
|
|
2905
2992
|
instance: before,
|
|
2906
|
-
|
|
2993
|
+
task,
|
|
2907
2994
|
action,
|
|
2908
2995
|
actor,
|
|
2996
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
2909
2997
|
clock,
|
|
2910
2998
|
...params !== void 0 ? { params } : {}
|
|
2911
2999
|
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
@@ -2953,8 +3041,8 @@ const workflow = {
|
|
|
2953
3041
|
* affected instances and the engine re-evaluates.
|
|
2954
3042
|
*/
|
|
2955
3043
|
tick: async (args) => {
|
|
2956
|
-
const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
2957
|
-
await
|
|
3044
|
+
const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload(client, instanceId, tags), guards = await verdictGuardsForInstance(client, current._id);
|
|
3045
|
+
await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
|
|
2958
3046
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
2959
3047
|
return {
|
|
2960
3048
|
instance: await reload(client, instanceId, tags),
|
|
@@ -2963,17 +3051,17 @@ const workflow = {
|
|
|
2963
3051
|
};
|
|
2964
3052
|
},
|
|
2965
3053
|
/**
|
|
2966
|
-
* Admin override — force the instance into `
|
|
3054
|
+
* Admin override — force the instance into `targetStage` regardless
|
|
2967
3055
|
* of filters or declared transitions. ACL gating should be enforced
|
|
2968
3056
|
* upstream; this verb performs the mechanical move.
|
|
2969
3057
|
*/
|
|
2970
3058
|
setStage: async (args) => {
|
|
2971
|
-
const { client, tags, instanceId,
|
|
3059
|
+
const { client, tags, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
2972
3060
|
await reload(client, instanceId, tags);
|
|
2973
3061
|
const result = await setStage({
|
|
2974
3062
|
client,
|
|
2975
3063
|
instanceId,
|
|
2976
|
-
|
|
3064
|
+
targetStage,
|
|
2977
3065
|
...reason !== void 0 ? { reason } : {},
|
|
2978
3066
|
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
2979
3067
|
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
@@ -2983,6 +3071,28 @@ const workflow = {
|
|
|
2983
3071
|
fired: result.fired
|
|
2984
3072
|
};
|
|
2985
3073
|
},
|
|
3074
|
+
/**
|
|
3075
|
+
* Admin override — hard-stop an in-flight instance where it stands.
|
|
3076
|
+
* No stage move, no transition effects, pending effects cancelled; see
|
|
3077
|
+
* the engine-level {@link engineAbortInstance} for the full contract.
|
|
3078
|
+
* Ancestors are propagated (not cascaded — the instance is terminal)
|
|
3079
|
+
* so a parent gate waiting on this child re-evaluates.
|
|
3080
|
+
*/
|
|
3081
|
+
abortInstance: async (args) => {
|
|
3082
|
+
const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3083
|
+
await reload(client, instanceId, tags);
|
|
3084
|
+
const result = await abortInstance({
|
|
3085
|
+
client,
|
|
3086
|
+
instanceId,
|
|
3087
|
+
...reason !== void 0 ? { reason } : {},
|
|
3088
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
3089
|
+
});
|
|
3090
|
+
return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), {
|
|
3091
|
+
instance: await reload(client, instanceId, tags),
|
|
3092
|
+
cascaded: 0,
|
|
3093
|
+
fired: result.fired
|
|
3094
|
+
};
|
|
3095
|
+
},
|
|
2986
3096
|
/**
|
|
2987
3097
|
* Fetch a workflow instance by id, scoped to the engine's tags.
|
|
2988
3098
|
* Throws when the instance doesn't exist or isn't visible to this
|
|
@@ -3002,6 +3112,20 @@ const workflow = {
|
|
|
3002
3112
|
instance
|
|
3003
3113
|
});
|
|
3004
3114
|
},
|
|
3115
|
+
guardsForDefinition: async (args) => {
|
|
3116
|
+
const { client, tags, workflowResource, definition, resourceClients } = args;
|
|
3117
|
+
validateTags(tags);
|
|
3118
|
+
const definitions = await loadDefinitionVersions(client, definition, tags);
|
|
3119
|
+
if (definitions.length === 0)
|
|
3120
|
+
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
3121
|
+
return guardsForDefinition({
|
|
3122
|
+
client,
|
|
3123
|
+
clientForGdr: buildClientForGdr(client, resourceClients),
|
|
3124
|
+
workflowResource,
|
|
3125
|
+
definition,
|
|
3126
|
+
definitions
|
|
3127
|
+
});
|
|
3128
|
+
},
|
|
3005
3129
|
/**
|
|
3006
3130
|
* Run a caller-supplied GROQ query with the engine's tags bound as
|
|
3007
3131
|
* `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
|
|
@@ -3024,12 +3148,13 @@ const workflow = {
|
|
|
3024
3148
|
* Snapshot-aware GROQ — runs against the same in-memory view that
|
|
3025
3149
|
* filters see for a given instance.
|
|
3026
3150
|
*
|
|
3027
|
-
* Hydrates the instance's snapshot (instance +
|
|
3028
|
-
*
|
|
3029
|
-
*
|
|
3030
|
-
*
|
|
3031
|
-
* `$ancestors`, `$stage`, `$now
|
|
3032
|
-
*
|
|
3151
|
+
* Hydrates the instance's snapshot (instance + ancestors + every doc
|
|
3152
|
+
* declared by a `doc.ref` / `doc.refs` entry in scope), then
|
|
3153
|
+
* evaluates the supplied GROQ in groq-js against that dataset. The
|
|
3154
|
+
* rendered scope is auto-bound: `$self`, `$state`, `$parent`,
|
|
3155
|
+
* `$ancestors`, `$stage`, `$now`, `$effects`, `$tasks`,
|
|
3156
|
+
* `$allTasksDone`, `$anyTaskFailed` — ids in GDR URI form to match
|
|
3157
|
+
* the snapshot's keying.
|
|
3033
3158
|
*
|
|
3034
3159
|
* Use when an external consumer (a UI, a debug pane, a test) wants
|
|
3035
3160
|
* to ask "what does the engine see for this workflow right now?"
|
|
@@ -3038,7 +3163,7 @@ const workflow = {
|
|
|
3038
3163
|
queryInScope: async (args) => {
|
|
3039
3164
|
const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
|
|
3040
3165
|
validateTags(tags);
|
|
3041
|
-
const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams(instance, clock()), tree = parse(groq, { params: { ...reserved, ...params } });
|
|
3166
|
+
const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = parse(groq, { params: { ...reserved, ...params } });
|
|
3042
3167
|
return await (await evaluate(tree, {
|
|
3043
3168
|
dataset: snapshot.docs,
|
|
3044
3169
|
params: { ...reserved, ...params }
|
|
@@ -3054,7 +3179,7 @@ const workflow = {
|
|
|
3054
3179
|
* filters on claim presence; `names` restricts to specific effect
|
|
3055
3180
|
* names. Both filters compose (AND).
|
|
3056
3181
|
*/
|
|
3057
|
-
findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.
|
|
3182
|
+
findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.name))),
|
|
3058
3183
|
/**
|
|
3059
3184
|
* Project the instance from a given actor's perspective. Returns a
|
|
3060
3185
|
* `WorkflowEvaluation` with per-action verdicts (`allowed` + a
|
|
@@ -3067,19 +3192,19 @@ const workflow = {
|
|
|
3067
3192
|
/**
|
|
3068
3193
|
* Materialised spawned children of a parent instance.
|
|
3069
3194
|
*
|
|
3070
|
-
* Walks `history` for `
|
|
3195
|
+
* Walks `history` for `spawned` events — the durable
|
|
3071
3196
|
* record. (The per-task `spawnedInstances` field on the active stage
|
|
3072
3197
|
* only exists while that stage is current; history survives.) Strips
|
|
3073
3198
|
* the GDR URI on each `instanceRef` to a bare `_id`, fetches the
|
|
3074
3199
|
* instances, drops any that aren't visible to this engine's tags, and
|
|
3075
3200
|
* returns them sorted by `startedAt` ascending.
|
|
3076
3201
|
*
|
|
3077
|
-
* Pass `
|
|
3202
|
+
* Pass `task` to restrict to a single spawning task on the parent.
|
|
3078
3203
|
*/
|
|
3079
3204
|
children: async (args) => {
|
|
3080
|
-
const { client, tags, instanceId,
|
|
3205
|
+
const { client, tags, instanceId, task } = args;
|
|
3081
3206
|
validateTags(tags);
|
|
3082
|
-
const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "
|
|
3207
|
+
const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "spawned", ids = parent.history.filter(isSpawned).filter((h) => task === void 0 || h.task === task).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
|
|
3083
3208
|
return ids.length === 0 ? [] : client.fetch(
|
|
3084
3209
|
`*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
|
|
3085
3210
|
{ ids, engineTags: tags }
|
|
@@ -3100,11 +3225,11 @@ async function verifyDeployedDefinitionsInternal(args) {
|
|
|
3100
3225
|
const { client, tags, effectHandlers, missingHandler, logger } = args;
|
|
3101
3226
|
validateTags(tags);
|
|
3102
3227
|
const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
|
|
3103
|
-
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(
|
|
3228
|
+
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
|
|
3104
3229
|
{ engineTags: tags }
|
|
3105
3230
|
), seen = [], missingByName = /* @__PURE__ */ new Map();
|
|
3106
3231
|
for (const def of definitions)
|
|
3107
|
-
seen.push({
|
|
3232
|
+
seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
|
|
3108
3233
|
if (effectHandlers[name] !== void 0) return;
|
|
3109
3234
|
const key = `${name}@@${def._id}`;
|
|
3110
3235
|
let bucket = missingByName.get(key);
|
|
@@ -3135,17 +3260,16 @@ async function verifyDeployedDefinitionsInternal(args) {
|
|
|
3135
3260
|
}
|
|
3136
3261
|
function walkEffectNames(def, visit) {
|
|
3137
3262
|
const visitEffects = (effects, location) => {
|
|
3138
|
-
for (const e of effects ?? []) visit(e.
|
|
3263
|
+
for (const e of effects ?? []) visit(e.name, location);
|
|
3139
3264
|
};
|
|
3140
3265
|
for (const stage of def.stages ?? []) {
|
|
3141
|
-
visitEffects(stage.onEnter, `stage[${stage.id}].onEnter`), visitEffects(stage.onExit, `stage[${stage.id}].onExit`);
|
|
3142
3266
|
for (const t of stage.tasks ?? []) {
|
|
3143
|
-
visitEffects(t.effects, `stage[${stage.
|
|
3267
|
+
visitEffects(t.effects, `stage[${stage.name}].task[${t.name}].effects`);
|
|
3144
3268
|
for (const a of t.actions ?? [])
|
|
3145
|
-
visitEffects(a.effects, `stage[${stage.
|
|
3269
|
+
visitEffects(a.effects, `stage[${stage.name}].task[${t.name}].action[${a.name}].effects`);
|
|
3146
3270
|
}
|
|
3147
3271
|
for (const tr of stage.transitions ?? [])
|
|
3148
|
-
visitEffects(tr.effects, `stage[${stage.
|
|
3272
|
+
visitEffects(tr.effects, `stage[${stage.name}].transition[${tr.name}].effects`);
|
|
3149
3273
|
}
|
|
3150
3274
|
}
|
|
3151
3275
|
async function drainEffectsInternal(args) {
|
|
@@ -3169,7 +3293,7 @@ async function drainEffectsInternal(args) {
|
|
|
3169
3293
|
(e) => e.claim === void 0 && !skippedKeys.has(e._key)
|
|
3170
3294
|
);
|
|
3171
3295
|
if (candidate === void 0) break;
|
|
3172
|
-
const handler = effectHandlers[candidate.
|
|
3296
|
+
const handler = effectHandlers[candidate.name];
|
|
3173
3297
|
if (handler === void 0) {
|
|
3174
3298
|
await assertSkippableOrThrow(missingHandler, candidate, instanceId, log), skipped.push(candidate), skippedKeys.add(candidate._key);
|
|
3175
3299
|
continue;
|
|
@@ -3207,16 +3331,16 @@ async function reportEffectOutcome(args) {
|
|
|
3207
3331
|
async function assertSkippableOrThrow(missingHandler, candidate, instanceId, log) {
|
|
3208
3332
|
if (await applyMissingHandler(
|
|
3209
3333
|
missingHandler,
|
|
3210
|
-
{ phase: "drain", name: candidate.
|
|
3334
|
+
{ phase: "drain", name: candidate.name, effectKey: candidate._key, instanceId },
|
|
3211
3335
|
log
|
|
3212
3336
|
) === "fail")
|
|
3213
3337
|
throw new Error(
|
|
3214
|
-
`drainEffects: no handler registered for "${candidate.
|
|
3338
|
+
`drainEffects: no handler registered for "${candidate.name}" (effectKey=${candidate._key}, instanceId=${instanceId})`
|
|
3215
3339
|
);
|
|
3216
3340
|
}
|
|
3217
3341
|
async function claimPendingEffect(client, instance, effectKey, drainerActor) {
|
|
3218
3342
|
const claim = {
|
|
3219
|
-
_type: "
|
|
3343
|
+
_type: "pendingEffect.claim",
|
|
3220
3344
|
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3221
3345
|
claimedBy: drainerActor
|
|
3222
3346
|
};
|
|
@@ -3233,7 +3357,7 @@ async function dispatchEffect(handler, candidate, ctx) {
|
|
|
3233
3357
|
client: ctx.client,
|
|
3234
3358
|
instanceId: ctx.instanceId,
|
|
3235
3359
|
effectKey: candidate._key,
|
|
3236
|
-
log: (message, extra) => ctx.logger(`effect.${candidate.
|
|
3360
|
+
log: (message, extra) => ctx.logger(`effect.${candidate.name}`).info(message, extra)
|
|
3237
3361
|
});
|
|
3238
3362
|
return result?.outputs !== void 0 ? { outputs: result.outputs } : {};
|
|
3239
3363
|
} catch (err) {
|
|
@@ -3253,7 +3377,7 @@ async function applyMissingHandler(policy, info, log) {
|
|
|
3253
3377
|
}
|
|
3254
3378
|
function createInstanceSession(args) {
|
|
3255
3379
|
const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
|
|
3256
|
-
let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
|
|
3380
|
+
let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
|
|
3257
3381
|
const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
|
|
3258
3382
|
const next = /* @__PURE__ */ new Map();
|
|
3259
3383
|
for (const ld of docs) {
|
|
@@ -3268,13 +3392,14 @@ function createInstanceSession(args) {
|
|
|
3268
3392
|
}, access = () => resolveAccess(client, {
|
|
3269
3393
|
...args.access !== void 0 ? { override: args.access } : {},
|
|
3270
3394
|
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
3271
|
-
}), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
|
|
3395
|
+
}), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held, guards) => {
|
|
3272
3396
|
const { actor, grants } = await access();
|
|
3273
3397
|
return evaluateFromSnapshot({
|
|
3274
3398
|
instance,
|
|
3275
3399
|
definition: parseDefinitionSnapshot(instance),
|
|
3276
3400
|
actor,
|
|
3277
3401
|
snapshot: snapshotFrom(held),
|
|
3402
|
+
guards,
|
|
3278
3403
|
...clock !== void 0 ? { now: clock() } : {},
|
|
3279
3404
|
...grants !== void 0 ? { grants } : {}
|
|
3280
3405
|
});
|
|
@@ -3302,24 +3427,29 @@ function createInstanceSession(args) {
|
|
|
3302
3427
|
}
|
|
3303
3428
|
applyUpdate(docs);
|
|
3304
3429
|
},
|
|
3430
|
+
updateGuards(guards) {
|
|
3431
|
+
heldGuards = guards.map((guard) => structuredClone(guard));
|
|
3432
|
+
},
|
|
3305
3433
|
evaluate() {
|
|
3306
|
-
return evaluateWith(overlay);
|
|
3434
|
+
return evaluateWith(overlay, heldGuards);
|
|
3307
3435
|
},
|
|
3308
3436
|
tick() {
|
|
3309
3437
|
return commit(async (actor, held) => {
|
|
3438
|
+
await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
|
|
3310
3439
|
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3311
3440
|
return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
|
|
3312
3441
|
});
|
|
3313
3442
|
},
|
|
3314
|
-
fireAction({
|
|
3443
|
+
fireAction({ task, action, params }) {
|
|
3315
3444
|
return commit(async (actor, held) => {
|
|
3316
|
-
assertActionAllowed(await evaluateWith(held),
|
|
3317
|
-
const ranOps = await applyAction({
|
|
3445
|
+
assertActionAllowed(await evaluateWith(held, heldGuards), task, action);
|
|
3446
|
+
const { grants } = await access(), ranOps = await applyAction({
|
|
3318
3447
|
client,
|
|
3319
3448
|
instance,
|
|
3320
|
-
|
|
3449
|
+
task,
|
|
3321
3450
|
action,
|
|
3322
3451
|
actor,
|
|
3452
|
+
...grants !== void 0 ? { grants } : {},
|
|
3323
3453
|
...clock !== void 0 ? { clock } : {},
|
|
3324
3454
|
...params !== void 0 ? { params } : {}
|
|
3325
3455
|
}), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
@@ -3330,7 +3460,7 @@ function createInstanceSession(args) {
|
|
|
3330
3460
|
}
|
|
3331
3461
|
function asHeldInstance(doc) {
|
|
3332
3462
|
const candidate = doc;
|
|
3333
|
-
if (candidate._type !==
|
|
3463
|
+
if (candidate._type !== WORKFLOW_INSTANCE_TYPE || typeof candidate.currentStage != "string" || !Array.isArray(candidate.stages))
|
|
3334
3464
|
throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
|
|
3335
3465
|
return doc;
|
|
3336
3466
|
}
|
|
@@ -3375,6 +3505,7 @@ function createEngine(args) {
|
|
|
3375
3505
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3376
3506
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
3377
3507
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
3508
|
+
abortInstance: (rest) => workflow.abortInstance(bind(rest)),
|
|
3378
3509
|
getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
|
|
3379
3510
|
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3380
3511
|
instance: (instanceDoc, opts) => createInstanceSession({
|
|
@@ -3393,12 +3524,19 @@ function createEngine(args) {
|
|
|
3393
3524
|
instanceId,
|
|
3394
3525
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3395
3526
|
}),
|
|
3396
|
-
|
|
3527
|
+
guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
|
|
3528
|
+
client,
|
|
3529
|
+
tags,
|
|
3530
|
+
workflowResource,
|
|
3531
|
+
definition,
|
|
3532
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3533
|
+
}),
|
|
3534
|
+
children: ({ instanceId, task }) => workflow.children({
|
|
3397
3535
|
client,
|
|
3398
3536
|
tags,
|
|
3399
3537
|
workflowResource,
|
|
3400
3538
|
instanceId,
|
|
3401
|
-
...
|
|
3539
|
+
...task !== void 0 ? { task } : {}
|
|
3402
3540
|
}),
|
|
3403
3541
|
query: ({ groq, params }) => workflow.query({
|
|
3404
3542
|
client,
|
|
@@ -3446,111 +3584,127 @@ function createEngine(args) {
|
|
|
3446
3584
|
};
|
|
3447
3585
|
}
|
|
3448
3586
|
const HISTORY_DISPLAY = {
|
|
3449
|
-
|
|
3587
|
+
stageEntered: {
|
|
3450
3588
|
title: "Stage entered",
|
|
3451
3589
|
description: "The instance crossed into a new stage."
|
|
3452
3590
|
},
|
|
3453
|
-
|
|
3591
|
+
stageExited: {
|
|
3454
3592
|
title: "Stage exited",
|
|
3455
3593
|
description: "The instance left a stage on the way to the next one."
|
|
3456
3594
|
},
|
|
3457
|
-
|
|
3595
|
+
taskActivated: {
|
|
3458
3596
|
title: "Task activated",
|
|
3459
3597
|
description: "A task flipped from `pending` to `active` and its onEnter effects queued."
|
|
3460
3598
|
},
|
|
3461
|
-
|
|
3599
|
+
taskStatusChanged: {
|
|
3462
3600
|
title: "Task status changed",
|
|
3463
3601
|
description: "An action or op flipped a task's status (active \u2192 done / skipped / failed)."
|
|
3464
3602
|
},
|
|
3465
|
-
|
|
3603
|
+
actionFired: {
|
|
3466
3604
|
title: "Action fired",
|
|
3467
3605
|
description: "An action was invoked against a task \u2014 ops ran, status may have flipped, effects queued."
|
|
3468
3606
|
},
|
|
3469
|
-
|
|
3607
|
+
transitionFired: {
|
|
3470
3608
|
title: "Transition fired",
|
|
3471
3609
|
description: "A transition committed and the instance moved to a new stage."
|
|
3472
3610
|
},
|
|
3473
|
-
|
|
3611
|
+
effectQueued: {
|
|
3474
3612
|
title: "Effect queued",
|
|
3475
3613
|
description: "A pending effect was added to the queue, waiting for a drainer to dispatch it."
|
|
3476
3614
|
},
|
|
3477
|
-
|
|
3615
|
+
effectCompleted: {
|
|
3478
3616
|
title: "Effect completed",
|
|
3479
3617
|
description: "A drainer ran an effect handler to completion (done or failed) and merged its outputs into effectsContext."
|
|
3480
3618
|
},
|
|
3481
|
-
|
|
3619
|
+
spawned: {
|
|
3482
3620
|
title: "Spawned child",
|
|
3483
|
-
description: "A
|
|
3621
|
+
description: "A task's `subworkflows` declaration spawned a child workflow instance."
|
|
3484
3622
|
},
|
|
3485
|
-
|
|
3623
|
+
aborted: {
|
|
3624
|
+
title: "Instance aborted",
|
|
3625
|
+
description: "An admin hard-stopped the instance \u2014 pending effects cancelled, stage guards lifted, abortedAt + completedAt stamped."
|
|
3626
|
+
},
|
|
3627
|
+
opApplied: {
|
|
3486
3628
|
title: "Op applied",
|
|
3487
3629
|
description: "An inline state-mutation op (state.set / state.append / status.set / etc.) ran during an action commit."
|
|
3488
3630
|
}
|
|
3489
3631
|
}, STATE_SLOT_DISPLAY = {
|
|
3490
|
-
"
|
|
3632
|
+
"doc.ref": {
|
|
3491
3633
|
title: "Document reference",
|
|
3492
3634
|
description: "Single GDR pointer at a document in this or another resource."
|
|
3493
3635
|
},
|
|
3494
|
-
"
|
|
3636
|
+
"doc.refs": {
|
|
3495
3637
|
title: "Document references",
|
|
3496
3638
|
description: "Ordered list of GDR pointers \u2014 multi-doc selection."
|
|
3497
3639
|
},
|
|
3498
|
-
"
|
|
3640
|
+
"release.ref": {
|
|
3641
|
+
title: "Content Release reference",
|
|
3642
|
+
description: "GDR pointer at a Content Release system doc, carrying the release name the engine derives the instance's read perspective from."
|
|
3643
|
+
},
|
|
3644
|
+
query: {
|
|
3499
3645
|
title: "Query result",
|
|
3500
|
-
description: "Snapshot of a GROQ projection captured at
|
|
3646
|
+
description: "Snapshot of a GROQ projection captured at entry-resolve time; resolvedAt is stamped on the value."
|
|
3501
3647
|
},
|
|
3502
|
-
"
|
|
3648
|
+
"value.string": {
|
|
3503
3649
|
title: "String value",
|
|
3504
|
-
description: "Free-form string
|
|
3650
|
+
description: "Free-form string entry."
|
|
3505
3651
|
},
|
|
3506
|
-
"
|
|
3652
|
+
"value.url": {
|
|
3507
3653
|
title: "URL value",
|
|
3508
|
-
description: "Validated URL
|
|
3654
|
+
description: "Validated URL entry."
|
|
3509
3655
|
},
|
|
3510
|
-
"
|
|
3656
|
+
"value.number": {
|
|
3511
3657
|
title: "Number value",
|
|
3512
|
-
description: "Numeric
|
|
3658
|
+
description: "Numeric entry."
|
|
3513
3659
|
},
|
|
3514
|
-
"
|
|
3660
|
+
"value.boolean": {
|
|
3515
3661
|
title: "Boolean value",
|
|
3516
|
-
description: "True/false
|
|
3662
|
+
description: "True/false entry."
|
|
3517
3663
|
},
|
|
3518
|
-
"
|
|
3664
|
+
"value.dateTime": {
|
|
3519
3665
|
title: "Date / time value",
|
|
3520
|
-
description: "ISO-timestamp
|
|
3666
|
+
description: "ISO-timestamp entry."
|
|
3521
3667
|
},
|
|
3522
|
-
"
|
|
3668
|
+
"value.actor": {
|
|
3669
|
+
title: "Actor value",
|
|
3670
|
+
description: "A typed (user|ai|system) actor identity."
|
|
3671
|
+
},
|
|
3672
|
+
checklist: {
|
|
3523
3673
|
title: "Checklist",
|
|
3524
3674
|
description: "Append-only list of checkable items; ops add/update/remove entries."
|
|
3525
3675
|
},
|
|
3526
|
-
|
|
3676
|
+
notes: {
|
|
3527
3677
|
title: "Notes log",
|
|
3528
3678
|
description: "Append-only structured-note log (signoffs, comments, audit rows)."
|
|
3529
3679
|
},
|
|
3530
|
-
|
|
3680
|
+
assignees: {
|
|
3531
3681
|
title: "Assignees",
|
|
3532
3682
|
description: "Ordered list of typed (user|role) assignees."
|
|
3533
3683
|
}
|
|
3534
3684
|
}, OP_DISPLAY = {
|
|
3535
|
-
"
|
|
3536
|
-
title: "Set
|
|
3537
|
-
description: "Overwrite a state
|
|
3685
|
+
"state.set": {
|
|
3686
|
+
title: "Set state entry",
|
|
3687
|
+
description: "Overwrite a state entry's value with a resolved Source."
|
|
3688
|
+
},
|
|
3689
|
+
"state.unset": {
|
|
3690
|
+
title: "Unset state entry",
|
|
3691
|
+
description: "Reset a state entry to its default (null for scalars, [] for array kinds)."
|
|
3538
3692
|
},
|
|
3539
|
-
"
|
|
3540
|
-
title: "Append to
|
|
3541
|
-
description: "Push a resolved item onto an array-
|
|
3693
|
+
"state.append": {
|
|
3694
|
+
title: "Append to state entry",
|
|
3695
|
+
description: "Push a resolved item onto an array-kind entry (notes, checklist, assignees, refs)."
|
|
3542
3696
|
},
|
|
3543
|
-
"
|
|
3697
|
+
"state.updateWhere": {
|
|
3544
3698
|
title: "Update matching rows",
|
|
3545
|
-
description: "Merge fields into rows of an array-
|
|
3699
|
+
description: "Merge fields into rows of an array-kind entry that match the predicate."
|
|
3546
3700
|
},
|
|
3547
|
-
"
|
|
3701
|
+
"state.removeWhere": {
|
|
3548
3702
|
title: "Remove matching rows",
|
|
3549
|
-
description: "Drop rows of an array-
|
|
3703
|
+
description: "Drop rows of an array-kind entry that match the predicate."
|
|
3550
3704
|
},
|
|
3551
|
-
"
|
|
3705
|
+
"status.set": {
|
|
3552
3706
|
title: "Set task status",
|
|
3553
|
-
description: "Flip a task's status explicitly (action
|
|
3707
|
+
description: "Flip a task's status explicitly (the action-level `status:` sugar desugars to this)."
|
|
3554
3708
|
}
|
|
3555
3709
|
}, EFFECTS_CONTEXT_DISPLAY = {
|
|
3556
3710
|
"effectsContext.string": {
|
|
@@ -3574,53 +3728,21 @@ const HISTORY_DISPLAY = {
|
|
|
3574
3728
|
description: "Stable JSON payload for effect handlers."
|
|
3575
3729
|
}
|
|
3576
3730
|
}, AUTHORING_DISPLAY = {
|
|
3577
|
-
|
|
3578
|
-
title: "Stage",
|
|
3579
|
-
description: "A bucket of tasks the workflow is currently in."
|
|
3580
|
-
},
|
|
3581
|
-
"workflow.task": {
|
|
3582
|
-
title: "Task",
|
|
3583
|
-
description: "A unit of work inside a stage; carries actions and may spawn children."
|
|
3584
|
-
},
|
|
3585
|
-
"workflow.action": {
|
|
3586
|
-
title: "Action",
|
|
3587
|
-
description: "An invocable transition on a task \u2014 runs ops, queues effects, may flip status."
|
|
3588
|
-
},
|
|
3589
|
-
"workflow.action.param": {
|
|
3590
|
-
title: "Action param",
|
|
3591
|
-
description: "Caller-supplied parameter on an action; validated before any commit."
|
|
3592
|
-
},
|
|
3593
|
-
"workflow.transition": {
|
|
3594
|
-
title: "Transition",
|
|
3595
|
-
description: "An edge between stages, gated by a filter."
|
|
3596
|
-
},
|
|
3597
|
-
"workflow.predicate": {
|
|
3598
|
-
title: "Predicate",
|
|
3599
|
-
description: "Named GROQ filter, reusable across transitions / tasks / actions."
|
|
3600
|
-
},
|
|
3601
|
-
"workflow.effect": {
|
|
3602
|
-
title: "Effect",
|
|
3603
|
-
description: "Externally-dispatched side effect; queued on commit, drained by a runtime, completed asynchronously."
|
|
3604
|
-
},
|
|
3605
|
-
"workflow.spawn": {
|
|
3606
|
-
title: "Spawn",
|
|
3607
|
-
description: "Declares a task that fans out one or more child workflow instances."
|
|
3608
|
-
},
|
|
3609
|
-
"workflow.pendingEffect": {
|
|
3731
|
+
pendingEffect: {
|
|
3610
3732
|
title: "Pending effect",
|
|
3611
3733
|
description: "An effect that has been queued but not yet dispatched."
|
|
3612
3734
|
},
|
|
3613
|
-
"
|
|
3735
|
+
"pendingEffect.claim": {
|
|
3614
3736
|
title: "Claim",
|
|
3615
3737
|
description: "A drainer's lock on a pending effect while it dispatches."
|
|
3616
3738
|
},
|
|
3617
|
-
|
|
3739
|
+
[WORKFLOW_DEFINITION_TYPE]: {
|
|
3618
3740
|
title: "Workflow definition",
|
|
3619
|
-
description: "An immutable workflow blueprint
|
|
3741
|
+
description: "An immutable workflow blueprint, addressed by `name` + `version`."
|
|
3620
3742
|
},
|
|
3621
|
-
|
|
3743
|
+
[WORKFLOW_INSTANCE_TYPE]: {
|
|
3622
3744
|
title: "Workflow instance",
|
|
3623
|
-
description: "A running (or finished) workflow against
|
|
3745
|
+
description: "A running (or finished) workflow against its declared state."
|
|
3624
3746
|
}
|
|
3625
3747
|
}, DISPLAY = {
|
|
3626
3748
|
...HISTORY_DISPLAY,
|
|
@@ -3659,11 +3781,13 @@ export {
|
|
|
3659
3781
|
compileGuard,
|
|
3660
3782
|
contentReleaseName,
|
|
3661
3783
|
createEngine,
|
|
3784
|
+
datasetResourceParts,
|
|
3662
3785
|
defaultLoggerFactory,
|
|
3663
3786
|
denyingGuards,
|
|
3664
3787
|
deployStageGuards,
|
|
3665
3788
|
displayDescription,
|
|
3666
3789
|
displayTitle,
|
|
3790
|
+
effectsContextMap,
|
|
3667
3791
|
evaluateFromSnapshot,
|
|
3668
3792
|
evaluateMutationGuard,
|
|
3669
3793
|
extractDocumentId,
|
|
@@ -3671,10 +3795,13 @@ export {
|
|
|
3671
3795
|
gdrRef,
|
|
3672
3796
|
gdrUri,
|
|
3673
3797
|
guardMatches,
|
|
3798
|
+
guardsForDefinition,
|
|
3674
3799
|
guardsForInstance,
|
|
3675
3800
|
guardsForResource,
|
|
3801
|
+
instanceGuardQuery,
|
|
3676
3802
|
isDefinitionUnchanged,
|
|
3677
3803
|
isGdr,
|
|
3804
|
+
isTerminalStage,
|
|
3678
3805
|
lakeGuardId,
|
|
3679
3806
|
parseGdr,
|
|
3680
3807
|
readsRaw,
|
|
@@ -3687,10 +3814,12 @@ export {
|
|
|
3687
3814
|
retractStageGuards,
|
|
3688
3815
|
silentLogger,
|
|
3689
3816
|
stripSystemFields,
|
|
3817
|
+
subscriptionDocument,
|
|
3690
3818
|
subscriptionDocumentsForInstance,
|
|
3691
3819
|
tagScopeFilter,
|
|
3692
3820
|
validateDefinition,
|
|
3693
3821
|
validateTags,
|
|
3822
|
+
verdictGuardsForInstance,
|
|
3694
3823
|
wallClock,
|
|
3695
3824
|
workflow
|
|
3696
3825
|
};
|