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