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