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