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