@sanity/workflow-engine 0.1.0 → 0.3.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 +431 -0
- package/dist/_chunks-cjs/schema.cjs.map +1 -0
- package/dist/_chunks-es/schema.js +416 -0
- package/dist/_chunks-es/schema.js.map +1 -0
- package/dist/define.cjs +57 -435
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +5035 -2647
- package/dist/define.d.ts +5035 -2647
- package/dist/define.js +34 -427
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +1737 -1289
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6141 -3196
- package/dist/index.d.ts +6141 -3196
- package/dist/index.js +1624 -1191
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
-
var groqJs = require("groq-js"),
|
|
3
|
+
var groqJs = require("groq-js"), schema = require("./_chunks-cjs/schema.cjs"), v = require("valibot");
|
|
4
|
+
function _interopNamespaceCompat(e) {
|
|
5
|
+
if (e && typeof e == "object" && "default" in e) return e;
|
|
6
|
+
var n = /* @__PURE__ */ Object.create(null);
|
|
7
|
+
return e && Object.keys(e).forEach(function(k) {
|
|
8
|
+
if (k !== "default") {
|
|
9
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
10
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
11
|
+
enumerable: !0,
|
|
12
|
+
get: function() {
|
|
13
|
+
return e[k];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}), n.default = e, Object.freeze(n);
|
|
18
|
+
}
|
|
19
|
+
var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
|
|
4
20
|
function validateDefinition(definition) {
|
|
5
|
-
const
|
|
21
|
+
const v2 = createDefinitionValidator();
|
|
6
22
|
for (const slot of definition.state ?? [])
|
|
7
|
-
|
|
23
|
+
v2.checkSlot(slot, `workflow.state "${slot.id}"`);
|
|
8
24
|
for (const p of definition.predicates ?? [])
|
|
9
|
-
p.groq &&
|
|
25
|
+
p.groq && v2.checkFilter(p.groq, `predicate "${p.id}"`);
|
|
10
26
|
for (const stage of definition.stages)
|
|
11
|
-
validateStage(
|
|
12
|
-
if (
|
|
27
|
+
validateStage(v2, stage);
|
|
28
|
+
if (v2.errors.length > 0)
|
|
13
29
|
throw new Error(
|
|
14
|
-
`defineWorkflow("${definition.workflowId}", v${definition.version}): ${
|
|
15
|
-
` +
|
|
30
|
+
`defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
|
|
31
|
+
` + v2.errors.join(`
|
|
16
32
|
`)
|
|
17
33
|
);
|
|
18
34
|
}
|
|
@@ -31,31 +47,29 @@ function createDefinitionValidator() {
|
|
|
31
47
|
};
|
|
32
48
|
return { errors, tryParse, checkFilter: (groq, where) => {
|
|
33
49
|
tryParse(groq, where), rejectTypeScan(groq, where);
|
|
34
|
-
}, checkSlot: (slot,
|
|
35
|
-
slot.source.kind === "
|
|
36
|
-
` \xB7 ${scopeLabel} slot "${slot.id}": source.kind="computed" is reserved (not implemented in v0.1)`
|
|
37
|
-
), slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
|
|
50
|
+
}, checkSlot: (slot, slotLabel) => {
|
|
51
|
+
slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
|
|
38
52
|
} };
|
|
39
53
|
}
|
|
40
|
-
function validateStage(
|
|
54
|
+
function validateStage(v2, stage) {
|
|
41
55
|
for (const slot of stage.state ?? [])
|
|
42
|
-
|
|
43
|
-
typeof stage.completion == "string" &&
|
|
56
|
+
v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
|
|
57
|
+
typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
|
|
44
58
|
for (const t of stage.transitions ?? [])
|
|
45
|
-
typeof t.filter == "string" &&
|
|
59
|
+
typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
|
|
46
60
|
for (const task of stage.tasks ?? [])
|
|
47
|
-
validateTask(
|
|
61
|
+
validateTask(v2, stage.id, task);
|
|
48
62
|
}
|
|
49
|
-
function validateTask(
|
|
63
|
+
function validateTask(v2, stageId, task) {
|
|
50
64
|
const where = `stage "${stageId}" task "${task.id}"`;
|
|
51
65
|
for (const slot of task.state ?? [])
|
|
52
|
-
|
|
53
|
-
typeof task.filter == "string" &&
|
|
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`);
|
|
54
68
|
for (const a of task.actions ?? [])
|
|
55
|
-
typeof a.filter == "string" &&
|
|
56
|
-
task.spawns?.forEach?.groq &&
|
|
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`);
|
|
57
71
|
}
|
|
58
|
-
const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
72
|
+
const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
|
|
59
73
|
function parseGdr(uri) {
|
|
60
74
|
const colon = uri.indexOf(":");
|
|
61
75
|
if (colon < 0)
|
|
@@ -122,6 +136,9 @@ function gdrFromResource(res, documentId) {
|
|
|
122
136
|
}
|
|
123
137
|
return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
|
|
124
138
|
}
|
|
139
|
+
function selfGdr(doc) {
|
|
140
|
+
return gdrFromResource(doc.workflowResource, doc._id);
|
|
141
|
+
}
|
|
125
142
|
function isGdrUri(value) {
|
|
126
143
|
if (typeof value != "string") return !1;
|
|
127
144
|
try {
|
|
@@ -136,15 +153,15 @@ function gdrRef(res, documentId, type) {
|
|
|
136
153
|
function isGdr(value) {
|
|
137
154
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
138
155
|
}
|
|
139
|
-
function buildParams(instance, extra) {
|
|
156
|
+
function buildParams(instance, now, extra) {
|
|
140
157
|
return {
|
|
141
|
-
self:
|
|
158
|
+
self: selfGdr(instance),
|
|
142
159
|
state: stateMap(instance.state ?? []),
|
|
143
160
|
parent: instance.ancestors.at(-1)?.id ?? null,
|
|
144
161
|
ancestors: instance.ancestors.map((a) => a.id),
|
|
145
162
|
/** Current stage id — bind for filters that scope to the current stage. */
|
|
146
163
|
stage: instance.currentStageId,
|
|
147
|
-
now
|
|
164
|
+
now,
|
|
148
165
|
...extra
|
|
149
166
|
};
|
|
150
167
|
}
|
|
@@ -168,8 +185,8 @@ function stripStateForLake(value) {
|
|
|
168
185
|
if (Array.isArray(value)) return value.map(stripStateForLake);
|
|
169
186
|
if (typeof value == "object") {
|
|
170
187
|
const out = {};
|
|
171
|
-
for (const [k,
|
|
172
|
-
out[k] = stripStateForLake(
|
|
188
|
+
for (const [k, v2] of Object.entries(value))
|
|
189
|
+
out[k] = stripStateForLake(v2);
|
|
173
190
|
return out;
|
|
174
191
|
}
|
|
175
192
|
return value;
|
|
@@ -177,9 +194,94 @@ function stripStateForLake(value) {
|
|
|
177
194
|
function bareId(id) {
|
|
178
195
|
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
179
196
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
function getPath(value, path) {
|
|
198
|
+
let current = value;
|
|
199
|
+
for (const part of path.split(".")) {
|
|
200
|
+
if (current == null || typeof current != "object") return;
|
|
201
|
+
current = current[part];
|
|
202
|
+
}
|
|
203
|
+
return current;
|
|
204
|
+
}
|
|
205
|
+
function readStateSlot(instance, scope, slotId, stageId, taskId) {
|
|
206
|
+
if (scope === "workflow")
|
|
207
|
+
return slotValue$1(instance.state?.find((s) => s.id === slotId));
|
|
208
|
+
const stageEntry = instance.stages.find(
|
|
209
|
+
(s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
|
|
210
|
+
);
|
|
211
|
+
if (stageEntry === void 0) return;
|
|
212
|
+
if (scope === "stage")
|
|
213
|
+
return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
|
|
214
|
+
const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
|
|
215
|
+
return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
|
|
216
|
+
}
|
|
217
|
+
function slotValue$1(slot) {
|
|
218
|
+
if (slot != null && typeof slot == "object")
|
|
219
|
+
return slot.value;
|
|
220
|
+
}
|
|
221
|
+
function resolveStaticSource(src, ctx) {
|
|
222
|
+
switch (src.source) {
|
|
223
|
+
case "literal":
|
|
224
|
+
return { handled: !0, value: src.value };
|
|
225
|
+
case "param":
|
|
226
|
+
return { handled: !0, value: ctx.params?.[src.paramId] };
|
|
227
|
+
case "actor":
|
|
228
|
+
return { handled: !0, value: ctx.actor };
|
|
229
|
+
case "now":
|
|
230
|
+
return { handled: !0, value: ctx.now };
|
|
231
|
+
default:
|
|
232
|
+
return { handled: !1 };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function resolveSource(src, ctx) {
|
|
236
|
+
const staticValue = resolveStaticSource(src, ctx);
|
|
237
|
+
if (staticValue.handled) return staticValue.value;
|
|
238
|
+
switch (src.source) {
|
|
239
|
+
case "self":
|
|
240
|
+
return ctx.instance._id;
|
|
241
|
+
case "stageId":
|
|
242
|
+
return ctx.stageId ?? ctx.instance.currentStageId;
|
|
243
|
+
case "stateRead":
|
|
244
|
+
return resolveStateRead(src, ctx);
|
|
245
|
+
case "effectOutput":
|
|
246
|
+
return resolveEffectOutput(src, ctx);
|
|
247
|
+
case "object":
|
|
248
|
+
return resolveObject(src, ctx);
|
|
249
|
+
case "row":
|
|
250
|
+
case "parentState":
|
|
251
|
+
return;
|
|
252
|
+
default:
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function resolveStateRead(src, ctx) {
|
|
257
|
+
const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
|
|
258
|
+
return src.path !== void 0 ? getPath(value, src.path) : value;
|
|
259
|
+
}
|
|
260
|
+
function resolveEffectOutput(src, ctx) {
|
|
261
|
+
const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
|
|
262
|
+
if (entry === void 0) return null;
|
|
263
|
+
const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
|
|
264
|
+
return value === void 0 ? null : value;
|
|
265
|
+
}
|
|
266
|
+
function resolveObject(src, ctx) {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const [field, fieldSrc] of Object.entries(src.fields))
|
|
269
|
+
out[field] = resolveSource(fieldSrc, ctx);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
async function resolveBindings(args) {
|
|
273
|
+
const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
|
|
274
|
+
if (bindings) {
|
|
275
|
+
const ctx = {
|
|
276
|
+
instance,
|
|
277
|
+
now,
|
|
278
|
+
...params !== void 0 ? { params } : {},
|
|
279
|
+
...actor !== void 0 ? { actor } : {}
|
|
280
|
+
};
|
|
281
|
+
for (const [key, src] of Object.entries(bindings))
|
|
282
|
+
resolved[key] = resolveSource(src, ctx);
|
|
283
|
+
}
|
|
284
|
+
return { ...resolved, ...staticInput };
|
|
183
285
|
}
|
|
184
286
|
async function evaluateFilter(args) {
|
|
185
287
|
const { filter, definition, snapshot, params } = args;
|
|
@@ -227,7 +329,7 @@ function matchesParamType(type, value) {
|
|
|
227
329
|
case "boolean":
|
|
228
330
|
return typeof value == "boolean";
|
|
229
331
|
case "string[]":
|
|
230
|
-
return Array.isArray(value) && value.every((
|
|
332
|
+
return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
|
|
231
333
|
case "reference":
|
|
232
334
|
return isGdr(value);
|
|
233
335
|
}
|
|
@@ -235,186 +337,403 @@ function matchesParamType(type, value) {
|
|
|
235
337
|
function describeValue(value) {
|
|
236
338
|
return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
237
339
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
340
|
+
const GUARD_DOC_TYPE = "temp.system.guard";
|
|
341
|
+
class MutationGuardDeniedError extends Error {
|
|
342
|
+
denied;
|
|
343
|
+
documentId;
|
|
344
|
+
action;
|
|
345
|
+
constructor(args) {
|
|
346
|
+
const ids = args.denied.map((d) => d.guardId).join(", ");
|
|
347
|
+
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;
|
|
243
348
|
}
|
|
244
|
-
return { docs, knownIds };
|
|
245
|
-
}
|
|
246
|
-
function restampForSnapshot(doc, gdrUri2, resource) {
|
|
247
|
-
return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
|
|
248
349
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
return value.map((v) => rewriteRefsRecursive(v, resource));
|
|
252
|
-
if (value === null || typeof value != "object") return value;
|
|
253
|
-
const obj = value, out = {};
|
|
254
|
-
for (const [k, v] of Object.entries(obj))
|
|
255
|
-
k === "_ref" && typeof v == "string" ? out[k] = v.includes(":") ? v : gdrFromResource(resource, v) : out[k] = rewriteRefsRecursive(v, resource);
|
|
256
|
-
return out;
|
|
350
|
+
function lakeGuardId(args) {
|
|
351
|
+
return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
|
|
257
352
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
353
|
+
function compileGuard(args) {
|
|
354
|
+
return {
|
|
355
|
+
_id: args.id,
|
|
356
|
+
_type: GUARD_DOC_TYPE,
|
|
357
|
+
resourceType: args.resourceType,
|
|
358
|
+
resourceId: args.resourceId,
|
|
359
|
+
owner: args.owner,
|
|
360
|
+
sourceInstanceId: args.sourceInstanceId,
|
|
361
|
+
sourceDefinitionId: args.sourceDefinitionId,
|
|
362
|
+
sourceStageId: args.sourceStageId,
|
|
363
|
+
...args.name !== void 0 ? { name: args.name } : {},
|
|
364
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
365
|
+
match: args.match,
|
|
366
|
+
predicate: args.predicate,
|
|
367
|
+
metadata: args.metadata
|
|
263
368
|
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
369
|
+
}
|
|
370
|
+
function toGroqJsPredicate(predicate) {
|
|
371
|
+
return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
|
|
372
|
+
}
|
|
373
|
+
function globMatch(pattern, value) {
|
|
374
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
375
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
376
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
377
|
+
}
|
|
378
|
+
function guardMatches(guard, doc, action) {
|
|
379
|
+
const m = guard.match;
|
|
380
|
+
if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
|
|
381
|
+
return !1;
|
|
382
|
+
if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
|
|
383
|
+
const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
|
|
384
|
+
if (!byRef && !byPattern) return !1;
|
|
275
385
|
}
|
|
276
|
-
return
|
|
386
|
+
return !0;
|
|
277
387
|
}
|
|
278
|
-
async function
|
|
279
|
-
|
|
388
|
+
async function evaluateMutationGuard(args) {
|
|
389
|
+
const { guard, context } = args;
|
|
390
|
+
if (guard.predicate === "") return !1;
|
|
391
|
+
const params = { guard, mutation: { action: context.action } };
|
|
280
392
|
try {
|
|
281
|
-
|
|
393
|
+
const tree = groqJs.parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
|
|
394
|
+
return await (await groqJs.evaluate(tree, {
|
|
395
|
+
before: context.before,
|
|
396
|
+
after: context.after,
|
|
397
|
+
params,
|
|
398
|
+
...context.identity !== void 0 ? { identity: context.identity } : {}
|
|
399
|
+
})).get() === !0;
|
|
282
400
|
} catch {
|
|
283
|
-
|
|
284
|
-
return doc2 ? { doc: doc2, resource: defaultResource } : null;
|
|
401
|
+
return !1;
|
|
285
402
|
}
|
|
286
|
-
const doc = await clientForGdr(parsed).getDocument(parsed.documentId);
|
|
287
|
-
return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
|
|
288
403
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
404
|
+
async function denyingGuards(args) {
|
|
405
|
+
const { guards, doc, context } = args, denied = [];
|
|
406
|
+
for (const guard of guards)
|
|
407
|
+
guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
|
|
408
|
+
return denied;
|
|
291
409
|
}
|
|
292
|
-
function
|
|
293
|
-
return
|
|
410
|
+
function resourceOf(p) {
|
|
411
|
+
return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
|
|
294
412
|
}
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const v = s.value;
|
|
304
|
-
return Array.isArray(v) ? v.map(refId).filter((id) => id !== void 0) : [];
|
|
413
|
+
function resolveIdRefTarget(src, ctx) {
|
|
414
|
+
const value = resolveSource(src, ctx);
|
|
415
|
+
if (typeof value == "string" && isGdrUri(value))
|
|
416
|
+
return { parsed: parseGdr(value) };
|
|
417
|
+
if (value && typeof value == "object") {
|
|
418
|
+
const v2 = value;
|
|
419
|
+
if (typeof v2.id == "string" && isGdrUri(v2.id))
|
|
420
|
+
return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
|
|
305
421
|
}
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
function refId(ref) {
|
|
309
|
-
return ref && typeof ref.id == "string" ? ref.id : void 0;
|
|
422
|
+
return null;
|
|
310
423
|
}
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
for (const
|
|
314
|
-
|
|
315
|
-
|
|
424
|
+
function resolveIdRefTargets(idRefs, ctx) {
|
|
425
|
+
const targets = [];
|
|
426
|
+
for (const src of idRefs ?? []) {
|
|
427
|
+
const target = resolveIdRefTarget(src, ctx);
|
|
428
|
+
if (target === null) return null;
|
|
429
|
+
targets.push(target);
|
|
316
430
|
}
|
|
317
|
-
return
|
|
431
|
+
return targets.length === 0 ? null : targets;
|
|
318
432
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
433
|
+
function assertSingleResource(targets) {
|
|
434
|
+
const resource = resourceOf(targets[0].parsed);
|
|
435
|
+
for (const g of targets) {
|
|
436
|
+
const r = resourceOf(g.parsed);
|
|
437
|
+
if (r.type !== resource.type || r.id !== resource.id)
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
|
|
440
|
+
);
|
|
326
441
|
}
|
|
442
|
+
return resource;
|
|
327
443
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
type: zod.z.string().min(1)
|
|
331
|
-
}).passthrough(), ReleaseRefShape = zod.z.object({
|
|
332
|
-
id: zod.z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
|
|
333
|
-
type: zod.z.literal("system.release"),
|
|
334
|
-
releaseName: zod.z.string().min(1)
|
|
335
|
-
}).passthrough(), ActorShape = zod.z.object({
|
|
336
|
-
kind: zod.z.enum(["user", "ai", "system"]),
|
|
337
|
-
id: zod.z.string().min(1),
|
|
338
|
-
roles: zod.z.array(zod.z.string()).optional(),
|
|
339
|
-
onBehalfOf: zod.z.string().optional()
|
|
340
|
-
}).passthrough(), AssigneeShape = zod.z.union([
|
|
341
|
-
zod.z.object({ kind: zod.z.literal("user"), id: zod.z.string().min(1) }).passthrough(),
|
|
342
|
-
zod.z.object({ kind: zod.z.literal("role"), role: zod.z.string().min(1) }).passthrough()
|
|
343
|
-
]), ChecklistItemShape = zod.z.object({
|
|
344
|
-
label: zod.z.string().min(1),
|
|
345
|
-
done: zod.z.boolean(),
|
|
346
|
-
doneBy: zod.z.string().optional(),
|
|
347
|
-
doneAt: zod.z.string().optional(),
|
|
348
|
-
_key: zod.z.string().min(1).optional()
|
|
349
|
-
}).passthrough(), NoteItemShape = zod.z.object({
|
|
350
|
-
_key: zod.z.string().min(1).optional()
|
|
351
|
-
}).passthrough().refine((v) => typeof v == "object" && v !== null && !Array.isArray(v), {
|
|
352
|
-
message: "must be an object"
|
|
353
|
-
}), NullableString = zod.z.union([zod.z.null(), zod.z.string()]), NullableNumber = zod.z.union([zod.z.null(), zod.z.number()]), NullableBoolean = zod.z.union([zod.z.null(), zod.z.boolean()]), NullableDateTime = zod.z.union([
|
|
354
|
-
zod.z.null(),
|
|
355
|
-
zod.z.string().refine((s) => !Number.isNaN(Date.parse(s)), {
|
|
356
|
-
message: "must be an ISO-8601 datetime string"
|
|
357
|
-
})
|
|
358
|
-
]), NullableUrl = NullableString, valueSchemas = {
|
|
359
|
-
"workflow.state.doc.ref": zod.z.union([zod.z.null(), GdrShape]),
|
|
360
|
-
"workflow.state.doc.refs": zod.z.array(GdrShape),
|
|
361
|
-
"workflow.state.release.ref": zod.z.union([zod.z.null(), ReleaseRefShape]),
|
|
362
|
-
"workflow.state.query": zod.z.any(),
|
|
363
|
-
"workflow.state.value.string": NullableString,
|
|
364
|
-
"workflow.state.value.url": NullableUrl,
|
|
365
|
-
"workflow.state.value.number": NullableNumber,
|
|
366
|
-
"workflow.state.value.boolean": NullableBoolean,
|
|
367
|
-
"workflow.state.value.dateTime": NullableDateTime,
|
|
368
|
-
"workflow.state.value.actor": zod.z.union([zod.z.null(), ActorShape]),
|
|
369
|
-
"workflow.state.checklist": zod.z.array(ChecklistItemShape),
|
|
370
|
-
"workflow.state.notes": zod.z.array(NoteItemShape),
|
|
371
|
-
"workflow.state.assignees": zod.z.array(AssigneeShape)
|
|
372
|
-
}, itemSchemas = {
|
|
373
|
-
"workflow.state.doc.refs": GdrShape,
|
|
374
|
-
"workflow.state.checklist": ChecklistItemShape,
|
|
375
|
-
"workflow.state.notes": NoteItemShape,
|
|
376
|
-
"workflow.state.assignees": AssigneeShape
|
|
377
|
-
};
|
|
378
|
-
function isAppendable(slotType) {
|
|
379
|
-
return slotType in itemSchemas;
|
|
444
|
+
function bareIdRefs(targets) {
|
|
445
|
+
return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
|
|
380
446
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
slotType: args.slotType,
|
|
386
|
-
slotId: args.slotId,
|
|
387
|
-
issues: [`unknown slot type ${args.slotType}`],
|
|
388
|
-
mode: "value"
|
|
389
|
-
});
|
|
390
|
-
const result = schema.safeParse(args.value);
|
|
391
|
-
if (!result.success)
|
|
392
|
-
throw new SlotValueShapeError({
|
|
393
|
-
slotType: args.slotType,
|
|
394
|
-
slotId: args.slotId,
|
|
395
|
-
issues: formatIssues(result.error.issues),
|
|
396
|
-
mode: "value"
|
|
397
|
-
});
|
|
447
|
+
function resolveMatchTypes(targets, authorTypes) {
|
|
448
|
+
if (authorTypes !== void 0) return authorTypes;
|
|
449
|
+
const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
|
|
450
|
+
return inferred.length > 0 ? inferred : void 0;
|
|
398
451
|
}
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
452
|
+
function resolveMetadata(metadata, ctx) {
|
|
453
|
+
const out = {};
|
|
454
|
+
for (const [k, src] of Object.entries(metadata ?? {}))
|
|
455
|
+
out[k] = resolveSource(src, ctx);
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
459
|
+
function resolveGuard(guard, index, instance, stageId, now) {
|
|
460
|
+
const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
461
|
+
if (targets === null) return null;
|
|
462
|
+
const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
|
|
463
|
+
return { doc: compileGuard({
|
|
464
|
+
id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
|
|
465
|
+
resourceType: resource.type,
|
|
466
|
+
resourceId: resource.id,
|
|
467
|
+
owner: GUARD_OWNER,
|
|
468
|
+
sourceInstanceId: instance._id,
|
|
469
|
+
sourceDefinitionId: instance.workflowId,
|
|
470
|
+
sourceStageId: stageId,
|
|
471
|
+
...guard.name !== void 0 ? { name: guard.name } : {},
|
|
472
|
+
...guard.description !== void 0 ? { description: guard.description } : {},
|
|
473
|
+
match: {
|
|
474
|
+
...types !== void 0 ? { types } : {},
|
|
475
|
+
idRefs: bareIdRefs(targets),
|
|
476
|
+
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
477
|
+
actions: guard.match.actions
|
|
478
|
+
},
|
|
479
|
+
predicate: guard.predicate ?? "",
|
|
480
|
+
metadata: resolveMetadata(guard.metadata, ctx)
|
|
481
|
+
}), routeGdr: targets[0].parsed };
|
|
482
|
+
}
|
|
483
|
+
async function upsertGuard(client, doc) {
|
|
484
|
+
if (!await client.getDocument(doc._id)) {
|
|
485
|
+
await client.create(doc);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
489
|
+
await client.patch(doc._id).set(body).commit();
|
|
490
|
+
}
|
|
491
|
+
function resolvedStageGuards(args) {
|
|
492
|
+
const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
|
|
493
|
+
for (const [index, guard] of (stage?.guards ?? []).entries()) {
|
|
494
|
+
const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
|
|
495
|
+
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
async function committedStageId(args) {
|
|
500
|
+
return (await args.client.getDocument(args.instance._id))?.currentStageId;
|
|
501
|
+
}
|
|
502
|
+
async function deployStageGuards(args) {
|
|
503
|
+
if (await committedStageId(args) === args.stageId)
|
|
504
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
505
|
+
await upsertGuard(client, doc);
|
|
506
|
+
}
|
|
507
|
+
async function retractStageGuards(args) {
|
|
508
|
+
const live = await committedStageId(args);
|
|
509
|
+
if (!(live === void 0 || live === args.stageId))
|
|
510
|
+
for (const { client, doc } of resolvedStageGuards(args))
|
|
511
|
+
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
512
|
+
}
|
|
513
|
+
function randomKey(length = 12) {
|
|
514
|
+
const bytes = new Uint8Array(length);
|
|
515
|
+
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
516
|
+
}
|
|
517
|
+
function buildSnapshot(args) {
|
|
518
|
+
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const { doc, resource } of args.docs) {
|
|
520
|
+
const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
|
|
521
|
+
docs.push(restamped), knownIds.add(uri);
|
|
522
|
+
}
|
|
523
|
+
return { docs, knownIds };
|
|
524
|
+
}
|
|
525
|
+
function restampForSnapshot(doc, gdrUri2, resource) {
|
|
526
|
+
return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
|
|
527
|
+
}
|
|
528
|
+
function rewriteRefsRecursive(value, resource) {
|
|
529
|
+
if (Array.isArray(value))
|
|
530
|
+
return value.map((v2) => rewriteRefsRecursive(v2, resource));
|
|
531
|
+
if (value === null || typeof value != "object") return value;
|
|
532
|
+
const obj = value, out = {};
|
|
533
|
+
for (const [k, v2] of Object.entries(obj))
|
|
534
|
+
k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
function findOpenStageEntry(host) {
|
|
538
|
+
return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
|
|
539
|
+
}
|
|
540
|
+
function collectWatchRefs(instance) {
|
|
541
|
+
const stage = findOpenStageEntry(instance);
|
|
542
|
+
return [
|
|
543
|
+
gdrRef(instance.workflowResource, instance._id, instance._type),
|
|
544
|
+
...instance.ancestors,
|
|
545
|
+
...slotDocRefs(instance.state),
|
|
546
|
+
...slotDocRefs(stage?.state),
|
|
547
|
+
...slotReleaseRefs(instance.state),
|
|
548
|
+
...slotReleaseRefs(stage?.state)
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
function readsRaw(ref) {
|
|
552
|
+
return ref.type === "workflow.instance" || ref.type === "system.release";
|
|
553
|
+
}
|
|
554
|
+
function contentReleaseName(args) {
|
|
555
|
+
const { ref, perspective } = args;
|
|
556
|
+
if (!readsRaw(ref) && Array.isArray(perspective))
|
|
557
|
+
return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
|
|
558
|
+
}
|
|
559
|
+
function subscriptionDocumentsForInstance(instance) {
|
|
560
|
+
const seen = /* @__PURE__ */ new Set(), documents = [];
|
|
561
|
+
for (const ref of collectWatchRefs(instance))
|
|
562
|
+
!isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push({ ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type }));
|
|
563
|
+
return {
|
|
564
|
+
documents,
|
|
565
|
+
...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function slotDocRefs(slots) {
|
|
569
|
+
return slotEntries(slots).flatMap((slot) => slot._type === "workflow.state.doc.ref" ? isGdr(slot.value) ? [slot.value] : [] : slot._type === "workflow.state.doc.refs" ? Array.isArray(slot.value) ? slot.value.filter(isGdr) : [] : []);
|
|
570
|
+
}
|
|
571
|
+
function slotReleaseRefs(slots) {
|
|
572
|
+
return slotEntries(slots).flatMap(
|
|
573
|
+
(slot) => slot._type === "workflow.state.release.ref" && isGdr(slot.value) ? [slot.value] : []
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
function slotEntries(slots) {
|
|
577
|
+
return Array.isArray(slots) ? slots.filter(
|
|
578
|
+
(slot) => !!slot && typeof slot == "object"
|
|
579
|
+
) : [];
|
|
580
|
+
}
|
|
581
|
+
async function hydrateSnapshot(args) {
|
|
582
|
+
const { client, clientForGdr, instance, overlay } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri, perspective) => {
|
|
583
|
+
if (visited.has(uri)) return;
|
|
584
|
+
const held = overlay?.get(uri);
|
|
585
|
+
if (held !== void 0) {
|
|
586
|
+
loaded.push(held), visited.add(uri);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const fetched = await loadByGdr(
|
|
590
|
+
client,
|
|
591
|
+
clientForGdr,
|
|
592
|
+
instance.workflowResource,
|
|
593
|
+
uri,
|
|
594
|
+
perspective
|
|
595
|
+
);
|
|
596
|
+
fetched && (loaded.push(fetched), visited.add(uri));
|
|
597
|
+
};
|
|
598
|
+
loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
|
|
599
|
+
for (const ref of collectWatchRefs(instance))
|
|
600
|
+
await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
|
|
601
|
+
return buildSnapshot({ docs: loaded });
|
|
602
|
+
}
|
|
603
|
+
async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
|
|
604
|
+
let parsed;
|
|
605
|
+
try {
|
|
606
|
+
parsed = parseGdr(uri);
|
|
607
|
+
} catch {
|
|
608
|
+
const doc2 = await readDoc(defaultClient, uri, perspective);
|
|
609
|
+
return doc2 ? { doc: doc2, resource: defaultResource } : null;
|
|
610
|
+
}
|
|
611
|
+
const routed = clientForGdr(parsed), doc = await readDoc(routed, parsed.documentId, perspective);
|
|
612
|
+
return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
|
|
613
|
+
}
|
|
614
|
+
async function readDoc(client, id, perspective) {
|
|
615
|
+
return perspective === void 0 ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
|
|
616
|
+
}
|
|
617
|
+
function resourceFromParsed(parsed) {
|
|
618
|
+
return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
|
|
619
|
+
}
|
|
620
|
+
function collectSlotDocUris(resolvedStateSlots) {
|
|
621
|
+
return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
|
|
622
|
+
}
|
|
623
|
+
const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
|
|
624
|
+
class SlotValueShapeError extends Error {
|
|
625
|
+
slotType;
|
|
626
|
+
slotId;
|
|
627
|
+
issues;
|
|
628
|
+
constructor(args) {
|
|
629
|
+
const issueText = args.issues.join("; ");
|
|
630
|
+
super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const GdrShape = v__namespace.looseObject({
|
|
634
|
+
id: v__namespace.pipe(
|
|
635
|
+
v__namespace.string(),
|
|
636
|
+
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
637
|
+
),
|
|
638
|
+
type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
639
|
+
}), ReleaseRefShape = v__namespace.looseObject({
|
|
640
|
+
id: v__namespace.pipe(
|
|
641
|
+
v__namespace.string(),
|
|
642
|
+
v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
|
|
643
|
+
),
|
|
644
|
+
type: v__namespace.literal("system.release"),
|
|
645
|
+
releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
|
|
646
|
+
}), ActorShape = v__namespace.looseObject({
|
|
647
|
+
kind: v__namespace.picklist(["user", "ai", "system"]),
|
|
648
|
+
id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
649
|
+
roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
|
|
650
|
+
onBehalfOf: v__namespace.optional(v__namespace.string())
|
|
651
|
+
}), AssigneeShape = v__namespace.union([
|
|
652
|
+
v__namespace.looseObject({ kind: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
|
|
653
|
+
v__namespace.looseObject({ kind: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
|
|
654
|
+
]), ChecklistItemShape = v__namespace.looseObject({
|
|
655
|
+
label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
|
|
656
|
+
done: v__namespace.boolean(),
|
|
657
|
+
doneBy: v__namespace.optional(v__namespace.string()),
|
|
658
|
+
doneAt: v__namespace.optional(v__namespace.string()),
|
|
659
|
+
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
660
|
+
}), NoteItemShape = v__namespace.pipe(
|
|
661
|
+
v__namespace.looseObject({
|
|
662
|
+
_key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
|
|
663
|
+
}),
|
|
664
|
+
v__namespace.check(
|
|
665
|
+
(val) => typeof val == "object" && val !== null && !Array.isArray(val),
|
|
666
|
+
"must be an object"
|
|
667
|
+
)
|
|
668
|
+
), NullableString = v__namespace.union([v__namespace.null(), v__namespace.string()]), NullableNumber = v__namespace.union([v__namespace.null(), v__namespace.number()]), NullableBoolean = v__namespace.union([v__namespace.null(), v__namespace.boolean()]), NullableDateTime = v__namespace.union([
|
|
669
|
+
v__namespace.null(),
|
|
670
|
+
v__namespace.pipe(
|
|
671
|
+
v__namespace.string(),
|
|
672
|
+
v__namespace.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
|
|
673
|
+
)
|
|
674
|
+
]), NullableUrl = NullableString, valueSchemas = {
|
|
675
|
+
"workflow.state.doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
|
|
676
|
+
"workflow.state.doc.refs": v__namespace.array(GdrShape),
|
|
677
|
+
"workflow.state.release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
|
|
678
|
+
"workflow.state.query": v__namespace.any(),
|
|
679
|
+
"workflow.state.value.string": NullableString,
|
|
680
|
+
"workflow.state.value.url": NullableUrl,
|
|
681
|
+
"workflow.state.value.number": NullableNumber,
|
|
682
|
+
"workflow.state.value.boolean": NullableBoolean,
|
|
683
|
+
"workflow.state.value.dateTime": NullableDateTime,
|
|
684
|
+
"workflow.state.value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
|
|
685
|
+
"workflow.state.checklist": v__namespace.array(ChecklistItemShape),
|
|
686
|
+
"workflow.state.notes": v__namespace.array(NoteItemShape),
|
|
687
|
+
"workflow.state.assignees": v__namespace.array(AssigneeShape)
|
|
688
|
+
}, itemSchemas = {
|
|
689
|
+
"workflow.state.doc.refs": GdrShape,
|
|
690
|
+
"workflow.state.checklist": ChecklistItemShape,
|
|
691
|
+
"workflow.state.notes": NoteItemShape,
|
|
692
|
+
"workflow.state.assignees": AssigneeShape
|
|
693
|
+
};
|
|
694
|
+
function isAppendable(slotType) {
|
|
695
|
+
return slotType in itemSchemas;
|
|
696
|
+
}
|
|
697
|
+
function validateSlotValue(args) {
|
|
698
|
+
const schema2 = valueSchemas[args.slotType];
|
|
699
|
+
if (schema2 === void 0)
|
|
700
|
+
throw new SlotValueShapeError({
|
|
701
|
+
slotType: args.slotType,
|
|
702
|
+
slotId: args.slotId,
|
|
703
|
+
issues: [`unknown slot type ${args.slotType}`],
|
|
704
|
+
mode: "value"
|
|
705
|
+
});
|
|
706
|
+
const result = v__namespace.safeParse(schema2, args.value);
|
|
707
|
+
if (!result.success)
|
|
708
|
+
throw new SlotValueShapeError({
|
|
709
|
+
slotType: args.slotType,
|
|
710
|
+
slotId: args.slotId,
|
|
711
|
+
issues: formatIssues(result.issues),
|
|
712
|
+
mode: "value"
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function validateSlotAppendItem(args) {
|
|
716
|
+
if (!isAppendable(args.slotType))
|
|
717
|
+
throw new SlotValueShapeError({
|
|
718
|
+
slotType: args.slotType,
|
|
719
|
+
slotId: args.slotId,
|
|
720
|
+
issues: [`slot type ${args.slotType} does not support append`],
|
|
721
|
+
mode: "item"
|
|
722
|
+
});
|
|
723
|
+
const schema2 = itemSchemas[args.slotType], result = v__namespace.safeParse(schema2, args.item);
|
|
724
|
+
if (!result.success)
|
|
725
|
+
throw new SlotValueShapeError({
|
|
726
|
+
slotType: args.slotType,
|
|
727
|
+
slotId: args.slotId,
|
|
728
|
+
issues: formatIssues(result.issues),
|
|
729
|
+
mode: "item"
|
|
730
|
+
});
|
|
415
731
|
}
|
|
416
732
|
function formatIssues(issues) {
|
|
417
|
-
return issues.map((i) =>
|
|
733
|
+
return issues.map((i) => {
|
|
734
|
+
const keys = i.path?.map((p) => p.key) ?? [];
|
|
735
|
+
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
|
|
736
|
+
});
|
|
418
737
|
}
|
|
419
738
|
function derivePerspectiveFromState(slots) {
|
|
420
739
|
if (slots !== void 0) {
|
|
@@ -459,7 +778,7 @@ async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
|
|
|
459
778
|
state: stateMapFromResolved(ctx.resolvedState ?? []),
|
|
460
779
|
stage: ctx.stageId ?? null,
|
|
461
780
|
task: ctx.taskId ?? null,
|
|
462
|
-
now:
|
|
781
|
+
now: ctx.now,
|
|
463
782
|
engineTags: ctx.tags ?? []
|
|
464
783
|
});
|
|
465
784
|
try {
|
|
@@ -476,7 +795,7 @@ async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
|
|
|
476
795
|
const source = slot.source;
|
|
477
796
|
return source.kind === "init" ? resolveInitValue(slot, initialState, defaultValue) : source.kind === "literal" ? source.value ?? defaultValue : source.kind === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.kind === "query" && ctx.client !== void 0 ? resolveQueryValue(slot, source, ctx, ctx.client, defaultValue) : defaultValue;
|
|
478
797
|
}
|
|
479
|
-
function buildResolvedSlot(slot, value, _key) {
|
|
798
|
+
function buildResolvedSlot(slot, value, _key, now) {
|
|
480
799
|
const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
|
|
481
800
|
return slot.type === "workflow.state.query" ? {
|
|
482
801
|
_key,
|
|
@@ -485,7 +804,7 @@ function buildResolvedSlot(slot, value, _key) {
|
|
|
485
804
|
...titleProp,
|
|
486
805
|
...descriptionProp,
|
|
487
806
|
value,
|
|
488
|
-
resolvedAt:
|
|
807
|
+
resolvedAt: now
|
|
489
808
|
} : {
|
|
490
809
|
_key,
|
|
491
810
|
_type: slot.type,
|
|
@@ -497,7 +816,7 @@ function buildResolvedSlot(slot, value, _key) {
|
|
|
497
816
|
}
|
|
498
817
|
async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
|
|
499
818
|
const defaultValue = defaultSlotValue(slot.type), value = await resolveSlotValue(slot, initialState, ctx, defaultValue);
|
|
500
|
-
return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2());
|
|
819
|
+
return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2(), ctx.now);
|
|
501
820
|
}
|
|
502
821
|
function stateMapFromResolved(slots) {
|
|
503
822
|
const out = {};
|
|
@@ -511,10 +830,10 @@ function assertInitValueShape(slot, value) {
|
|
|
511
830
|
}
|
|
512
831
|
if (slot.type === "workflow.state.release.ref") {
|
|
513
832
|
assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
|
|
514
|
-
const
|
|
515
|
-
if (typeof
|
|
833
|
+
const v2 = value;
|
|
834
|
+
if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
|
|
516
835
|
throw new Error(
|
|
517
|
-
`Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(
|
|
836
|
+
`Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
|
|
518
837
|
);
|
|
519
838
|
return;
|
|
520
839
|
}
|
|
@@ -532,18 +851,18 @@ function assertGdrShape(value, context) {
|
|
|
532
851
|
throw new Error(
|
|
533
852
|
`Invalid GDR for ${context}: expected { id: "<scheme>:...", type: "<schema>" }, got ${typeof value}.`
|
|
534
853
|
);
|
|
535
|
-
const
|
|
536
|
-
if (typeof
|
|
854
|
+
const v2 = value;
|
|
855
|
+
if (typeof v2.id != "string" || !isGdrUri(v2.id))
|
|
537
856
|
throw new Error(
|
|
538
|
-
`Invalid GDR for ${context}: \`id\` must be a GDR URI ("<scheme>:<...id-parts>" with scheme dataset|canvas|media-library|dashboard). Got ${JSON.stringify(
|
|
857
|
+
`Invalid GDR for ${context}: \`id\` must be a GDR URI ("<scheme>:<...id-parts>" with scheme dataset|canvas|media-library|dashboard). Got ${JSON.stringify(v2.id)}. Construct via \`gdrFromResource\` / \`refDataset\` / \`refCanvas\` etc. \u2014 bare document ids are not accepted.`
|
|
539
858
|
);
|
|
540
|
-
if (typeof
|
|
859
|
+
if (typeof v2.type != "string" || v2.type.length === 0)
|
|
541
860
|
throw new Error(
|
|
542
|
-
`Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(
|
|
861
|
+
`Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
|
|
543
862
|
);
|
|
544
863
|
}
|
|
545
864
|
function normalizeQueryResult(slotType, raw, workflowResource) {
|
|
546
|
-
return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((
|
|
865
|
+
return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v2) => v2 !== null) : [] : raw;
|
|
547
866
|
}
|
|
548
867
|
function toGdrUri(docId, workflowResource) {
|
|
549
868
|
return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
|
|
@@ -568,25 +887,32 @@ async function loadContext(client, instanceId, options) {
|
|
|
568
887
|
const instance = await client.getDocument(instanceId);
|
|
569
888
|
if (!instance)
|
|
570
889
|
throw new Error(`Workflow instance ${instanceId} not found`);
|
|
571
|
-
const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client),
|
|
572
|
-
|
|
890
|
+
const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
|
|
891
|
+
client,
|
|
892
|
+
clientForGdr,
|
|
893
|
+
instance,
|
|
894
|
+
...options?.overlay !== void 0 ? { overlay: options.overlay } : {}
|
|
895
|
+
});
|
|
896
|
+
return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
|
|
573
897
|
}
|
|
574
898
|
function ctxEvaluateFilter(ctx, filter) {
|
|
575
899
|
return evaluateFilter({
|
|
576
900
|
filter,
|
|
577
901
|
definition: ctx.definition,
|
|
578
902
|
snapshot: ctx.snapshot,
|
|
579
|
-
params: buildParams(ctx.instance)
|
|
903
|
+
params: buildParams(ctx.instance, ctx.now)
|
|
580
904
|
});
|
|
581
905
|
}
|
|
582
906
|
async function buildEngineContext(args) {
|
|
583
|
-
const { client, clientForGdr, instance, definition } = args;
|
|
907
|
+
const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
|
|
584
908
|
return {
|
|
585
909
|
client,
|
|
586
910
|
clientForGdr,
|
|
911
|
+
clock,
|
|
912
|
+
now: clock(),
|
|
587
913
|
instance,
|
|
588
914
|
definition,
|
|
589
|
-
snapshot: await hydrateSnapshot({ client, clientForGdr, instance
|
|
915
|
+
snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
|
|
590
916
|
};
|
|
591
917
|
}
|
|
592
918
|
function findStage(definition, stageId) {
|
|
@@ -596,12 +922,13 @@ function findStage(definition, stageId) {
|
|
|
596
922
|
return stage;
|
|
597
923
|
}
|
|
598
924
|
async function resolveStageStateSlots(args) {
|
|
599
|
-
const { client, instance, stage, initialState } = args;
|
|
925
|
+
const { client, instance, stage, now, initialState } = args;
|
|
600
926
|
return resolveDeclaredState({
|
|
601
927
|
slotDefs: stage.state,
|
|
602
928
|
initialState: initialState ?? [],
|
|
603
929
|
ctx: {
|
|
604
930
|
client,
|
|
931
|
+
now,
|
|
605
932
|
selfId: instance._id,
|
|
606
933
|
tags: instance.tags ?? [],
|
|
607
934
|
stageId: stage.id,
|
|
@@ -616,12 +943,13 @@ async function resolveStageStateSlots(args) {
|
|
|
616
943
|
});
|
|
617
944
|
}
|
|
618
945
|
async function resolveTaskStateSlots(args) {
|
|
619
|
-
const { client, instance, stage, task } = args;
|
|
946
|
+
const { client, instance, stage, task, now } = args;
|
|
620
947
|
return resolveDeclaredState({
|
|
621
948
|
slotDefs: task.state,
|
|
622
949
|
initialState: [],
|
|
623
950
|
ctx: {
|
|
624
951
|
client,
|
|
952
|
+
now,
|
|
625
953
|
selfId: instance._id,
|
|
626
954
|
tags: instance.tags ?? [],
|
|
627
955
|
stageId: stage.id,
|
|
@@ -633,73 +961,6 @@ async function resolveTaskStateSlots(args) {
|
|
|
633
961
|
randomKey
|
|
634
962
|
});
|
|
635
963
|
}
|
|
636
|
-
function readStateSlot(instance, scope, slotId, stageId, taskId) {
|
|
637
|
-
if (scope === "workflow")
|
|
638
|
-
return slotValue$1(instance.state?.find((s) => s.id === slotId));
|
|
639
|
-
const stageEntry = instance.stages.find(
|
|
640
|
-
(s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
|
|
641
|
-
);
|
|
642
|
-
if (stageEntry === void 0) return;
|
|
643
|
-
if (scope === "stage")
|
|
644
|
-
return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
|
|
645
|
-
const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
|
|
646
|
-
return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
|
|
647
|
-
}
|
|
648
|
-
function slotValue$1(slot) {
|
|
649
|
-
if (slot != null && typeof slot == "object")
|
|
650
|
-
return slot.value;
|
|
651
|
-
}
|
|
652
|
-
function resolveSource(src, ctx) {
|
|
653
|
-
switch (src.source) {
|
|
654
|
-
case "literal":
|
|
655
|
-
return src.value;
|
|
656
|
-
case "param":
|
|
657
|
-
return ctx.params?.[src.paramId];
|
|
658
|
-
case "actor":
|
|
659
|
-
return ctx.actor;
|
|
660
|
-
case "now":
|
|
661
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
662
|
-
case "self":
|
|
663
|
-
return ctx.instance._id;
|
|
664
|
-
case "stageId":
|
|
665
|
-
return ctx.stageId ?? ctx.instance.currentStageId;
|
|
666
|
-
case "stateRead":
|
|
667
|
-
return resolveStateRead(src, ctx);
|
|
668
|
-
case "effectOutput":
|
|
669
|
-
return resolveEffectOutput(src, ctx);
|
|
670
|
-
case "object":
|
|
671
|
-
return resolveObject(src, ctx);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
function resolveStateRead(src, ctx) {
|
|
675
|
-
const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
|
|
676
|
-
return src.path !== void 0 ? getPath(value, src.path) : value;
|
|
677
|
-
}
|
|
678
|
-
function resolveEffectOutput(src, ctx) {
|
|
679
|
-
const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
|
|
680
|
-
if (entry === void 0) return null;
|
|
681
|
-
const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
|
|
682
|
-
return value === void 0 ? null : value;
|
|
683
|
-
}
|
|
684
|
-
function resolveObject(src, ctx) {
|
|
685
|
-
const out = {};
|
|
686
|
-
for (const [field, fieldSrc] of Object.entries(src.fields))
|
|
687
|
-
out[field] = resolveSource(fieldSrc, ctx);
|
|
688
|
-
return out;
|
|
689
|
-
}
|
|
690
|
-
async function resolveBindings(args) {
|
|
691
|
-
const { bindings, staticInput, instance, params, actor } = args, resolved = {};
|
|
692
|
-
if (bindings) {
|
|
693
|
-
const ctx = {
|
|
694
|
-
instance,
|
|
695
|
-
...params !== void 0 ? { params } : {},
|
|
696
|
-
...actor !== void 0 ? { actor } : {}
|
|
697
|
-
};
|
|
698
|
-
for (const [key, src] of Object.entries(bindings))
|
|
699
|
-
resolved[key] = resolveSource(src, ctx);
|
|
700
|
-
}
|
|
701
|
-
return { ...resolved, ...staticInput };
|
|
702
|
-
}
|
|
703
964
|
function effectsContextEntry(id, value) {
|
|
704
965
|
const _key = randomKey();
|
|
705
966
|
return typeof value == "string" ? { _key, _type: "effectsContext.string", id, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", id, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", id, value } : { _key, _type: "effectsContext.ref", id, value };
|
|
@@ -708,7 +969,7 @@ function buildInstanceBase(args) {
|
|
|
708
969
|
const { id, now, actor, perspective } = args;
|
|
709
970
|
return {
|
|
710
971
|
_id: id,
|
|
711
|
-
_type:
|
|
972
|
+
_type: WORKFLOW_INSTANCE_TYPE,
|
|
712
973
|
_rev: "",
|
|
713
974
|
_createdAt: now,
|
|
714
975
|
_updatedAt: now,
|
|
@@ -738,175 +999,337 @@ function buildInstanceBase(args) {
|
|
|
738
999
|
lastChangedAt: now
|
|
739
1000
|
};
|
|
740
1001
|
}
|
|
741
|
-
function
|
|
742
|
-
return `temp.system.guard.${args.instanceDocId}.${args.stageId}.${args.index}`;
|
|
743
|
-
}
|
|
744
|
-
function compileGuard(args) {
|
|
1002
|
+
function startMutation(instance) {
|
|
745
1003
|
return {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1004
|
+
currentStageId: instance.currentStageId,
|
|
1005
|
+
// Deep-ish copy. Per-scope state slot arrays need their own copies
|
|
1006
|
+
// so ops can mutate without leaking into the source.
|
|
1007
|
+
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
1008
|
+
stages: instance.stages.map((s) => ({
|
|
1009
|
+
...s,
|
|
1010
|
+
state: (s.state ?? []).map((slot) => ({ ...slot })),
|
|
1011
|
+
tasks: s.tasks.map((t) => ({
|
|
1012
|
+
...t,
|
|
1013
|
+
...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
|
|
1014
|
+
}))
|
|
1015
|
+
})),
|
|
1016
|
+
pendingEffects: [...instance.pendingEffects],
|
|
1017
|
+
effectHistory: [...instance.effectHistory],
|
|
1018
|
+
effectsContext: [...instance.effectsContext],
|
|
1019
|
+
history: [...instance.history],
|
|
1020
|
+
lastChangedAt: instance.lastChangedAt,
|
|
1021
|
+
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
1022
|
+
pendingCreates: []
|
|
756
1023
|
};
|
|
757
1024
|
}
|
|
758
|
-
function
|
|
759
|
-
return
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1025
|
+
function taskStatusChangedEntry(args) {
|
|
1026
|
+
return {
|
|
1027
|
+
_key: randomKey(),
|
|
1028
|
+
_type: "workflow.history.taskStatusChanged",
|
|
1029
|
+
at: args.at,
|
|
1030
|
+
stageId: args.stageId,
|
|
1031
|
+
taskId: args.taskId,
|
|
1032
|
+
from: args.from,
|
|
1033
|
+
to: args.to,
|
|
1034
|
+
...args.actor !== void 0 ? { actor: args.actor } : {}
|
|
1035
|
+
};
|
|
765
1036
|
}
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
return
|
|
1037
|
+
function stageTransitionHistory(args) {
|
|
1038
|
+
const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
|
|
1039
|
+
at,
|
|
1040
|
+
...transitionId !== void 0 ? { transitionId } : {},
|
|
1041
|
+
...actor !== void 0 ? { actor } : {},
|
|
1042
|
+
...via !== void 0 ? { via } : {},
|
|
1043
|
+
...reason !== void 0 ? { reason } : {}
|
|
1044
|
+
};
|
|
1045
|
+
return [
|
|
1046
|
+
{
|
|
1047
|
+
_key: randomKey(),
|
|
1048
|
+
_type: "workflow.history.stageExited",
|
|
1049
|
+
stageId: fromStageId,
|
|
1050
|
+
toStageId,
|
|
1051
|
+
...shared
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
_key: randomKey(),
|
|
1055
|
+
_type: "workflow.history.transitionFired",
|
|
1056
|
+
fromStageId,
|
|
1057
|
+
toStageId,
|
|
1058
|
+
...shared
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
_key: randomKey(),
|
|
1062
|
+
_type: "workflow.history.stageEntered",
|
|
1063
|
+
stageId: toStageId,
|
|
1064
|
+
fromStageId,
|
|
1065
|
+
...shared
|
|
1066
|
+
}
|
|
1067
|
+
];
|
|
775
1068
|
}
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
})).get() === !0;
|
|
788
|
-
} catch {
|
|
789
|
-
return !1;
|
|
790
|
-
}
|
|
1069
|
+
function instanceStateFields(src) {
|
|
1070
|
+
const fields = {
|
|
1071
|
+
currentStageId: src.currentStageId,
|
|
1072
|
+
state: src.state,
|
|
1073
|
+
stages: src.stages,
|
|
1074
|
+
pendingEffects: src.pendingEffects,
|
|
1075
|
+
effectHistory: src.effectHistory,
|
|
1076
|
+
effectsContext: src.effectsContext,
|
|
1077
|
+
history: src.history
|
|
1078
|
+
};
|
|
1079
|
+
return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
|
|
791
1080
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1081
|
+
function materializeInstance(base, mutation) {
|
|
1082
|
+
return {
|
|
1083
|
+
...base,
|
|
1084
|
+
currentStageId: mutation.currentStageId,
|
|
1085
|
+
state: mutation.state,
|
|
1086
|
+
stages: mutation.stages
|
|
1087
|
+
};
|
|
797
1088
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
1089
|
+
async function persist(ctx, mutation) {
|
|
1090
|
+
const set = {
|
|
1091
|
+
...instanceStateFields(mutation),
|
|
1092
|
+
lastChangedAt: ctx.now
|
|
1093
|
+
}, pendingCreates = mutation.pendingCreates;
|
|
1094
|
+
if (pendingCreates.length === 0)
|
|
1095
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1096
|
+
const tx = ctx.client.transaction();
|
|
1097
|
+
for (const body of pendingCreates) tx.create(body);
|
|
1098
|
+
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1099
|
+
const actorForPriming = void 0;
|
|
1100
|
+
for (const body of pendingCreates)
|
|
1101
|
+
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
|
|
1102
|
+
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1103
|
+
if (!reloaded)
|
|
1104
|
+
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
1105
|
+
return reloaded;
|
|
800
1106
|
}
|
|
801
|
-
function
|
|
802
|
-
const
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return typeof v.type == "string" ? { parsed: parseGdr(v.id), type: v.type } : { parsed: parseGdr(v.id) };
|
|
809
|
-
}
|
|
810
|
-
return null;
|
|
1107
|
+
function currentStageEntry(mutation) {
|
|
1108
|
+
const entry = findOpenStageEntry(mutation);
|
|
1109
|
+
if (entry === void 0)
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
|
|
1112
|
+
);
|
|
1113
|
+
return entry;
|
|
811
1114
|
}
|
|
812
|
-
function
|
|
813
|
-
|
|
814
|
-
for (const src of idRefs ?? []) {
|
|
815
|
-
const target = resolveIdRefTarget(src, ctx);
|
|
816
|
-
if (target === null) return null;
|
|
817
|
-
targets.push(target);
|
|
818
|
-
}
|
|
819
|
-
return targets.length === 0 ? null : targets;
|
|
1115
|
+
function currentTasks(mutation) {
|
|
1116
|
+
return currentStageEntry(mutation).tasks;
|
|
820
1117
|
}
|
|
821
|
-
function
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
return resource;
|
|
1118
|
+
function findTaskInCurrentStage(ctx, taskId) {
|
|
1119
|
+
const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
|
|
1120
|
+
if (task === void 0)
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
|
|
1123
|
+
);
|
|
1124
|
+
return { stage, task };
|
|
831
1125
|
}
|
|
832
|
-
function
|
|
833
|
-
|
|
1126
|
+
function requireMutationTaskEntry(mutation, taskId) {
|
|
1127
|
+
const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
|
|
1128
|
+
if (mutEntry === void 0)
|
|
1129
|
+
throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
|
|
1130
|
+
return mutEntry;
|
|
834
1131
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
|
|
838
|
-
return inferred.length > 0 ? inferred : void 0;
|
|
1132
|
+
function findCurrentStageEntry(instance) {
|
|
1133
|
+
return findOpenStageEntry(instance);
|
|
839
1134
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
for (const [k, src] of Object.entries(metadata ?? {}))
|
|
843
|
-
out[k] = resolveSource(src, ctx);
|
|
844
|
-
return out;
|
|
1135
|
+
function findCurrentTasks(instance) {
|
|
1136
|
+
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
845
1137
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
idRefs: bareIdRefs(targets),
|
|
861
|
-
...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
|
|
862
|
-
actions: guard.match.actions
|
|
863
|
-
},
|
|
864
|
-
predicate: guard.predicate ?? "",
|
|
865
|
-
metadata: resolveMetadata(guard.metadata, ctx)
|
|
866
|
-
}), routeGdr: targets[0].parsed };
|
|
1138
|
+
async function completeEffect(args) {
|
|
1139
|
+
const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1140
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
1141
|
+
});
|
|
1142
|
+
return commitCompleteEffect(
|
|
1143
|
+
ctx,
|
|
1144
|
+
effectKey,
|
|
1145
|
+
status,
|
|
1146
|
+
outputs,
|
|
1147
|
+
detail,
|
|
1148
|
+
error,
|
|
1149
|
+
durationMs,
|
|
1150
|
+
options?.actor
|
|
1151
|
+
);
|
|
867
1152
|
}
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1153
|
+
function buildEffectHistoryEntry(pending, outcome) {
|
|
1154
|
+
const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
|
|
1155
|
+
return {
|
|
1156
|
+
_key: pending._key,
|
|
1157
|
+
effectId: pending.effectId,
|
|
1158
|
+
...pending.title !== void 0 ? { title: pending.title } : {},
|
|
1159
|
+
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
1160
|
+
params: pending.params,
|
|
1161
|
+
origin: pending.origin,
|
|
1162
|
+
...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
|
|
1163
|
+
ranAt,
|
|
1164
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
1165
|
+
status,
|
|
1166
|
+
...detail !== void 0 ? { detail } : {},
|
|
1167
|
+
...error !== void 0 ? { error } : {},
|
|
1168
|
+
...outputs !== void 0 ? { outputs } : {}
|
|
876
1169
|
};
|
|
877
|
-
doc.name !== void 0 && (set.name = doc.name), doc.description !== void 0 && (set.description = doc.description), existing ? await client.patch(doc._id).set(set).commit() : await client.create(doc);
|
|
878
1170
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
|
|
1171
|
+
function upsertEffectsContext(mutation, outputs) {
|
|
1172
|
+
for (const [id, value] of Object.entries(outputs)) {
|
|
1173
|
+
const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
|
|
1174
|
+
existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
|
|
884
1175
|
}
|
|
885
|
-
return out;
|
|
886
1176
|
}
|
|
887
|
-
async function
|
|
888
|
-
|
|
889
|
-
|
|
1177
|
+
async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
|
|
1178
|
+
const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
|
|
1179
|
+
if (pending === void 0)
|
|
1180
|
+
throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
|
|
1181
|
+
const mutation = startMutation(ctx.instance);
|
|
1182
|
+
mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
|
|
1183
|
+
const ranAt = ctx.now;
|
|
1184
|
+
return mutation.effectHistory.push(
|
|
1185
|
+
buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
|
|
1186
|
+
), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
|
|
1187
|
+
_key: randomKey(),
|
|
1188
|
+
_type: "workflow.history.effectCompleted",
|
|
1189
|
+
at: ranAt,
|
|
1190
|
+
effectKey,
|
|
1191
|
+
effectId: pending.effectId,
|
|
1192
|
+
status,
|
|
1193
|
+
...outputs !== void 0 ? { outputs } : {},
|
|
1194
|
+
...detail !== void 0 ? { detail } : {},
|
|
1195
|
+
...actor !== void 0 ? { actor } : {}
|
|
1196
|
+
}), await persist(ctx, mutation), { effectKey, status };
|
|
890
1197
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1198
|
+
function buildQueuedEffect(effect, origin, params, actor, now) {
|
|
1199
|
+
const key = randomKey(), pending = {
|
|
1200
|
+
_key: key,
|
|
1201
|
+
_type: "workflow.pendingEffect",
|
|
1202
|
+
effectId: effect.id,
|
|
1203
|
+
...effect.title !== void 0 ? { title: effect.title } : {},
|
|
1204
|
+
...effect.description !== void 0 ? { description: effect.description } : {},
|
|
1205
|
+
...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
|
|
1206
|
+
params,
|
|
1207
|
+
origin,
|
|
1208
|
+
...actor !== void 0 ? { actor } : {},
|
|
1209
|
+
queuedAt: now
|
|
1210
|
+
}, history = {
|
|
1211
|
+
_key: randomKey(),
|
|
1212
|
+
_type: "workflow.history.effectQueued",
|
|
1213
|
+
at: now,
|
|
1214
|
+
effectKey: key,
|
|
1215
|
+
effectId: effect.id,
|
|
1216
|
+
origin
|
|
1217
|
+
};
|
|
1218
|
+
return { pending, history };
|
|
894
1219
|
}
|
|
895
|
-
async function
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1220
|
+
async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
|
|
1221
|
+
if (!effects) return;
|
|
1222
|
+
const now = ctx.now, liveInstance = {
|
|
1223
|
+
...ctx.instance,
|
|
1224
|
+
state: mutation.state,
|
|
1225
|
+
stages: mutation.stages,
|
|
1226
|
+
effectsContext: mutation.effectsContext
|
|
1227
|
+
};
|
|
1228
|
+
for (const effect of effects) {
|
|
1229
|
+
const params = await resolveBindings({
|
|
1230
|
+
bindings: effect.bindings,
|
|
1231
|
+
staticInput: effect.input,
|
|
1232
|
+
instance: liveInstance,
|
|
1233
|
+
now,
|
|
1234
|
+
...callerParams !== void 0 ? { params: callerParams } : {},
|
|
1235
|
+
...actor !== void 0 ? { actor } : {}
|
|
1236
|
+
}), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
|
|
1237
|
+
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
class ActionParamsInvalidError extends Error {
|
|
1241
|
+
action;
|
|
1242
|
+
taskId;
|
|
1243
|
+
issues;
|
|
1244
|
+
constructor(args) {
|
|
1245
|
+
const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
|
|
1246
|
+
`);
|
|
1247
|
+
super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
|
|
1248
|
+
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
class WorkflowStateDivergedError extends Error {
|
|
1252
|
+
instanceId;
|
|
1253
|
+
/** The guard-deploy failure that triggered the (attempted) rollback. */
|
|
1254
|
+
guardError;
|
|
1255
|
+
/** The rollback failure, when rollback was attempted and itself failed. */
|
|
1256
|
+
rollbackError;
|
|
1257
|
+
constructor(args) {
|
|
1258
|
+
super(
|
|
1259
|
+
`Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
|
|
1260
|
+
{ cause: args.guardError }
|
|
1261
|
+
), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
|
|
1265
|
+
class ConcurrentFireActionError extends Error {
|
|
1266
|
+
instanceId;
|
|
1267
|
+
taskId;
|
|
1268
|
+
action;
|
|
1269
|
+
attempts;
|
|
1270
|
+
constructor(args) {
|
|
1271
|
+
super(
|
|
1272
|
+
`Action "${args.action}" on task "${args.taskId}" (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.`
|
|
1273
|
+
), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.taskId = args.taskId, this.action = args.action, this.attempts = args.attempts;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function isRevisionConflict(error) {
|
|
1277
|
+
if (typeof error != "object" || error === null) return !1;
|
|
1278
|
+
const { statusCode, message } = error;
|
|
1279
|
+
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
1280
|
+
}
|
|
1281
|
+
class CascadeLimitError extends Error {
|
|
1282
|
+
instanceId;
|
|
1283
|
+
limit;
|
|
1284
|
+
constructor(args) {
|
|
1285
|
+
super(
|
|
1286
|
+
`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.`
|
|
1287
|
+
), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async function deployOrRollback(args) {
|
|
1291
|
+
try {
|
|
1292
|
+
await args.deploy();
|
|
1293
|
+
} catch (guardError) {
|
|
1294
|
+
if (!args.reversible)
|
|
1295
|
+
throw new WorkflowStateDivergedError({
|
|
1296
|
+
instanceId: args.instanceId,
|
|
1297
|
+
guardError,
|
|
1298
|
+
reason: "the move created child instances a parent-only rollback cannot undo"
|
|
1299
|
+
});
|
|
1300
|
+
try {
|
|
1301
|
+
let patch = args.client.patch(args.instanceId).set(args.restore);
|
|
1302
|
+
args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
|
|
1303
|
+
} catch (rollbackError) {
|
|
1304
|
+
throw new WorkflowStateDivergedError({
|
|
1305
|
+
instanceId: args.instanceId,
|
|
1306
|
+
guardError,
|
|
1307
|
+
rollbackError,
|
|
1308
|
+
reason: "rollback of the committed move failed"
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
throw guardError;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1315
|
+
function validateTags(tags) {
|
|
1316
|
+
if (tags.length === 0)
|
|
1317
|
+
throw new Error("tags: must be a non-empty array");
|
|
1318
|
+
for (const tag of tags)
|
|
1319
|
+
if (!TAG_RE.test(tag))
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
function canonicalTag(tags) {
|
|
1325
|
+
return tags[0];
|
|
1326
|
+
}
|
|
1327
|
+
function tagScopeFilter(param = "engineTags") {
|
|
1328
|
+
return `count(tags[@ in $${param}]) > 0`;
|
|
1329
|
+
}
|
|
1330
|
+
function definitionLookupGroq(explicit) {
|
|
1331
|
+
const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && workflowId == $workflowId && ${tagScopeFilter()}`;
|
|
1332
|
+
return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
|
|
910
1333
|
}
|
|
911
1334
|
async function discoverItems(args) {
|
|
912
1335
|
const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
|
|
@@ -923,31 +1346,29 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
923
1346
|
const MAX_SPAWN_DEPTH = 6;
|
|
924
1347
|
async function spawnChildren(ctx, mutation, task, spawn, actor) {
|
|
925
1348
|
if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
|
|
926
|
-
const chain = [
|
|
927
|
-
...ctx.instance.ancestors.map((a) => a.id),
|
|
928
|
-
gdrFromResource(ctx.instance.workflowResource, ctx.instance._id)
|
|
929
|
-
].join(" \u2192 ");
|
|
1349
|
+
const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
|
|
930
1350
|
throw new Error(
|
|
931
1351
|
`Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
|
|
932
1352
|
);
|
|
933
1353
|
}
|
|
934
|
-
const rows = await resolveSpawnRows(ctx, spawn), refs = [];
|
|
1354
|
+
const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
|
|
935
1355
|
for (const row of rows) {
|
|
936
|
-
const projected = projectSpawnRow(ctx.instance, spawn, row), { ref, body } = await prepareChildInstance({
|
|
1356
|
+
const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
|
|
937
1357
|
client: ctx.client,
|
|
938
1358
|
parent: ctx.instance,
|
|
939
1359
|
task,
|
|
940
1360
|
spawn,
|
|
941
1361
|
initialState: projected.initialState,
|
|
942
1362
|
effectsContext: projected.effectsContext,
|
|
943
|
-
actor
|
|
1363
|
+
actor,
|
|
1364
|
+
now
|
|
944
1365
|
});
|
|
945
1366
|
refs.push(ref), mutation.pendingCreates.push(body);
|
|
946
1367
|
}
|
|
947
1368
|
return refs;
|
|
948
1369
|
}
|
|
949
1370
|
async function resolveSpawnRows(ctx, spawn) {
|
|
950
|
-
const params = buildParams(ctx.instance), groq = spawn.forEach.groq;
|
|
1371
|
+
const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
|
|
951
1372
|
let value;
|
|
952
1373
|
if (groq.includes("*")) {
|
|
953
1374
|
const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
|
|
@@ -969,7 +1390,7 @@ function firstDocRefGdrUri(slots) {
|
|
|
969
1390
|
if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
|
|
970
1391
|
}
|
|
971
1392
|
}
|
|
972
|
-
function resolveSpawnSource(source, parent, row) {
|
|
1393
|
+
function resolveSpawnSource(source, parent, row, now) {
|
|
973
1394
|
switch (source.source) {
|
|
974
1395
|
case "literal":
|
|
975
1396
|
return source.value;
|
|
@@ -980,15 +1401,15 @@ function resolveSpawnSource(source, parent, row) {
|
|
|
980
1401
|
return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
|
|
981
1402
|
}
|
|
982
1403
|
case "self":
|
|
983
|
-
return
|
|
1404
|
+
return selfGdr(parent);
|
|
984
1405
|
case "stageId":
|
|
985
1406
|
return parent.currentStageId;
|
|
986
1407
|
case "now":
|
|
987
|
-
return
|
|
1408
|
+
return now;
|
|
988
1409
|
case "object": {
|
|
989
1410
|
const out = {};
|
|
990
|
-
for (const [k,
|
|
991
|
-
out[k] = resolveSpawnSource(
|
|
1411
|
+
for (const [k, v2] of Object.entries(source.fields))
|
|
1412
|
+
out[k] = resolveSpawnSource(v2, parent, row, now);
|
|
992
1413
|
return out;
|
|
993
1414
|
}
|
|
994
1415
|
case "actor":
|
|
@@ -998,10 +1419,10 @@ function resolveSpawnSource(source, parent, row) {
|
|
|
998
1419
|
return null;
|
|
999
1420
|
}
|
|
1000
1421
|
}
|
|
1001
|
-
function projectSpawnRow(parent, spawn, row) {
|
|
1422
|
+
function projectSpawnRow(parent, spawn, row, now) {
|
|
1002
1423
|
const initialState = [];
|
|
1003
1424
|
for (const entry of spawn.forEach.as.initialState ?? []) {
|
|
1004
|
-
const raw = resolveSpawnSource(entry.value, parent, row), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
|
|
1425
|
+
const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
|
|
1005
1426
|
initialState.push({
|
|
1006
1427
|
type: entry.type,
|
|
1007
1428
|
id: entry.id,
|
|
@@ -1010,13 +1431,13 @@ function projectSpawnRow(parent, spawn, row) {
|
|
|
1010
1431
|
}
|
|
1011
1432
|
const effectsContext = {};
|
|
1012
1433
|
for (const [k, src] of Object.entries(spawn.forEach.as.effectsContext ?? {})) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1434
|
+
const v2 = resolveSpawnSource(src, parent, row, now);
|
|
1435
|
+
v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (effectsContext[k] = v2);
|
|
1015
1436
|
}
|
|
1016
1437
|
return { initialState, effectsContext };
|
|
1017
1438
|
}
|
|
1018
1439
|
function canonicaliseSpawnValue(slotType, value, workflowResource) {
|
|
1019
|
-
return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((
|
|
1440
|
+
return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
|
|
1020
1441
|
}
|
|
1021
1442
|
function coerceSpawnGdr(raw, workflowResource) {
|
|
1022
1443
|
const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
|
|
@@ -1033,30 +1454,32 @@ function coerceSpawnGdr(raw, workflowResource) {
|
|
|
1033
1454
|
return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
|
|
1034
1455
|
}
|
|
1035
1456
|
async function resolveLogicalDefinition(client, ref, tags) {
|
|
1036
|
-
const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
|
|
1037
|
-
wantsExplicit && (params.version = ref.version)
|
|
1038
|
-
|
|
1039
|
-
|
|
1457
|
+
const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
|
|
1458
|
+
return wantsExplicit && (params.version = ref.version), await client.fetch(
|
|
1459
|
+
definitionLookupGroq(wantsExplicit),
|
|
1460
|
+
params
|
|
1461
|
+
) ?? null;
|
|
1040
1462
|
}
|
|
1041
1463
|
async function prepareChildInstance(args) {
|
|
1042
|
-
const { client, parent, task, spawn, initialState, effectsContext, actor } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
|
|
1464
|
+
const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
|
|
1043
1465
|
if (definition === null) {
|
|
1044
1466
|
const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
|
|
1045
1467
|
throw new Error(
|
|
1046
1468
|
`Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
|
|
1047
1469
|
);
|
|
1048
1470
|
}
|
|
1049
|
-
const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type:
|
|
1471
|
+
const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
|
|
1050
1472
|
...parent.ancestors,
|
|
1051
1473
|
{
|
|
1052
|
-
id:
|
|
1053
|
-
type:
|
|
1474
|
+
id: selfGdr(parent),
|
|
1475
|
+
type: WORKFLOW_INSTANCE_TYPE
|
|
1054
1476
|
}
|
|
1055
1477
|
], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
|
|
1056
1478
|
slotDefs: definition.state,
|
|
1057
1479
|
initialState,
|
|
1058
1480
|
ctx: {
|
|
1059
1481
|
client,
|
|
1482
|
+
now,
|
|
1060
1483
|
selfId: childDocId,
|
|
1061
1484
|
tags: childTags,
|
|
1062
1485
|
workflowResource,
|
|
@@ -1076,7 +1499,7 @@ async function prepareChildInstance(args) {
|
|
|
1076
1499
|
}
|
|
1077
1500
|
), base = buildInstanceBase({
|
|
1078
1501
|
id: childDocId,
|
|
1079
|
-
now
|
|
1502
|
+
now,
|
|
1080
1503
|
tags: childTags,
|
|
1081
1504
|
workflowResource,
|
|
1082
1505
|
workflowId: definition.workflowId,
|
|
@@ -1112,7 +1535,9 @@ async function checkSpawnCompletion(client, entry, spawn) {
|
|
|
1112
1535
|
}
|
|
1113
1536
|
}
|
|
1114
1537
|
async function invokeTask(args) {
|
|
1115
|
-
const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId
|
|
1538
|
+
const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1539
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
1540
|
+
});
|
|
1116
1541
|
return commitInvoke(ctx, taskId, options?.actor);
|
|
1117
1542
|
}
|
|
1118
1543
|
async function commitInvoke(ctx, taskId, actor) {
|
|
@@ -1124,41 +1549,41 @@ async function commitInvoke(ctx, taskId, actor) {
|
|
|
1124
1549
|
const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
|
|
1125
1550
|
return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
|
|
1126
1551
|
}
|
|
1127
|
-
async function autoResolveOnActivate(ctx, mutation, task, entry) {
|
|
1552
|
+
async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
|
|
1128
1553
|
const checks = [
|
|
1129
1554
|
{ filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
|
|
1130
1555
|
{ filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
|
|
1131
1556
|
];
|
|
1132
|
-
for (const { filter, outcome, systemId } of checks)
|
|
1133
|
-
if (filter
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
), !0;
|
|
1145
|
-
}
|
|
1557
|
+
for (const { filter, outcome, systemId } of checks)
|
|
1558
|
+
if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
|
|
1559
|
+
return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
|
|
1560
|
+
taskStatusChangedEntry({
|
|
1561
|
+
stageId: mutation.currentStageId,
|
|
1562
|
+
taskId: task.id,
|
|
1563
|
+
from: "active",
|
|
1564
|
+
to: outcome,
|
|
1565
|
+
at: now,
|
|
1566
|
+
actor: { kind: "system", id: systemId }
|
|
1567
|
+
})
|
|
1568
|
+
), !0;
|
|
1146
1569
|
return !1;
|
|
1147
1570
|
}
|
|
1148
1571
|
async function activateTask(ctx, mutation, task, entry, actor) {
|
|
1149
|
-
|
|
1572
|
+
const now = ctx.now;
|
|
1573
|
+
if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
|
|
1150
1574
|
const stage = findStage(ctx.definition, mutation.currentStageId);
|
|
1151
1575
|
entry.state = await resolveTaskStateSlots({
|
|
1152
1576
|
client: ctx.client,
|
|
1153
1577
|
instance: ctx.instance,
|
|
1154
1578
|
stage,
|
|
1155
|
-
task
|
|
1579
|
+
task,
|
|
1580
|
+
now
|
|
1156
1581
|
});
|
|
1157
1582
|
}
|
|
1158
|
-
if (!await autoResolveOnActivate(ctx, mutation, task, entry) && (mutation.history.push({
|
|
1583
|
+
if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
|
|
1159
1584
|
_key: randomKey(),
|
|
1160
1585
|
_type: "workflow.history.taskActivated",
|
|
1161
|
-
at:
|
|
1586
|
+
at: now,
|
|
1162
1587
|
stageId: mutation.currentStageId,
|
|
1163
1588
|
taskId: task.id,
|
|
1164
1589
|
...actor !== void 0 ? { actor } : {}
|
|
@@ -1178,18 +1603,19 @@ async function activateTask(ctx, mutation, task, entry, actor) {
|
|
|
1178
1603
|
mutation.history.push({
|
|
1179
1604
|
_key: randomKey(),
|
|
1180
1605
|
_type: "workflow.history.spawned",
|
|
1181
|
-
at:
|
|
1606
|
+
at: now,
|
|
1182
1607
|
taskId: task.id,
|
|
1183
1608
|
instanceRef: ref
|
|
1184
1609
|
});
|
|
1185
1610
|
if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
|
|
1186
1611
|
const previousStatus = entry.status;
|
|
1187
|
-
entry.status = "done", entry.completedAt =
|
|
1612
|
+
entry.status = "done", entry.completedAt = now, mutation.history.push(
|
|
1188
1613
|
taskStatusChangedEntry({
|
|
1189
1614
|
stageId: mutation.currentStageId,
|
|
1190
1615
|
taskId: task.id,
|
|
1191
1616
|
from: previousStatus,
|
|
1192
1617
|
to: "done",
|
|
1618
|
+
at: now,
|
|
1193
1619
|
...actor !== void 0 ? { actor } : {}
|
|
1194
1620
|
})
|
|
1195
1621
|
);
|
|
@@ -1206,7 +1632,7 @@ async function buildStageTasks(ctx, stage) {
|
|
|
1206
1632
|
status: inScope ? "pending" : "skipped",
|
|
1207
1633
|
...task.filter !== void 0 ? {
|
|
1208
1634
|
filterEvaluation: {
|
|
1209
|
-
at:
|
|
1635
|
+
at: ctx.now,
|
|
1210
1636
|
truthy: inScope
|
|
1211
1637
|
}
|
|
1212
1638
|
} : {}
|
|
@@ -1218,7 +1644,9 @@ function activatesOnStageEnter(task) {
|
|
|
1218
1644
|
return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
|
|
1219
1645
|
}
|
|
1220
1646
|
async function setStage(args) {
|
|
1221
|
-
const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId
|
|
1647
|
+
const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
|
|
1648
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
1649
|
+
});
|
|
1222
1650
|
return commitSetStage(ctx, targetStageId, reason, options?.actor);
|
|
1223
1651
|
}
|
|
1224
1652
|
async function enterStage(ctx, mutation, nextStage, actor, at) {
|
|
@@ -1230,7 +1658,8 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
|
|
|
1230
1658
|
state: await resolveStageStateSlots({
|
|
1231
1659
|
client: ctx.client,
|
|
1232
1660
|
instance: ctx.instance,
|
|
1233
|
-
stage: nextStage
|
|
1661
|
+
stage: nextStage,
|
|
1662
|
+
now: at
|
|
1234
1663
|
}),
|
|
1235
1664
|
tasks: await buildStageTasks(ctx, nextStage)
|
|
1236
1665
|
};
|
|
@@ -1249,7 +1678,7 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
|
|
|
1249
1678
|
nextStage.kind === "terminal" && (mutation.completedAt = at);
|
|
1250
1679
|
}
|
|
1251
1680
|
async function beginStageMove(ctx, currentStage, actor) {
|
|
1252
|
-
const mutation = startMutation(ctx.instance), at =
|
|
1681
|
+
const mutation = startMutation(ctx.instance), at = ctx.now;
|
|
1253
1682
|
return await queueEffects(
|
|
1254
1683
|
ctx,
|
|
1255
1684
|
mutation,
|
|
@@ -1283,13 +1712,46 @@ async function commitSetStage(ctx, targetStageId, reason, actor) {
|
|
|
1283
1712
|
};
|
|
1284
1713
|
}
|
|
1285
1714
|
async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
|
|
1286
|
-
await
|
|
1715
|
+
await persistThenDeploy(
|
|
1716
|
+
ctx,
|
|
1717
|
+
mutation,
|
|
1718
|
+
() => deployStageGuards({
|
|
1719
|
+
client: ctx.client,
|
|
1720
|
+
clientForGdr: ctx.clientForGdr,
|
|
1721
|
+
instance: materializeInstance(ctx.instance, mutation),
|
|
1722
|
+
definition: ctx.definition,
|
|
1723
|
+
stageId: enteredStageId,
|
|
1724
|
+
now: ctx.now
|
|
1725
|
+
})
|
|
1726
|
+
), await retractStageGuards({
|
|
1287
1727
|
client: ctx.client,
|
|
1288
1728
|
clientForGdr: ctx.clientForGdr,
|
|
1289
1729
|
instance: ctx.instance,
|
|
1290
1730
|
definition: ctx.definition,
|
|
1291
|
-
exitedStageId,
|
|
1292
|
-
|
|
1731
|
+
stageId: exitedStageId,
|
|
1732
|
+
now: ctx.now
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
async function persistThenDeploy(ctx, mutation, deploy) {
|
|
1736
|
+
const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
|
|
1737
|
+
await deployOrRollback({
|
|
1738
|
+
client: ctx.client,
|
|
1739
|
+
instanceId: ctx.instance._id,
|
|
1740
|
+
committedRev: committed._rev,
|
|
1741
|
+
restore: instanceStateFields(ctx.instance),
|
|
1742
|
+
unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
|
|
1743
|
+
reversible: !spawned,
|
|
1744
|
+
deploy
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
async function refreshStageGuards(ctx, mutation, stageId) {
|
|
1748
|
+
await deployStageGuards({
|
|
1749
|
+
client: ctx.client,
|
|
1750
|
+
clientForGdr: ctx.clientForGdr,
|
|
1751
|
+
instance: materializeInstance(ctx.instance, mutation),
|
|
1752
|
+
definition: ctx.definition,
|
|
1753
|
+
stageId,
|
|
1754
|
+
now: ctx.now
|
|
1293
1755
|
});
|
|
1294
1756
|
}
|
|
1295
1757
|
async function commitTransition(ctx, action, actor) {
|
|
@@ -1331,26 +1793,29 @@ async function pickTransition(ctx, stage, action) {
|
|
|
1331
1793
|
if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
|
|
1332
1794
|
}
|
|
1333
1795
|
async function evaluateAutoTransitions(args) {
|
|
1334
|
-
const { client, instanceId, options, clientForGdr } = args, ctx = await loadContext(client, instanceId,
|
|
1796
|
+
const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
|
|
1797
|
+
...clientForGdr ? { clientForGdr } : {},
|
|
1798
|
+
...clock ? { clock } : {},
|
|
1799
|
+
...overlay ? { overlay } : {}
|
|
1800
|
+
});
|
|
1335
1801
|
return commitTransition(ctx, void 0, options?.actor);
|
|
1336
1802
|
}
|
|
1337
|
-
async function primeInitialStage(client, instanceId, actor, clientForGdr) {
|
|
1803
|
+
async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
|
|
1338
1804
|
const instance = await client.getDocument(instanceId);
|
|
1339
1805
|
if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
|
|
1340
1806
|
const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
|
|
1341
1807
|
if (stage === void 0) return;
|
|
1342
|
-
const snapshot = await hydrateSnapshot({
|
|
1808
|
+
const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
|
|
1343
1809
|
client,
|
|
1344
1810
|
clientForGdr: clientForGdr ?? (() => client),
|
|
1345
|
-
instance
|
|
1346
|
-
definition
|
|
1811
|
+
instance
|
|
1347
1812
|
}), tasks = [];
|
|
1348
1813
|
for (const task of stage.tasks ?? []) {
|
|
1349
1814
|
const inScope = await evaluateFilter({
|
|
1350
1815
|
filter: task.filter,
|
|
1351
1816
|
definition,
|
|
1352
1817
|
snapshot,
|
|
1353
|
-
params: buildParams(instance)
|
|
1818
|
+
params: buildParams(instance, now)
|
|
1354
1819
|
});
|
|
1355
1820
|
tasks.push({
|
|
1356
1821
|
_key: randomKey(),
|
|
@@ -1361,8 +1826,8 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
|
|
|
1361
1826
|
const initialStageEntry = {
|
|
1362
1827
|
_key: randomKey(),
|
|
1363
1828
|
id: stage.id,
|
|
1364
|
-
enteredAt:
|
|
1365
|
-
state: await resolveStageStateSlots({ client, instance, stage }),
|
|
1829
|
+
enteredAt: now,
|
|
1830
|
+
state: await resolveStageStateSlots({ client, instance, stage, now }),
|
|
1366
1831
|
tasks
|
|
1367
1832
|
}, pendingEffects = [], newHistory = [...instance.history];
|
|
1368
1833
|
for (const effect of stage.onEnter ?? []) {
|
|
@@ -1370,29 +1835,44 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
|
|
|
1370
1835
|
bindings: effect.bindings,
|
|
1371
1836
|
staticInput: effect.input,
|
|
1372
1837
|
instance,
|
|
1838
|
+
now,
|
|
1373
1839
|
...actor !== void 0 ? { actor } : {}
|
|
1374
1840
|
}), { pending, history } = buildQueuedEffect(
|
|
1375
1841
|
effect,
|
|
1376
1842
|
{ kind: "stageEnter", id: stage.id },
|
|
1377
1843
|
params,
|
|
1378
|
-
actor
|
|
1844
|
+
actor,
|
|
1845
|
+
now
|
|
1379
1846
|
);
|
|
1380
1847
|
pendingEffects.push(pending), newHistory.push(history);
|
|
1381
1848
|
}
|
|
1382
|
-
await client.patch(instance._id).set({
|
|
1849
|
+
const committed = await client.patch(instance._id).set({
|
|
1383
1850
|
stages: [initialStageEntry],
|
|
1384
1851
|
pendingEffects,
|
|
1385
1852
|
history: newHistory,
|
|
1386
|
-
lastChangedAt:
|
|
1387
|
-
}).ifRevisionId(instance._rev).commit()
|
|
1853
|
+
lastChangedAt: now
|
|
1854
|
+
}).ifRevisionId(instance._rev).commit();
|
|
1855
|
+
await deployOrRollback({
|
|
1388
1856
|
client,
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1857
|
+
instanceId: instance._id,
|
|
1858
|
+
committedRev: committed._rev,
|
|
1859
|
+
restore: {
|
|
1860
|
+
stages: instance.stages,
|
|
1861
|
+
pendingEffects: instance.pendingEffects,
|
|
1862
|
+
history: instance.history
|
|
1863
|
+
},
|
|
1864
|
+
reversible: !0,
|
|
1865
|
+
deploy: () => deployStageGuards({
|
|
1866
|
+
client,
|
|
1867
|
+
clientForGdr: clientForGdr ?? (() => client),
|
|
1868
|
+
instance,
|
|
1869
|
+
definition,
|
|
1870
|
+
stageId: stage.id,
|
|
1871
|
+
now
|
|
1872
|
+
})
|
|
1873
|
+
}), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
|
|
1394
1874
|
}
|
|
1395
|
-
async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr) {
|
|
1875
|
+
async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
|
|
1396
1876
|
const resolvedClientForGdr = clientForGdr ?? (() => client);
|
|
1397
1877
|
for (const task of stage.tasks ?? []) {
|
|
1398
1878
|
if (!activatesOnStageEnter(task)) continue;
|
|
@@ -1402,7 +1882,8 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
|
|
|
1402
1882
|
client,
|
|
1403
1883
|
clientForGdr: resolvedClientForGdr,
|
|
1404
1884
|
instance: updated,
|
|
1405
|
-
definition
|
|
1885
|
+
definition,
|
|
1886
|
+
...clock ? { clock } : {}
|
|
1406
1887
|
}), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
|
|
1407
1888
|
mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
|
|
1408
1889
|
}
|
|
@@ -1420,8 +1901,12 @@ async function gatherAutoResolveCandidates(ctx, tasks) {
|
|
|
1420
1901
|
}
|
|
1421
1902
|
return candidates;
|
|
1422
1903
|
}
|
|
1423
|
-
async function resolveCompleteWhen(client, instanceId, clientForGdr) {
|
|
1424
|
-
const ctx = await loadContext(client, instanceId,
|
|
1904
|
+
async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
|
|
1905
|
+
const ctx = await loadContext(client, instanceId, {
|
|
1906
|
+
...clientForGdr ? { clientForGdr } : {},
|
|
1907
|
+
...clock ? { clock } : {},
|
|
1908
|
+
...overlay ? { overlay } : {}
|
|
1909
|
+
}), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
|
|
1425
1910
|
(t) => t.completeWhen !== void 0 || t.failWhen !== void 0
|
|
1426
1911
|
);
|
|
1427
1912
|
if (tasksWithAutoPredicate.length === 0) return 0;
|
|
@@ -1435,339 +1920,517 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr) {
|
|
|
1435
1920
|
const previousStatus = mutEntry.status, actor = {
|
|
1436
1921
|
kind: "system",
|
|
1437
1922
|
id: outcome === "failed" ? "engine.failWhen" : "engine.completeWhen"
|
|
1438
|
-
};
|
|
1439
|
-
mutEntry.status = outcome, mutEntry.completedAt =
|
|
1923
|
+
}, now = ctx.now;
|
|
1924
|
+
mutEntry.status = outcome, mutEntry.completedAt = now, mutEntry.completedBy = actor.id, mutation.history.push(
|
|
1440
1925
|
taskStatusChangedEntry({
|
|
1441
1926
|
stageId: stage.id,
|
|
1442
1927
|
taskId: task.id,
|
|
1443
1928
|
from: previousStatus,
|
|
1444
1929
|
to: outcome,
|
|
1930
|
+
at: now,
|
|
1445
1931
|
actor
|
|
1446
1932
|
})
|
|
1447
1933
|
), count++;
|
|
1448
1934
|
}
|
|
1449
1935
|
return count > 0 && await persist(ctx, mutation), count;
|
|
1450
1936
|
}
|
|
1451
|
-
async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr) {
|
|
1937
|
+
async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock, overlay) {
|
|
1452
1938
|
let count = 0;
|
|
1453
1939
|
for (; ; ) {
|
|
1454
|
-
if (await resolveCompleteWhen(client, instanceId, clientForGdr), !(await evaluateAutoTransitions({
|
|
1940
|
+
if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
|
|
1455
1941
|
client,
|
|
1456
1942
|
instanceId,
|
|
1457
1943
|
...actor !== void 0 ? { options: { actor } } : {},
|
|
1458
|
-
...clientForGdr ? { clientForGdr } : {}
|
|
1944
|
+
...clientForGdr ? { clientForGdr } : {},
|
|
1945
|
+
...clock ? { clock } : {},
|
|
1946
|
+
...overlay ? { overlay } : {}
|
|
1459
1947
|
})).fired) return count;
|
|
1460
|
-
if (count++, count
|
|
1461
|
-
throw new
|
|
1462
|
-
`Cascade did not stabilise after ${CASCADE_LIMIT} auto-transitions on ${instanceId} \u2014 possible infinite loop`
|
|
1463
|
-
);
|
|
1948
|
+
if (count++, count >= CASCADE_LIMIT)
|
|
1949
|
+
throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
|
|
1464
1950
|
}
|
|
1465
1951
|
}
|
|
1466
|
-
async function
|
|
1467
|
-
const
|
|
1468
|
-
if (!instance || instance.ancestors.length === 0) return;
|
|
1469
|
-
const parentRef = instance.ancestors[instance.ancestors.length - 1];
|
|
1952
|
+
async function findSatisfiedParentSpawn(client, child) {
|
|
1953
|
+
const parentRef = child.ancestors.at(-1);
|
|
1470
1954
|
if (parentRef === void 0) return;
|
|
1471
|
-
const
|
|
1472
|
-
if (!parent || parent._type !==
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1955
|
+
const parent = await client.getDocument(gdrToBareId(parentRef.id));
|
|
1956
|
+
if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
|
|
1957
|
+
const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
|
|
1958
|
+
if (stage === void 0) return;
|
|
1959
|
+
const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
|
|
1960
|
+
if (task?.spawns === void 0) return;
|
|
1961
|
+
const entry = parentCurrentTasks.find((e) => e.id === task.id);
|
|
1962
|
+
if (!(entry === void 0 || entry.status === "done") && await checkSpawnCompletion(client, entry, task.spawns))
|
|
1963
|
+
return { parent, definition, stage, task };
|
|
1964
|
+
}
|
|
1965
|
+
async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
|
|
1966
|
+
const instance = await client.getDocument(instanceId);
|
|
1967
|
+
if (!instance) return;
|
|
1968
|
+
const found = await findSatisfiedParentSpawn(client, instance);
|
|
1969
|
+
if (found === void 0) return;
|
|
1970
|
+
const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
|
|
1481
1971
|
client,
|
|
1482
1972
|
clientForGdr: clientForGdr ?? (() => client),
|
|
1483
1973
|
instance: parent,
|
|
1484
|
-
definition
|
|
1485
|
-
|
|
1974
|
+
definition,
|
|
1975
|
+
...clock ? { clock } : {}
|
|
1976
|
+
}), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === task.id);
|
|
1486
1977
|
if (mutEntry === void 0) return;
|
|
1487
|
-
const previousStatus = mutEntry.status;
|
|
1488
|
-
mutEntry.status = "done", mutEntry.completedAt =
|
|
1978
|
+
const previousStatus = mutEntry.status, now = ctx.now;
|
|
1979
|
+
mutEntry.status = "done", mutEntry.completedAt = now, mutation.history.push(
|
|
1489
1980
|
taskStatusChangedEntry({
|
|
1490
|
-
stageId:
|
|
1491
|
-
taskId:
|
|
1981
|
+
stageId: stage.id,
|
|
1982
|
+
taskId: task.id,
|
|
1492
1983
|
from: previousStatus,
|
|
1493
1984
|
to: "done",
|
|
1985
|
+
at: now,
|
|
1494
1986
|
...actor !== void 0 ? { actor } : {}
|
|
1495
1987
|
})
|
|
1496
|
-
), await persist(ctx, mutation), await cascadeAutoTransitions(client,
|
|
1497
|
-
}
|
|
1498
|
-
function startMutation(instance) {
|
|
1499
|
-
return {
|
|
1500
|
-
currentStageId: instance.currentStageId,
|
|
1501
|
-
// Deep-ish copy. Per-scope state slot arrays need their own copies
|
|
1502
|
-
// so ops can mutate without leaking into the source.
|
|
1503
|
-
state: (instance.state ?? []).map((s) => ({ ...s })),
|
|
1504
|
-
stages: instance.stages.map((s) => ({
|
|
1505
|
-
...s,
|
|
1506
|
-
state: (s.state ?? []).map((slot) => ({ ...slot })),
|
|
1507
|
-
tasks: s.tasks.map((t) => ({
|
|
1508
|
-
...t,
|
|
1509
|
-
...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
|
|
1510
|
-
}))
|
|
1511
|
-
})),
|
|
1512
|
-
pendingEffects: [...instance.pendingEffects],
|
|
1513
|
-
effectHistory: [...instance.effectHistory],
|
|
1514
|
-
effectsContext: [...instance.effectsContext],
|
|
1515
|
-
history: [...instance.history],
|
|
1516
|
-
lastChangedAt: instance.lastChangedAt,
|
|
1517
|
-
...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
|
|
1518
|
-
pendingCreates: []
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
function taskStatusChangedEntry(args) {
|
|
1522
|
-
return {
|
|
1523
|
-
_key: randomKey(),
|
|
1524
|
-
_type: "workflow.history.taskStatusChanged",
|
|
1525
|
-
at: args.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1526
|
-
stageId: args.stageId,
|
|
1527
|
-
taskId: args.taskId,
|
|
1528
|
-
from: args.from,
|
|
1529
|
-
to: args.to,
|
|
1530
|
-
...args.actor !== void 0 ? { actor: args.actor } : {}
|
|
1531
|
-
};
|
|
1988
|
+
), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
|
|
1532
1989
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
...
|
|
1540
|
-
};
|
|
1541
|
-
return
|
|
1542
|
-
{
|
|
1543
|
-
_key: randomKey(),
|
|
1544
|
-
_type: "workflow.history.stageExited",
|
|
1545
|
-
stageId: fromStageId,
|
|
1546
|
-
toStageId,
|
|
1547
|
-
...shared
|
|
1548
|
-
},
|
|
1549
|
-
{
|
|
1550
|
-
_key: randomKey(),
|
|
1551
|
-
_type: "workflow.history.transitionFired",
|
|
1552
|
-
fromStageId,
|
|
1553
|
-
toStageId,
|
|
1554
|
-
...shared
|
|
1555
|
-
},
|
|
1556
|
-
{
|
|
1557
|
-
_key: randomKey(),
|
|
1558
|
-
_type: "workflow.history.stageEntered",
|
|
1559
|
-
stageId: toStageId,
|
|
1560
|
-
fromStageId,
|
|
1561
|
-
...shared
|
|
1562
|
-
}
|
|
1563
|
-
];
|
|
1990
|
+
const parsedFilters = /* @__PURE__ */ new Map();
|
|
1991
|
+
async function matchesFilter(args) {
|
|
1992
|
+
const { document, filter, userId } = args;
|
|
1993
|
+
parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
|
|
1994
|
+
const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
|
|
1995
|
+
dataset: [document],
|
|
1996
|
+
...userId !== void 0 ? { identity: userId } : {}
|
|
1997
|
+
})).get();
|
|
1998
|
+
return Array.isArray(data) && data.length === 1;
|
|
1564
1999
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
mutation.completedAt !== void 0 && (set.completedAt = mutation.completedAt);
|
|
1577
|
-
const pendingCreates = mutation.pendingCreates;
|
|
1578
|
-
if (pendingCreates.length === 0)
|
|
1579
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1580
|
-
const tx = ctx.client.transaction();
|
|
1581
|
-
for (const body of pendingCreates) tx.create(body);
|
|
1582
|
-
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
1583
|
-
const actorForPriming = void 0;
|
|
1584
|
-
for (const body of pendingCreates)
|
|
1585
|
-
await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr);
|
|
1586
|
-
const reloaded = await ctx.client.getDocument(ctx.instance._id);
|
|
1587
|
-
if (!reloaded)
|
|
1588
|
-
throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
|
|
1589
|
-
return reloaded;
|
|
2000
|
+
async function grantsPermissionOn(args) {
|
|
2001
|
+
const { document, grants, permission, userId } = args;
|
|
2002
|
+
if (!document || grants.length === 0) return !1;
|
|
2003
|
+
for (const grant of grants)
|
|
2004
|
+
if (grant.permissions.includes(permission) && await matchesFilter({
|
|
2005
|
+
document,
|
|
2006
|
+
filter: grant.filter,
|
|
2007
|
+
...userId !== void 0 ? { userId } : {}
|
|
2008
|
+
}))
|
|
2009
|
+
return !0;
|
|
2010
|
+
return !1;
|
|
1590
2011
|
}
|
|
1591
|
-
function
|
|
1592
|
-
|
|
2012
|
+
async function fetchGrants(args) {
|
|
2013
|
+
const { client, resourcePath, signal } = args;
|
|
2014
|
+
return client.request({ url: resourcePath, ...signal ? { signal } : {} });
|
|
1593
2015
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2016
|
+
const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
|
|
2017
|
+
async function resolveAccess(client, args = {}) {
|
|
2018
|
+
const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
|
|
2019
|
+
if (overrideActor !== void 0 && overrideGrants !== void 0)
|
|
2020
|
+
return { actor: overrideActor, grants: overrideGrants };
|
|
2021
|
+
const requestFn = client.request === void 0 ? void 0 : (opts) => client.request(opts);
|
|
2022
|
+
if (requestFn === void 0) {
|
|
2023
|
+
if (overrideActor === void 0)
|
|
2024
|
+
throw new Error(
|
|
2025
|
+
"workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
|
|
2026
|
+
);
|
|
2027
|
+
return { actor: overrideActor };
|
|
2028
|
+
}
|
|
2029
|
+
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]);
|
|
2030
|
+
if (actor === void 0)
|
|
1597
2031
|
throw new Error(
|
|
1598
|
-
|
|
2032
|
+
"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."
|
|
1599
2033
|
);
|
|
1600
|
-
return
|
|
2034
|
+
return { actor, ...grants !== void 0 ? { grants } : {} };
|
|
1601
2035
|
}
|
|
1602
|
-
function
|
|
1603
|
-
|
|
2036
|
+
function cachedActor(client, requestFn) {
|
|
2037
|
+
const cached = actorCache.get(client);
|
|
2038
|
+
if (cached !== void 0) return cached;
|
|
2039
|
+
const pending = fetchActor(requestFn).catch((err) => {
|
|
2040
|
+
throw actorCache.get(client) === pending && actorCache.delete(client), err;
|
|
2041
|
+
});
|
|
2042
|
+
return actorCache.set(client, pending), pending;
|
|
1604
2043
|
}
|
|
1605
|
-
function
|
|
1606
|
-
|
|
1607
|
-
if (task === void 0)
|
|
1608
|
-
throw new Error(
|
|
1609
|
-
`Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
|
|
1610
|
-
);
|
|
1611
|
-
return { stage, task };
|
|
1612
|
-
}
|
|
1613
|
-
function requireMutationTaskEntry(mutation, taskId) {
|
|
1614
|
-
const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
|
|
1615
|
-
if (mutEntry === void 0)
|
|
1616
|
-
throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
|
|
1617
|
-
return mutEntry;
|
|
1618
|
-
}
|
|
1619
|
-
function findCurrentStageEntry(instance) {
|
|
1620
|
-
return findOpenStageEntry(instance);
|
|
1621
|
-
}
|
|
1622
|
-
function findCurrentTasks(instance) {
|
|
1623
|
-
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
2044
|
+
function resolveGrants(overrideGrants, grantsFromPath, client, requestFn) {
|
|
2045
|
+
return overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : grantsFromPath !== void 0 ? cachedGrants(client, requestFn, grantsFromPath) : Promise.resolve(void 0);
|
|
1624
2046
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
status,
|
|
1631
|
-
outputs,
|
|
1632
|
-
detail,
|
|
1633
|
-
error,
|
|
1634
|
-
durationMs,
|
|
1635
|
-
options?.actor
|
|
1636
|
-
);
|
|
2047
|
+
function cachedGrants(client, requestFn, resourcePath) {
|
|
2048
|
+
let byPath = grantsCache.get(client);
|
|
2049
|
+
byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
|
|
2050
|
+
let cached = byPath.get(resourcePath);
|
|
2051
|
+
return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
|
|
1637
2052
|
}
|
|
1638
|
-
function
|
|
1639
|
-
|
|
2053
|
+
async function fetchActor(requestFn) {
|
|
2054
|
+
let user;
|
|
2055
|
+
try {
|
|
2056
|
+
user = await requestFn({
|
|
2057
|
+
uri: "/users/me",
|
|
2058
|
+
tag: "workflow.access.resolve-actor"
|
|
2059
|
+
});
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
|
|
2063
|
+
{ cause: err }
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (!user || typeof user.id != "string" || user.id.length === 0) return;
|
|
2067
|
+
const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
|
|
1640
2068
|
return {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
...
|
|
1644
|
-
...pending.description !== void 0 ? { description: pending.description } : {},
|
|
1645
|
-
params: pending.params,
|
|
1646
|
-
origin: pending.origin,
|
|
1647
|
-
...actor !== void 0 ? { actor } : pending.actor !== void 0 ? { actor: pending.actor } : {},
|
|
1648
|
-
ranAt,
|
|
1649
|
-
...durationMs !== void 0 ? { durationMs } : {},
|
|
1650
|
-
status,
|
|
1651
|
-
...detail !== void 0 ? { detail } : {},
|
|
1652
|
-
...error !== void 0 ? { error } : {},
|
|
1653
|
-
...outputs !== void 0 ? { outputs } : {}
|
|
2069
|
+
kind: "user",
|
|
2070
|
+
id: user.id,
|
|
2071
|
+
...roleNames.length > 0 ? { roles: roleNames } : {}
|
|
1654
2072
|
};
|
|
1655
2073
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2074
|
+
async function fetchGrantsCached(requestFn, resourcePath) {
|
|
2075
|
+
try {
|
|
2076
|
+
return await fetchGrants({ client: { request: requestFn }, resourcePath });
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
console.warn(
|
|
2079
|
+
`workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
2080
|
+
);
|
|
2081
|
+
return;
|
|
1660
2082
|
}
|
|
1661
2083
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2084
|
+
const WILDCARD_ROLE = "*";
|
|
2085
|
+
async function evaluateInstance(args) {
|
|
2086
|
+
const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2087
|
+
validateTags(tags);
|
|
2088
|
+
const { actor, grants } = await resolveAccess(client, {
|
|
2089
|
+
...args.access !== void 0 ? { override: args.access } : {},
|
|
2090
|
+
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
2091
|
+
}), instance = await client.getDocument(instanceId);
|
|
2092
|
+
if (!instance)
|
|
2093
|
+
throw new Error(`Workflow instance "${instanceId}" not found`);
|
|
2094
|
+
if (!tags.some((t) => instance.tags?.includes(t)))
|
|
2095
|
+
throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
|
|
2096
|
+
const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance });
|
|
2097
|
+
return evaluateFromSnapshot({
|
|
2098
|
+
instance,
|
|
2099
|
+
definition,
|
|
2100
|
+
actor,
|
|
2101
|
+
snapshot,
|
|
2102
|
+
now,
|
|
2103
|
+
...grants !== void 0 ? { grants } : {}
|
|
2104
|
+
});
|
|
1682
2105
|
}
|
|
1683
|
-
function
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2106
|
+
async function evaluateFromSnapshot(args) {
|
|
2107
|
+
const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.id === instance.currentStageId);
|
|
2108
|
+
if (stage === void 0)
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`Instance "${instance._id}" currentStageId "${instance.currentStageId}" not in definition`
|
|
2111
|
+
);
|
|
2112
|
+
const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
|
|
2113
|
+
for (const task of stage.tasks ?? [])
|
|
2114
|
+
taskEvaluations.push(
|
|
2115
|
+
await evaluateTask({
|
|
2116
|
+
task,
|
|
2117
|
+
statusEntry: currentTaskEntries.find((t) => t.id === task.id),
|
|
2118
|
+
instance,
|
|
2119
|
+
definition,
|
|
2120
|
+
actor,
|
|
2121
|
+
grants,
|
|
2122
|
+
snapshot,
|
|
2123
|
+
now
|
|
2124
|
+
})
|
|
2125
|
+
);
|
|
2126
|
+
const transitionEvaluations = [];
|
|
2127
|
+
for (const transition of stage.transitions ?? [])
|
|
2128
|
+
transitionEvaluations.push({
|
|
2129
|
+
transition,
|
|
2130
|
+
filterSatisfied: await evaluateFilter({
|
|
2131
|
+
filter: transition.filter,
|
|
2132
|
+
definition,
|
|
2133
|
+
snapshot,
|
|
2134
|
+
params: buildParams(instance, now)
|
|
2135
|
+
})
|
|
2136
|
+
});
|
|
2137
|
+
const currentStage = {
|
|
2138
|
+
stage,
|
|
2139
|
+
tasks: taskEvaluations,
|
|
2140
|
+
transitions: transitionEvaluations
|
|
2141
|
+
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
|
|
2142
|
+
return {
|
|
2143
|
+
instance,
|
|
2144
|
+
definition,
|
|
2145
|
+
actor,
|
|
2146
|
+
currentStage,
|
|
2147
|
+
pendingOnYou,
|
|
2148
|
+
canInteract
|
|
1702
2149
|
};
|
|
1703
|
-
return { pending, history };
|
|
1704
2150
|
}
|
|
1705
|
-
async function
|
|
1706
|
-
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2151
|
+
async function evaluateTask(args) {
|
|
2152
|
+
const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
|
|
2153
|
+
for (const action of task.actions ?? [])
|
|
2154
|
+
actions.push(
|
|
2155
|
+
await evaluateAction({
|
|
2156
|
+
action,
|
|
2157
|
+
task,
|
|
2158
|
+
...statusEntry !== void 0 ? { statusEntry } : {},
|
|
2159
|
+
status,
|
|
2160
|
+
instance,
|
|
2161
|
+
definition,
|
|
2162
|
+
actor,
|
|
2163
|
+
...grants !== void 0 ? { grants } : {},
|
|
2164
|
+
snapshot,
|
|
2165
|
+
now
|
|
2166
|
+
})
|
|
2167
|
+
);
|
|
2168
|
+
return {
|
|
2169
|
+
task,
|
|
2170
|
+
status,
|
|
2171
|
+
// "pending on actor" treats both `pending` and `active` as waiting on
|
|
2172
|
+
// the assignee — pending tasks just haven't been auto-invoked yet, but
|
|
2173
|
+
// they're still inbox items.
|
|
2174
|
+
pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
|
|
2175
|
+
actions
|
|
1712
2176
|
};
|
|
1713
|
-
for (const effect of effects) {
|
|
1714
|
-
const params = await resolveBindings({
|
|
1715
|
-
bindings: effect.bindings,
|
|
1716
|
-
staticInput: effect.input,
|
|
1717
|
-
instance: liveInstance,
|
|
1718
|
-
...callerParams !== void 0 ? { params: callerParams } : {},
|
|
1719
|
-
...actor !== void 0 ? { actor } : {}
|
|
1720
|
-
}), { pending, history } = buildQueuedEffect(effect, origin, params, actor);
|
|
1721
|
-
mutation.pendingEffects.push(pending), mutation.history.push(history);
|
|
1722
|
-
}
|
|
1723
2177
|
}
|
|
1724
|
-
|
|
1725
|
-
action;
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
|
|
1730
|
-
`);
|
|
1731
|
-
super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
|
|
1732
|
-
${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
|
|
1733
|
-
}
|
|
2178
|
+
async function evaluateAction(args) {
|
|
2179
|
+
const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
|
|
2180
|
+
if (syncReason !== void 0) return disabled(action, syncReason);
|
|
2181
|
+
const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
|
|
2182
|
+
return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
|
|
1734
2183
|
}
|
|
1735
|
-
function
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
}
|
|
1743
|
-
present && (checkParamType(value, decl) || issues.push({
|
|
1744
|
-
paramId: decl.id,
|
|
1745
|
-
reason: `expected paramType=${decl.paramType}, got ${typeof value}`
|
|
1746
|
-
}));
|
|
1747
|
-
}
|
|
1748
|
-
if (issues.length > 0)
|
|
1749
|
-
throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
|
|
1750
|
-
return params;
|
|
2184
|
+
function lifecycleReason(instance, definition, status) {
|
|
2185
|
+
if (instance.completedAt !== void 0)
|
|
2186
|
+
return { kind: "instance-completed", completedAt: instance.completedAt };
|
|
2187
|
+
if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
|
|
2188
|
+
return { kind: "stage-terminal", stageId: instance.currentStageId };
|
|
2189
|
+
if (status === "done" || status === "skipped" || status === "failed")
|
|
2190
|
+
return { kind: "task-not-active", status };
|
|
1751
2191
|
}
|
|
1752
|
-
function
|
|
1753
|
-
|
|
2192
|
+
function actorReason(action, task, actor) {
|
|
2193
|
+
const actorRoles = actor.roles ?? [];
|
|
2194
|
+
if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
|
|
2195
|
+
return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
|
|
2196
|
+
if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
|
|
2197
|
+
return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
|
|
1754
2198
|
}
|
|
1755
|
-
function
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2199
|
+
async function permissionReason(action, instance, actor, grants) {
|
|
2200
|
+
if (grants === void 0) return;
|
|
2201
|
+
const permission = action.requiredPermission ?? "update";
|
|
2202
|
+
if (!await grantsPermissionOn({
|
|
2203
|
+
document: instance,
|
|
2204
|
+
grants,
|
|
2205
|
+
permission,
|
|
2206
|
+
userId: actor.id
|
|
2207
|
+
}))
|
|
2208
|
+
return { kind: "permission-denied", permission, matchingGrants: grants.length };
|
|
2209
|
+
}
|
|
2210
|
+
async function filterReason(action, instance, definition, snapshot, now) {
|
|
2211
|
+
if (!(action.filter === void 0 || await evaluateFilter({
|
|
2212
|
+
filter: action.filter,
|
|
2213
|
+
definition,
|
|
2214
|
+
snapshot,
|
|
2215
|
+
params: buildParams(instance, now)
|
|
2216
|
+
})))
|
|
2217
|
+
return { kind: "filter-failed", filter: action.filter };
|
|
2218
|
+
}
|
|
2219
|
+
function disabled(action, reason) {
|
|
2220
|
+
return { action, allowed: !1, disabledReason: reason };
|
|
2221
|
+
}
|
|
2222
|
+
function actorIsAssignee(actor, assignees) {
|
|
2223
|
+
if (assignees.length === 0) return !1;
|
|
2224
|
+
const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
|
|
2225
|
+
for (const assignee of assignees)
|
|
2226
|
+
if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
|
|
2227
|
+
return !0;
|
|
2228
|
+
return !1;
|
|
2229
|
+
}
|
|
2230
|
+
function parseDefinitionSnapshot(instance) {
|
|
2231
|
+
try {
|
|
2232
|
+
return JSON.parse(instance.definitionSnapshot);
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
throw new Error(
|
|
2235
|
+
`Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2236
|
+
{ cause: err }
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const resourceKey = (r) => `${r.type}.${r.id}`;
|
|
2241
|
+
function guardsForResource(client) {
|
|
2242
|
+
return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
|
|
2243
|
+
}
|
|
2244
|
+
async function guardsForInstance(args) {
|
|
2245
|
+
const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
|
|
2246
|
+
[resourceKey(instance.workflowResource), client]
|
|
2247
|
+
]), stateUris = [
|
|
2248
|
+
...collectSlotDocUris(instance.state),
|
|
2249
|
+
...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
|
|
2250
|
+
];
|
|
2251
|
+
for (const uri of stateUris) {
|
|
2252
|
+
const parsed = tryParseGdr(uri);
|
|
2253
|
+
if (parsed === void 0) continue;
|
|
2254
|
+
const key = resourceKey(resourceFromParsed(parsed));
|
|
2255
|
+
clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
|
|
2256
|
+
}
|
|
2257
|
+
const perResource = await Promise.all(
|
|
2258
|
+
[...clientsByResource.values()].map(
|
|
2259
|
+
(c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
|
|
2260
|
+
t: GUARD_DOC_TYPE,
|
|
2261
|
+
id: instance._id
|
|
2262
|
+
})
|
|
2263
|
+
)
|
|
2264
|
+
);
|
|
2265
|
+
return dedupById(perResource.flat());
|
|
2266
|
+
}
|
|
2267
|
+
function tryParseGdr(uri) {
|
|
2268
|
+
try {
|
|
2269
|
+
return parseGdr(uri);
|
|
2270
|
+
} catch {
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function dedupById(guards) {
|
|
2275
|
+
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
2276
|
+
}
|
|
2277
|
+
function definitionDocId(_workflowResource, prefix, workflowId, version) {
|
|
2278
|
+
return `${prefix}.${workflowId}.v${version}`;
|
|
2279
|
+
}
|
|
2280
|
+
async function sortByDependencies(client, definitions, tags, workflowResource) {
|
|
2281
|
+
const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
|
|
2282
|
+
for (const def of definitions) {
|
|
2283
|
+
const list = byWorkflowId.get(def.workflowId) ?? [];
|
|
2284
|
+
list.push(def), byWorkflowId.set(def.workflowId, list);
|
|
2285
|
+
}
|
|
2286
|
+
const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
|
|
2287
|
+
return await assertCrossBatchRefsDeployed(client, definitions, {
|
|
2288
|
+
tags,
|
|
2289
|
+
workflowResource,
|
|
2290
|
+
prefix,
|
|
2291
|
+
findInBatch
|
|
2292
|
+
}), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
|
|
2293
|
+
}
|
|
2294
|
+
function findRefInBatch(byWorkflowId, ref) {
|
|
2295
|
+
const candidates = byWorkflowId.get(ref.workflowId);
|
|
2296
|
+
if (!(candidates === void 0 || candidates.length === 0))
|
|
2297
|
+
return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
|
|
2298
|
+
(best, c) => best === void 0 || c.version > best.version ? c : best,
|
|
2299
|
+
void 0
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
2303
|
+
const missing = [];
|
|
2304
|
+
for (const def of definitions) {
|
|
2305
|
+
const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
|
|
2306
|
+
for (const ref of refsOf(def)) {
|
|
2307
|
+
if (ctx.findInBatch(ref) !== void 0) continue;
|
|
2308
|
+
const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
|
|
2309
|
+
label !== void 0 && missing.push({ from: fromId, ref: label });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (missing.length === 0) return;
|
|
2313
|
+
const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
`workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
|
|
2316
|
+
` + lines.join(`
|
|
2317
|
+
`)
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
async function resolveDeployedRefLabel(client, ref, tags) {
|
|
2321
|
+
const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
|
|
2322
|
+
if (wantsExplicit && (params.version = ref.version), !await client.fetch(
|
|
2323
|
+
definitionLookupGroq(wantsExplicit),
|
|
2324
|
+
params
|
|
2325
|
+
))
|
|
2326
|
+
return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
|
|
2327
|
+
}
|
|
2328
|
+
function topoSortDefinitions(definitions, ctx) {
|
|
2329
|
+
const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
|
|
2330
|
+
const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
|
|
2331
|
+
if (!visited.has(id)) {
|
|
2332
|
+
if (visiting.has(id))
|
|
2333
|
+
throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
|
|
2334
|
+
visiting.add(id);
|
|
2335
|
+
for (const ref of refsOf(def)) {
|
|
2336
|
+
const child = ctx.findInBatch(ref);
|
|
2337
|
+
child !== void 0 && visit(child);
|
|
2338
|
+
}
|
|
2339
|
+
visiting.delete(id), visited.add(id), ordered.push(def);
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
for (const def of definitions) visit(def);
|
|
2343
|
+
return ordered;
|
|
2344
|
+
}
|
|
2345
|
+
function refsOf(def) {
|
|
2346
|
+
const out = [];
|
|
2347
|
+
for (const stage of def.stages)
|
|
2348
|
+
for (const task of stage.tasks ?? []) {
|
|
2349
|
+
const ref = task.spawns?.definitionRef;
|
|
2350
|
+
ref !== void 0 && out.push({
|
|
2351
|
+
workflowId: ref.workflowId,
|
|
2352
|
+
...ref.version !== void 0 ? { version: ref.version } : {}
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
return out;
|
|
2356
|
+
}
|
|
2357
|
+
function isDefinitionUnchanged(existing, expected) {
|
|
2358
|
+
return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
|
|
2359
|
+
}
|
|
2360
|
+
function stripSystemFields(doc) {
|
|
2361
|
+
const out = {};
|
|
2362
|
+
for (const [k, v2] of Object.entries(doc))
|
|
2363
|
+
k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v2);
|
|
2364
|
+
return out;
|
|
2365
|
+
}
|
|
2366
|
+
function stableStringify(value) {
|
|
2367
|
+
return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
|
|
2368
|
+
([a], [b]) => a.localeCompare(b)
|
|
2369
|
+
).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
|
|
2370
|
+
}
|
|
2371
|
+
async function loadDefinition(client, workflowId, version, tags) {
|
|
2372
|
+
if (version !== void 0) {
|
|
2373
|
+
const doc = await client.fetch(
|
|
2374
|
+
definitionLookupGroq(!0),
|
|
2375
|
+
{ workflowId, version, engineTags: tags }
|
|
2376
|
+
);
|
|
2377
|
+
if (!doc)
|
|
2378
|
+
throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
|
|
2379
|
+
return doc;
|
|
2380
|
+
}
|
|
2381
|
+
const latest = await client.fetch(definitionLookupGroq(!1), {
|
|
2382
|
+
workflowId,
|
|
2383
|
+
engineTags: tags
|
|
2384
|
+
});
|
|
2385
|
+
if (!latest)
|
|
2386
|
+
throw new Error(`No deployed definition for workflow ${workflowId}`);
|
|
2387
|
+
return latest;
|
|
2388
|
+
}
|
|
2389
|
+
const STATE_OP_TYPES = [
|
|
2390
|
+
"workflow.op.state.set",
|
|
2391
|
+
"workflow.op.state.append",
|
|
2392
|
+
"workflow.op.state.updateWhere",
|
|
2393
|
+
"workflow.op.state.removeWhere"
|
|
2394
|
+
];
|
|
2395
|
+
function isStateOp(summary) {
|
|
2396
|
+
return STATE_OP_TYPES.includes(summary.opType);
|
|
2397
|
+
}
|
|
2398
|
+
function validateActionParams(action, taskId, callerParams) {
|
|
2399
|
+
const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
|
|
2400
|
+
for (const decl of declared) {
|
|
2401
|
+
const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
|
|
2402
|
+
if (decl.required === !0 && !present) {
|
|
2403
|
+
issues.push({ paramId: decl.id, reason: "required but missing" });
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
present && (checkParamType(value, decl) || issues.push({
|
|
2407
|
+
paramId: decl.id,
|
|
2408
|
+
reason: `expected paramType=${decl.paramType}, got ${typeof value}`
|
|
2409
|
+
}));
|
|
2410
|
+
}
|
|
2411
|
+
if (issues.length > 0)
|
|
2412
|
+
throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
|
|
2413
|
+
return params;
|
|
2414
|
+
}
|
|
2415
|
+
function hasStringField(value, field) {
|
|
2416
|
+
return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
|
|
2417
|
+
}
|
|
2418
|
+
function checkParamType(value, decl) {
|
|
2419
|
+
switch (decl.paramType) {
|
|
2420
|
+
case "string":
|
|
2421
|
+
case "url":
|
|
2422
|
+
case "dateTime":
|
|
2423
|
+
return typeof value == "string";
|
|
2424
|
+
case "number":
|
|
2425
|
+
return typeof value == "number" && Number.isFinite(value);
|
|
2426
|
+
case "boolean":
|
|
1764
2427
|
return typeof value == "boolean";
|
|
1765
2428
|
case "actor":
|
|
1766
2429
|
return hasStringField(value, "kind");
|
|
1767
2430
|
case "doc.ref":
|
|
1768
2431
|
return hasStringField(value, "id");
|
|
1769
2432
|
case "doc.refs":
|
|
1770
|
-
return Array.isArray(value) && value.every((
|
|
2433
|
+
return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
|
|
1771
2434
|
case "json":
|
|
1772
2435
|
return !0;
|
|
1773
2436
|
default:
|
|
@@ -1775,15 +2438,15 @@ function checkParamType(value, decl) {
|
|
|
1775
2438
|
}
|
|
1776
2439
|
}
|
|
1777
2440
|
async function runActionOps(args) {
|
|
1778
|
-
const { ops, mutation, stageId, taskId, actionName, params, actor } = args;
|
|
2441
|
+
const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
|
|
1779
2442
|
if (ops === void 0 || ops.length === 0) return [];
|
|
1780
2443
|
const summaries = [];
|
|
1781
2444
|
for (const op of ops) {
|
|
1782
|
-
const summary = applyOp(op, { mutation, stageId, taskId, params, actor });
|
|
2445
|
+
const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
|
|
1783
2446
|
summaries.push(summary), mutation.history.push({
|
|
1784
2447
|
_key: randomKey(),
|
|
1785
2448
|
_type: "workflow.history.opApplied",
|
|
1786
|
-
at:
|
|
2449
|
+
at: now,
|
|
1787
2450
|
stageId,
|
|
1788
2451
|
taskId,
|
|
1789
2452
|
actionId: actionName,
|
|
@@ -1842,8 +2505,8 @@ function applyStateUpdateWhere(op, ctx) {
|
|
|
1842
2505
|
/*createIfMissing*/
|
|
1843
2506
|
!1
|
|
1844
2507
|
), resolvedMerge = {};
|
|
1845
|
-
for (const [k,
|
|
1846
|
-
resolvedMerge[k] = resolveOpSource(
|
|
2508
|
+
for (const [k, v2] of Object.entries(op.merge))
|
|
2509
|
+
resolvedMerge[k] = resolveOpSource(v2, ctx);
|
|
1847
2510
|
return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
|
|
1848
2511
|
(row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
|
|
1849
2512
|
)), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
|
|
@@ -1886,18 +2549,11 @@ function locateSlot(ctx, target, createIfMissing) {
|
|
|
1886
2549
|
}, host.push(slot)), slot;
|
|
1887
2550
|
}
|
|
1888
2551
|
function resolveOpSource(src, ctx) {
|
|
2552
|
+
const staticValue = resolveStaticSource(src, ctx);
|
|
2553
|
+
if (staticValue.handled) return staticValue.value;
|
|
1889
2554
|
switch (src.source) {
|
|
1890
|
-
case "literal":
|
|
1891
|
-
return src.value;
|
|
1892
|
-
case "param":
|
|
1893
|
-
return ctx.params[src.paramId];
|
|
1894
|
-
case "actor":
|
|
1895
|
-
return ctx.actor;
|
|
1896
|
-
case "now":
|
|
1897
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1898
2555
|
case "self":
|
|
1899
|
-
return;
|
|
1900
|
-
// Not applicable inside ops; reserved for higher-level binding.
|
|
2556
|
+
return ctx.self;
|
|
1901
2557
|
case "stageId":
|
|
1902
2558
|
return ctx.stageId;
|
|
1903
2559
|
case "stateRead": {
|
|
@@ -1907,8 +2563,8 @@ function resolveOpSource(src, ctx) {
|
|
|
1907
2563
|
case "effectOutput": {
|
|
1908
2564
|
const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
|
|
1909
2565
|
if (entry === void 0) return null;
|
|
1910
|
-
const
|
|
1911
|
-
return
|
|
2566
|
+
const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
|
|
2567
|
+
return v2 === void 0 ? null : v2;
|
|
1912
2568
|
}
|
|
1913
2569
|
case "object": {
|
|
1914
2570
|
const out = {};
|
|
@@ -1916,6 +2572,11 @@ function resolveOpSource(src, ctx) {
|
|
|
1916
2572
|
out[field] = resolveOpSource(fieldSrc, ctx);
|
|
1917
2573
|
return out;
|
|
1918
2574
|
}
|
|
2575
|
+
case "row":
|
|
2576
|
+
case "parentState":
|
|
2577
|
+
return;
|
|
2578
|
+
default:
|
|
2579
|
+
return;
|
|
1919
2580
|
}
|
|
1920
2581
|
}
|
|
1921
2582
|
function slotValue(slots, slotId) {
|
|
@@ -1934,333 +2595,101 @@ function evalOpPredicate(p, row, ctx) {
|
|
|
1934
2595
|
const target = resolveOpSource(p.equals, ctx);
|
|
1935
2596
|
return row[p.field] === target;
|
|
1936
2597
|
}
|
|
1937
|
-
async function fireAction(args) {
|
|
1938
|
-
const { client, instanceId, taskId, action, params, options } = args
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
}
|
|
1955
|
-
function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor) {
|
|
1956
|
-
if (action.setStatus === void 0) return;
|
|
1957
|
-
const initialStatus = mutEntry.status, newStatus = action.setStatus;
|
|
1958
|
-
return mutEntry.status = newStatus, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), actor && (mutEntry.completedBy = actor.id), mutation.history.push(
|
|
1959
|
-
taskStatusChangedEntry({
|
|
1960
|
-
stageId,
|
|
1961
|
-
taskId,
|
|
1962
|
-
from: initialStatus,
|
|
1963
|
-
to: newStatus,
|
|
1964
|
-
...actor !== void 0 ? { actor } : {}
|
|
1965
|
-
})
|
|
1966
|
-
), newStatus;
|
|
1967
|
-
}
|
|
1968
|
-
async function commitAction(ctx, taskId, actionName, callerParams, actor) {
|
|
1969
|
-
const { stage, task, action, params } = await resolveActionCommit(
|
|
1970
|
-
ctx,
|
|
1971
|
-
taskId,
|
|
1972
|
-
actionName,
|
|
1973
|
-
callerParams
|
|
1974
|
-
), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
|
|
1975
|
-
mutation.history.push({
|
|
1976
|
-
_key: randomKey(),
|
|
1977
|
-
_type: "workflow.history.actionFired",
|
|
1978
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1979
|
-
stageId: stage.id,
|
|
1980
|
-
taskId,
|
|
1981
|
-
actionId: actionName,
|
|
1982
|
-
...actor !== void 0 ? { actor } : {}
|
|
1983
|
-
});
|
|
1984
|
-
const ranOps = await runActionOps({
|
|
1985
|
-
ops: action.ops,
|
|
1986
|
-
mutation,
|
|
1987
|
-
stageId: stage.id,
|
|
1988
|
-
taskId,
|
|
1989
|
-
actionName,
|
|
1990
|
-
params,
|
|
1991
|
-
actor
|
|
1992
|
-
}), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor);
|
|
1993
|
-
return await queueEffects(
|
|
1994
|
-
ctx,
|
|
1995
|
-
mutation,
|
|
1996
|
-
action.effects,
|
|
1997
|
-
{
|
|
1998
|
-
kind: "action",
|
|
1999
|
-
id: `${task.id}:${actionName}`
|
|
2000
|
-
},
|
|
2001
|
-
actor,
|
|
2002
|
-
params
|
|
2003
|
-
), await persist(ctx, mutation), {
|
|
2004
|
-
fired: !0,
|
|
2005
|
-
taskId,
|
|
2006
|
-
action: actionName,
|
|
2007
|
-
...newStatus !== void 0 ? { newStatus } : {},
|
|
2008
|
-
...ranOps.length > 0 ? { ranOps } : {}
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
const parsedFilters = /* @__PURE__ */ new Map();
|
|
2012
|
-
async function matchesFilter(args) {
|
|
2013
|
-
const { document, filter, userId } = args;
|
|
2014
|
-
parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
|
|
2015
|
-
const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
|
|
2016
|
-
dataset: [document],
|
|
2017
|
-
...userId !== void 0 ? { identity: userId } : {}
|
|
2018
|
-
})).get();
|
|
2019
|
-
return Array.isArray(data) && data.length === 1;
|
|
2020
|
-
}
|
|
2021
|
-
async function grantsPermissionOn(args) {
|
|
2022
|
-
const { document, grants, permission, userId } = args;
|
|
2023
|
-
if (!document || grants.length === 0) return !1;
|
|
2024
|
-
for (const grant of grants)
|
|
2025
|
-
if (grant.permissions.includes(permission) && await matchesFilter({
|
|
2026
|
-
document,
|
|
2027
|
-
filter: grant.filter,
|
|
2028
|
-
...userId !== void 0 ? { userId } : {}
|
|
2029
|
-
}))
|
|
2030
|
-
return !0;
|
|
2031
|
-
return !1;
|
|
2032
|
-
}
|
|
2033
|
-
async function fetchGrants(args) {
|
|
2034
|
-
const { client, resourcePath, signal } = args;
|
|
2035
|
-
return client.request({ url: resourcePath, ...signal ? { signal } : {} });
|
|
2036
|
-
}
|
|
2037
|
-
const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
|
|
2038
|
-
async function resolveAccess(client, args = {}) {
|
|
2039
|
-
const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
|
|
2040
|
-
if (overrideActor !== void 0 && overrideGrants !== void 0)
|
|
2041
|
-
return { actor: overrideActor, grants: overrideGrants };
|
|
2042
|
-
const requestFn = client.request;
|
|
2043
|
-
if (requestFn === void 0) {
|
|
2044
|
-
if (overrideActor === void 0)
|
|
2045
|
-
throw new Error(
|
|
2046
|
-
"workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
|
|
2047
|
-
);
|
|
2048
|
-
return { actor: overrideActor };
|
|
2049
|
-
}
|
|
2050
|
-
const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : args.grantsFromPath !== void 0 ? cachedGrants(client, requestFn, args.grantsFromPath) : Promise.resolve(void 0), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
|
|
2051
|
-
if (actor === void 0)
|
|
2052
|
-
throw new Error(
|
|
2053
|
-
"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."
|
|
2054
|
-
);
|
|
2055
|
-
return { actor, ...grants !== void 0 ? { grants } : {} };
|
|
2056
|
-
}
|
|
2057
|
-
function cachedActor(client, requestFn) {
|
|
2058
|
-
const cached = actorCache.get(client);
|
|
2059
|
-
if (cached !== void 0) return cached;
|
|
2060
|
-
const pending = fetchActor(requestFn).catch((err) => {
|
|
2061
|
-
throw actorCache.get(client) === pending && actorCache.delete(client), err;
|
|
2062
|
-
});
|
|
2063
|
-
return actorCache.set(client, pending), pending;
|
|
2064
|
-
}
|
|
2065
|
-
function cachedGrants(client, requestFn, resourcePath) {
|
|
2066
|
-
let byPath = grantsCache.get(client);
|
|
2067
|
-
byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
|
|
2068
|
-
let cached = byPath.get(resourcePath);
|
|
2069
|
-
return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
|
|
2070
|
-
}
|
|
2071
|
-
async function fetchActor(requestFn) {
|
|
2072
|
-
let user;
|
|
2073
|
-
try {
|
|
2074
|
-
user = await requestFn({
|
|
2075
|
-
uri: "/users/me",
|
|
2076
|
-
tag: "workflow.access.resolve-actor"
|
|
2077
|
-
});
|
|
2078
|
-
} catch (err) {
|
|
2079
|
-
throw new Error(
|
|
2080
|
-
'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
|
|
2081
|
-
{ cause: err }
|
|
2082
|
-
);
|
|
2083
|
-
}
|
|
2084
|
-
if (!user || typeof user.id != "string" || user.id.length === 0) return;
|
|
2085
|
-
const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
|
|
2086
|
-
return {
|
|
2087
|
-
kind: "user",
|
|
2088
|
-
id: user.id,
|
|
2089
|
-
...roleNames.length > 0 ? { roles: roleNames } : {}
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
async function fetchGrantsCached(requestFn, resourcePath) {
|
|
2093
|
-
try {
|
|
2094
|
-
return await fetchGrants({ client: { request: requestFn }, resourcePath });
|
|
2095
|
-
} catch (err) {
|
|
2096
|
-
console.warn(
|
|
2097
|
-
`workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
|
|
2098
|
-
);
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
2103
|
-
function validateTags(tags) {
|
|
2104
|
-
if (tags.length === 0)
|
|
2105
|
-
throw new Error("tags: must be a non-empty array");
|
|
2106
|
-
for (const tag of tags)
|
|
2107
|
-
if (!TAG_RE.test(tag))
|
|
2108
|
-
throw new Error(
|
|
2109
|
-
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
2110
|
-
);
|
|
2111
|
-
}
|
|
2112
|
-
function canonicalTag(tags) {
|
|
2113
|
-
return tags[0];
|
|
2114
|
-
}
|
|
2115
|
-
const WILDCARD_ROLE = "*";
|
|
2116
|
-
async function evaluateInstance(args) {
|
|
2117
|
-
const { client, tags, instanceId, resourceClients } = args;
|
|
2118
|
-
validateTags(tags);
|
|
2119
|
-
const { actor, grants } = await resolveAccess(client, {
|
|
2120
|
-
...args.access !== void 0 ? { override: args.access } : {},
|
|
2121
|
-
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
2122
|
-
}), instance = await client.getDocument(instanceId);
|
|
2123
|
-
if (!instance)
|
|
2124
|
-
throw new Error(`Workflow instance "${instanceId}" not found`);
|
|
2125
|
-
if (!tags.some((t) => instance.tags?.includes(t)))
|
|
2126
|
-
throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
|
|
2127
|
-
const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.id === instance.currentStageId);
|
|
2128
|
-
if (stage === void 0)
|
|
2129
|
-
throw new Error(
|
|
2130
|
-
`Instance "${instanceId}" currentStageId "${instance.currentStageId}" not in definition`
|
|
2131
|
-
);
|
|
2132
|
-
const snapshot = await hydrateSnapshot({
|
|
2133
|
-
client,
|
|
2134
|
-
clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client,
|
|
2135
|
-
instance,
|
|
2136
|
-
definition
|
|
2137
|
-
}), currentTaskEntries = instance.stages.find(
|
|
2138
|
-
(s) => s.id === instance.currentStageId && s.exitedAt === void 0
|
|
2139
|
-
)?.tasks ?? [], taskEvaluations = [];
|
|
2140
|
-
for (const task of stage.tasks ?? [])
|
|
2141
|
-
taskEvaluations.push(
|
|
2142
|
-
await evaluateTask({
|
|
2143
|
-
task,
|
|
2144
|
-
statusEntry: currentTaskEntries.find((t) => t.id === task.id),
|
|
2145
|
-
instance,
|
|
2146
|
-
definition,
|
|
2147
|
-
actor,
|
|
2148
|
-
grants,
|
|
2149
|
-
snapshot
|
|
2150
|
-
})
|
|
2151
|
-
);
|
|
2152
|
-
const transitionEvaluations = [];
|
|
2153
|
-
for (const transition of stage.transitions ?? [])
|
|
2154
|
-
transitionEvaluations.push({
|
|
2155
|
-
transition,
|
|
2156
|
-
filterSatisfied: await evaluateFilter({
|
|
2157
|
-
filter: transition.filter,
|
|
2158
|
-
definition,
|
|
2159
|
-
snapshot,
|
|
2160
|
-
params: buildParams(instance)
|
|
2161
|
-
})
|
|
2162
|
-
});
|
|
2163
|
-
const currentStage = {
|
|
2164
|
-
stage,
|
|
2165
|
-
tasks: taskEvaluations,
|
|
2166
|
-
transitions: transitionEvaluations
|
|
2167
|
-
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
|
|
2168
|
-
return {
|
|
2169
|
-
instance,
|
|
2170
|
-
definition,
|
|
2171
|
-
actor,
|
|
2172
|
-
currentStage,
|
|
2173
|
-
pendingOnYou,
|
|
2174
|
-
canInteract
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
async function evaluateTask(args) {
|
|
2178
|
-
const { task, statusEntry, instance, definition, actor, grants, snapshot } = args, status = statusEntry?.status ?? "pending", actions = [];
|
|
2179
|
-
for (const action of task.actions ?? [])
|
|
2180
|
-
actions.push(
|
|
2181
|
-
await evaluateAction({
|
|
2182
|
-
action,
|
|
2183
|
-
task,
|
|
2184
|
-
...statusEntry !== void 0 ? { statusEntry } : {},
|
|
2185
|
-
status,
|
|
2186
|
-
instance,
|
|
2187
|
-
definition,
|
|
2188
|
-
actor,
|
|
2189
|
-
...grants !== void 0 ? { grants } : {},
|
|
2190
|
-
snapshot
|
|
2191
|
-
})
|
|
2192
|
-
);
|
|
2193
|
-
return {
|
|
2194
|
-
task,
|
|
2195
|
-
status,
|
|
2196
|
-
// "pending on actor" treats both `pending` and `active` as waiting on
|
|
2197
|
-
// the assignee — pending tasks just haven't been auto-invoked yet, but
|
|
2198
|
-
// they're still inbox items.
|
|
2199
|
-
pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
|
|
2200
|
-
actions
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
async function evaluateAction(args) {
|
|
2204
|
-
const { action, task, status, instance, definition, actor, grants, snapshot } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
|
|
2205
|
-
if (syncReason !== void 0) return disabled(action, syncReason);
|
|
2206
|
-
const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot);
|
|
2207
|
-
return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
|
|
2208
|
-
}
|
|
2209
|
-
function lifecycleReason(instance, definition, status) {
|
|
2210
|
-
if (instance.completedAt !== void 0)
|
|
2211
|
-
return { kind: "instance-completed", completedAt: instance.completedAt };
|
|
2212
|
-
if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
|
|
2213
|
-
return { kind: "stage-terminal", stageId: instance.currentStageId };
|
|
2214
|
-
if (status === "done" || status === "skipped" || status === "failed")
|
|
2215
|
-
return { kind: "task-not-active", status };
|
|
2216
|
-
}
|
|
2217
|
-
function actorReason(action, task, actor) {
|
|
2218
|
-
const actorRoles = actor.roles ?? [];
|
|
2219
|
-
if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
|
|
2220
|
-
return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
|
|
2221
|
-
if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
|
|
2222
|
-
return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
|
|
2223
|
-
}
|
|
2224
|
-
async function permissionReason(action, instance, actor, grants) {
|
|
2225
|
-
if (grants === void 0) return;
|
|
2226
|
-
const permission = action.requiredPermission ?? "update";
|
|
2227
|
-
if (!await grantsPermissionOn({
|
|
2228
|
-
document: instance,
|
|
2229
|
-
grants,
|
|
2230
|
-
permission,
|
|
2231
|
-
userId: actor.id
|
|
2232
|
-
}))
|
|
2233
|
-
return { kind: "permission-denied", permission, matchingGrants: grants.length };
|
|
2234
|
-
}
|
|
2235
|
-
async function filterReason(action, instance, definition, snapshot) {
|
|
2236
|
-
if (!(action.filter === void 0 || await evaluateFilter({
|
|
2237
|
-
filter: action.filter,
|
|
2238
|
-
definition,
|
|
2239
|
-
snapshot,
|
|
2240
|
-
params: buildParams(instance)
|
|
2241
|
-
})))
|
|
2242
|
-
return { kind: "filter-failed", filter: action.filter };
|
|
2243
|
-
}
|
|
2244
|
-
function disabled(action, reason) {
|
|
2245
|
-
return { action, allowed: !1, disabledReason: reason };
|
|
2246
|
-
}
|
|
2247
|
-
function actorIsAssignee(actor, assignees) {
|
|
2248
|
-
if (assignees.length === 0) return !1;
|
|
2249
|
-
const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
|
|
2250
|
-
for (const assignee of assignees)
|
|
2251
|
-
if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
|
|
2252
|
-
return !0;
|
|
2253
|
-
return !1;
|
|
2598
|
+
async function fireAction(args) {
|
|
2599
|
+
const { client, instanceId, taskId, action, params, options } = args;
|
|
2600
|
+
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
2601
|
+
const ctx = await loadContext(client, instanceId, {
|
|
2602
|
+
...options?.clock ? { clock: options.clock } : {}
|
|
2603
|
+
});
|
|
2604
|
+
try {
|
|
2605
|
+
return await commitAction(ctx, taskId, action, params, options?.actor);
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
if (!isRevisionConflict(error)) throw error;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
throw new ConcurrentFireActionError({
|
|
2611
|
+
instanceId,
|
|
2612
|
+
taskId,
|
|
2613
|
+
action,
|
|
2614
|
+
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
2615
|
+
});
|
|
2254
2616
|
}
|
|
2255
|
-
function
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2617
|
+
async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
|
|
2618
|
+
const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
|
|
2619
|
+
if (action === void 0)
|
|
2620
|
+
throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
|
|
2621
|
+
if (!await ctxEvaluateFilter(ctx, action.filter))
|
|
2622
|
+
throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
|
|
2623
|
+
const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
|
|
2624
|
+
if (entry === void 0 || entry.status !== "active")
|
|
2259
2625
|
throw new Error(
|
|
2260
|
-
`
|
|
2261
|
-
{ cause: err }
|
|
2626
|
+
`Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
|
|
2262
2627
|
);
|
|
2263
|
-
|
|
2628
|
+
const params = validateActionParams(action, taskId, callerParams);
|
|
2629
|
+
return { stage, task, action, params };
|
|
2630
|
+
}
|
|
2631
|
+
function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
|
|
2632
|
+
if (action.setStatus === void 0) return;
|
|
2633
|
+
const initialStatus = mutEntry.status, newStatus = action.setStatus;
|
|
2634
|
+
return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
|
|
2635
|
+
taskStatusChangedEntry({
|
|
2636
|
+
stageId,
|
|
2637
|
+
taskId,
|
|
2638
|
+
from: initialStatus,
|
|
2639
|
+
to: newStatus,
|
|
2640
|
+
at: now,
|
|
2641
|
+
...actor !== void 0 ? { actor } : {}
|
|
2642
|
+
})
|
|
2643
|
+
), newStatus;
|
|
2644
|
+
}
|
|
2645
|
+
async function commitAction(ctx, taskId, actionName, callerParams, actor) {
|
|
2646
|
+
const { stage, task, action, params } = await resolveActionCommit(
|
|
2647
|
+
ctx,
|
|
2648
|
+
taskId,
|
|
2649
|
+
actionName,
|
|
2650
|
+
callerParams
|
|
2651
|
+
), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
|
|
2652
|
+
mutation.history.push({
|
|
2653
|
+
_key: randomKey(),
|
|
2654
|
+
_type: "workflow.history.actionFired",
|
|
2655
|
+
at: now,
|
|
2656
|
+
stageId: stage.id,
|
|
2657
|
+
taskId,
|
|
2658
|
+
actionId: actionName,
|
|
2659
|
+
...actor !== void 0 ? { actor } : {}
|
|
2660
|
+
});
|
|
2661
|
+
const ranOps = await runActionOps({
|
|
2662
|
+
ops: action.ops,
|
|
2663
|
+
mutation,
|
|
2664
|
+
stageId: stage.id,
|
|
2665
|
+
taskId,
|
|
2666
|
+
actionName,
|
|
2667
|
+
params,
|
|
2668
|
+
actor,
|
|
2669
|
+
self: selfGdr(ctx.instance),
|
|
2670
|
+
now
|
|
2671
|
+
}), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
|
|
2672
|
+
return await queueEffects(
|
|
2673
|
+
ctx,
|
|
2674
|
+
mutation,
|
|
2675
|
+
action.effects,
|
|
2676
|
+
{
|
|
2677
|
+
kind: "action",
|
|
2678
|
+
id: `${task.id}:${actionName}`
|
|
2679
|
+
},
|
|
2680
|
+
actor,
|
|
2681
|
+
params
|
|
2682
|
+
), await persistThenDeploy(
|
|
2683
|
+
ctx,
|
|
2684
|
+
mutation,
|
|
2685
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
|
|
2686
|
+
), {
|
|
2687
|
+
fired: !0,
|
|
2688
|
+
taskId,
|
|
2689
|
+
action: actionName,
|
|
2690
|
+
...newStatus !== void 0 ? { newStatus } : {},
|
|
2691
|
+
...ranOps.length > 0 ? { ranOps } : {}
|
|
2692
|
+
};
|
|
2264
2693
|
}
|
|
2265
2694
|
class ActionDisabledError extends Error {
|
|
2266
2695
|
reason;
|
|
@@ -2284,121 +2713,6 @@ function formatDisabledReason(taskId, action, reason) {
|
|
|
2284
2713
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
2285
2714
|
return `Action "${taskId}:${action}" is not allowed: ${detail}`;
|
|
2286
2715
|
}
|
|
2287
|
-
function definitionDocId(_workflowResource, prefix, workflowId, version) {
|
|
2288
|
-
return `${prefix}.${workflowId}.v${version}`;
|
|
2289
|
-
}
|
|
2290
|
-
async function sortByDependencies(client, definitions, tags, workflowResource) {
|
|
2291
|
-
const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
|
|
2292
|
-
for (const def of definitions) {
|
|
2293
|
-
const list = byWorkflowId.get(def.workflowId) ?? [];
|
|
2294
|
-
list.push(def), byWorkflowId.set(def.workflowId, list);
|
|
2295
|
-
}
|
|
2296
|
-
const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
|
|
2297
|
-
return await assertCrossBatchRefsDeployed(client, definitions, {
|
|
2298
|
-
tags,
|
|
2299
|
-
workflowResource,
|
|
2300
|
-
prefix,
|
|
2301
|
-
findInBatch
|
|
2302
|
-
}), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
|
|
2303
|
-
}
|
|
2304
|
-
function findRefInBatch(byWorkflowId, ref) {
|
|
2305
|
-
const candidates = byWorkflowId.get(ref.workflowId);
|
|
2306
|
-
if (!(candidates === void 0 || candidates.length === 0))
|
|
2307
|
-
return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
|
|
2308
|
-
(best, c) => best === void 0 || c.version > best.version ? c : best,
|
|
2309
|
-
void 0
|
|
2310
|
-
);
|
|
2311
|
-
}
|
|
2312
|
-
async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
2313
|
-
const missing = [];
|
|
2314
|
-
for (const def of definitions) {
|
|
2315
|
-
const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
|
|
2316
|
-
for (const ref of refsOf(def)) {
|
|
2317
|
-
if (ctx.findInBatch(ref) !== void 0) continue;
|
|
2318
|
-
const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
|
|
2319
|
-
label !== void 0 && missing.push({ from: fromId, ref: label });
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
if (missing.length === 0) return;
|
|
2323
|
-
const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
|
|
2324
|
-
throw new Error(
|
|
2325
|
-
`workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
|
|
2326
|
-
` + lines.join(`
|
|
2327
|
-
`)
|
|
2328
|
-
);
|
|
2329
|
-
}
|
|
2330
|
-
async function resolveDeployedRefLabel(client, ref, tags) {
|
|
2331
|
-
const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
|
|
2332
|
-
if (wantsExplicit && (params.version = ref.version), !await client.fetch(
|
|
2333
|
-
definitionLookupGroq(wantsExplicit),
|
|
2334
|
-
params
|
|
2335
|
-
))
|
|
2336
|
-
return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
|
|
2337
|
-
}
|
|
2338
|
-
function topoSortDefinitions(definitions, ctx) {
|
|
2339
|
-
const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
|
|
2340
|
-
const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
|
|
2341
|
-
if (!visited.has(id)) {
|
|
2342
|
-
if (visiting.has(id))
|
|
2343
|
-
throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
|
|
2344
|
-
visiting.add(id);
|
|
2345
|
-
for (const ref of refsOf(def)) {
|
|
2346
|
-
const child = ctx.findInBatch(ref);
|
|
2347
|
-
child !== void 0 && visit(child);
|
|
2348
|
-
}
|
|
2349
|
-
visiting.delete(id), visited.add(id), ordered.push(def);
|
|
2350
|
-
}
|
|
2351
|
-
};
|
|
2352
|
-
for (const def of definitions) visit(def);
|
|
2353
|
-
return ordered;
|
|
2354
|
-
}
|
|
2355
|
-
function definitionLookupGroq(wantsExplicit) {
|
|
2356
|
-
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]';
|
|
2357
|
-
}
|
|
2358
|
-
function refsOf(def) {
|
|
2359
|
-
const out = [];
|
|
2360
|
-
for (const stage of def.stages)
|
|
2361
|
-
for (const task of stage.tasks ?? []) {
|
|
2362
|
-
const ref = task.spawns?.definitionRef;
|
|
2363
|
-
ref !== void 0 && out.push({
|
|
2364
|
-
workflowId: ref.workflowId,
|
|
2365
|
-
...ref.version !== void 0 ? { version: ref.version } : {}
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
return out;
|
|
2369
|
-
}
|
|
2370
|
-
function isDefinitionUnchanged(existing, expected) {
|
|
2371
|
-
return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
|
|
2372
|
-
}
|
|
2373
|
-
function stripSystemFields(doc) {
|
|
2374
|
-
const out = {};
|
|
2375
|
-
for (const [k, v] of Object.entries(doc))
|
|
2376
|
-
k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v);
|
|
2377
|
-
return out;
|
|
2378
|
-
}
|
|
2379
|
-
function stableStringify(value) {
|
|
2380
|
-
return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
|
|
2381
|
-
([a], [b]) => a.localeCompare(b)
|
|
2382
|
-
).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2383
|
-
}
|
|
2384
|
-
async function loadDefinition(client, workflowId, version, tags) {
|
|
2385
|
-
if (version !== void 0) {
|
|
2386
|
-
const doc = await client.fetch(
|
|
2387
|
-
"*[_type == 'workflow.definition' && workflowId == $id && version == $v && count(tags[@ in $engineTags]) > 0][0]",
|
|
2388
|
-
{ id: workflowId, v: version, engineTags: tags }
|
|
2389
|
-
);
|
|
2390
|
-
if (!doc)
|
|
2391
|
-
throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
|
|
2392
|
-
return doc;
|
|
2393
|
-
}
|
|
2394
|
-
const latest = await client.fetch(
|
|
2395
|
-
"*[_type == 'workflow.definition' && workflowId == $id && count(tags[@ in $engineTags]) > 0] | order(version desc)[0]",
|
|
2396
|
-
{ id: workflowId, engineTags: tags }
|
|
2397
|
-
);
|
|
2398
|
-
if (!latest)
|
|
2399
|
-
throw new Error(`No deployed definition for workflow ${workflowId}`);
|
|
2400
|
-
return latest;
|
|
2401
|
-
}
|
|
2402
2716
|
function bareIdFromSpawnRef(uri) {
|
|
2403
2717
|
if (!uri.includes(":")) return [uri];
|
|
2404
2718
|
try {
|
|
@@ -2419,9 +2733,16 @@ async function resolveOperationContext(args) {
|
|
|
2419
2733
|
clientForGdr: buildClientForGdr(args.client, args.resourceClients)
|
|
2420
2734
|
};
|
|
2421
2735
|
}
|
|
2422
|
-
async function cascade(client, instanceId, actor, clientForGdr) {
|
|
2423
|
-
const count = await cascadeAutoTransitions(
|
|
2424
|
-
|
|
2736
|
+
async function cascade(client, instanceId, actor, clientForGdr, clock, overlay) {
|
|
2737
|
+
const count = await cascadeAutoTransitions(
|
|
2738
|
+
client,
|
|
2739
|
+
instanceId,
|
|
2740
|
+
actor,
|
|
2741
|
+
clientForGdr,
|
|
2742
|
+
clock,
|
|
2743
|
+
overlay
|
|
2744
|
+
);
|
|
2745
|
+
return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
|
|
2425
2746
|
}
|
|
2426
2747
|
function buildClientForGdr(defaultClient, resolver) {
|
|
2427
2748
|
return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
|
|
@@ -2439,6 +2760,22 @@ function intersectsTags(docTags, engineTags) {
|
|
|
2439
2760
|
function toEffectsContextEntries(ctx) {
|
|
2440
2761
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
2441
2762
|
}
|
|
2763
|
+
function assertActionAllowed(evaluation, taskId, action) {
|
|
2764
|
+
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
|
|
2765
|
+
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
2766
|
+
throw new ActionDisabledError({ taskId, action, reason: actionEval.disabledReason });
|
|
2767
|
+
}
|
|
2768
|
+
async function applyAction(args) {
|
|
2769
|
+
const { client, instance, taskId, action, params, actor, clock } = args, options = { actor, ...clock !== void 0 ? { clock } : {} };
|
|
2770
|
+
return findOpenStageEntry(instance)?.tasks.find((t) => t.id === taskId)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, taskId, options }), (await fireAction({
|
|
2771
|
+
client,
|
|
2772
|
+
instanceId: instance._id,
|
|
2773
|
+
taskId,
|
|
2774
|
+
action,
|
|
2775
|
+
...params !== void 0 ? { params } : {},
|
|
2776
|
+
options
|
|
2777
|
+
})).ranOps;
|
|
2778
|
+
}
|
|
2442
2779
|
const workflow = {
|
|
2443
2780
|
/**
|
|
2444
2781
|
* Persist a workflow definition to the lake. Stored as a
|
|
@@ -2471,7 +2808,7 @@ const workflow = {
|
|
|
2471
2808
|
const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
|
|
2472
2809
|
...def,
|
|
2473
2810
|
_id: id,
|
|
2474
|
-
_type:
|
|
2811
|
+
_type: schema.WORKFLOW_DEFINITION_TYPE,
|
|
2475
2812
|
tags
|
|
2476
2813
|
}, existing = await client.getDocument(id);
|
|
2477
2814
|
if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
|
|
@@ -2513,16 +2850,17 @@ const workflow = {
|
|
|
2513
2850
|
effectsContext,
|
|
2514
2851
|
instanceId,
|
|
2515
2852
|
perspective
|
|
2516
|
-
} = args, { actor, clientForGdr } = await resolveOperationContext(args), definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
|
|
2853
|
+
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
|
|
2517
2854
|
if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
|
|
2518
2855
|
throw new Error(
|
|
2519
2856
|
`Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
|
|
2520
2857
|
);
|
|
2521
|
-
const now = (
|
|
2858
|
+
const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
|
|
2522
2859
|
slotDefs: definition.state ?? [],
|
|
2523
2860
|
initialState: initialState ?? [],
|
|
2524
2861
|
ctx: {
|
|
2525
2862
|
client,
|
|
2863
|
+
now,
|
|
2526
2864
|
selfId: id,
|
|
2527
2865
|
tags,
|
|
2528
2866
|
workflowResource,
|
|
@@ -2544,7 +2882,7 @@ const workflow = {
|
|
|
2544
2882
|
initialStageId: definition.initialStageId,
|
|
2545
2883
|
actor
|
|
2546
2884
|
});
|
|
2547
|
-
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr), await cascade(client, id, actor, clientForGdr), reload(client, id, tags);
|
|
2885
|
+
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
|
|
2548
2886
|
},
|
|
2549
2887
|
/**
|
|
2550
2888
|
* Fire an action against a task. If the task is pending, it is
|
|
@@ -2566,43 +2904,32 @@ const workflow = {
|
|
|
2566
2904
|
params,
|
|
2567
2905
|
idempotent,
|
|
2568
2906
|
resourceClients
|
|
2569
|
-
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), before = await reload(client, instanceId, tags)
|
|
2570
|
-
|
|
2571
|
-
)?.tasks.find((t) => t.id === taskId);
|
|
2572
|
-
if (taskEntry === void 0 && idempotent === !0)
|
|
2907
|
+
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
|
|
2908
|
+
if (findOpenStageEntry(before)?.tasks.find((t) => t.id === taskId) === void 0 && idempotent === !0)
|
|
2573
2909
|
return { instance: before, cascaded: 0, fired: !1 };
|
|
2574
|
-
const
|
|
2910
|
+
const evaluation = await evaluateInstance({
|
|
2575
2911
|
client,
|
|
2576
2912
|
tags,
|
|
2577
2913
|
instanceId,
|
|
2578
2914
|
access,
|
|
2915
|
+
clock,
|
|
2579
2916
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
2580
|
-
})).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
|
|
2581
|
-
if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
|
|
2582
|
-
throw new ActionDisabledError({
|
|
2583
|
-
taskId,
|
|
2584
|
-
action,
|
|
2585
|
-
reason: actionEval.disabledReason
|
|
2586
|
-
});
|
|
2587
|
-
taskEntry?.status === "pending" && await invokeTask({
|
|
2588
|
-
client,
|
|
2589
|
-
instanceId,
|
|
2590
|
-
taskId,
|
|
2591
|
-
options: { actor }
|
|
2592
2917
|
});
|
|
2593
|
-
|
|
2918
|
+
assertActionAllowed(evaluation, taskId, action);
|
|
2919
|
+
const ranOps = await applyAction({
|
|
2594
2920
|
client,
|
|
2595
|
-
|
|
2921
|
+
instance: before,
|
|
2596
2922
|
taskId,
|
|
2597
2923
|
action,
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2924
|
+
actor,
|
|
2925
|
+
clock,
|
|
2926
|
+
...params !== void 0 ? { params } : {}
|
|
2927
|
+
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
2601
2928
|
return {
|
|
2602
2929
|
instance: await reload(client, instanceId, tags),
|
|
2603
2930
|
cascaded,
|
|
2604
2931
|
fired: !0,
|
|
2605
|
-
...
|
|
2932
|
+
...ranOps !== void 0 ? { ranOps } : {}
|
|
2606
2933
|
};
|
|
2607
2934
|
},
|
|
2608
2935
|
/**
|
|
@@ -2613,7 +2940,7 @@ const workflow = {
|
|
|
2613
2940
|
* reference them. Cascades after.
|
|
2614
2941
|
*/
|
|
2615
2942
|
completeEffect: async (args) => {
|
|
2616
|
-
const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args);
|
|
2943
|
+
const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
2617
2944
|
await completeEffect({
|
|
2618
2945
|
client,
|
|
2619
2946
|
instanceId,
|
|
@@ -2623,9 +2950,9 @@ const workflow = {
|
|
|
2623
2950
|
...detail !== void 0 ? { detail } : {},
|
|
2624
2951
|
...error !== void 0 ? { error } : {},
|
|
2625
2952
|
...durationMs !== void 0 ? { durationMs } : {},
|
|
2626
|
-
...actor !== void 0 ? {
|
|
2953
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
2627
2954
|
});
|
|
2628
|
-
const cascaded = await cascade(client, instanceId, actor, clientForGdr);
|
|
2955
|
+
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
2629
2956
|
return {
|
|
2630
2957
|
instance: await reload(client, instanceId, tags),
|
|
2631
2958
|
cascaded,
|
|
@@ -2642,9 +2969,9 @@ const workflow = {
|
|
|
2642
2969
|
* affected instances and the engine re-evaluates.
|
|
2643
2970
|
*/
|
|
2644
2971
|
tick: async (args) => {
|
|
2645
|
-
const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args);
|
|
2972
|
+
const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
2646
2973
|
await reload(client, instanceId, tags);
|
|
2647
|
-
const cascaded = await cascade(client, instanceId, actor, clientForGdr);
|
|
2974
|
+
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
2648
2975
|
return {
|
|
2649
2976
|
instance: await reload(client, instanceId, tags),
|
|
2650
2977
|
cascaded,
|
|
@@ -2657,15 +2984,15 @@ const workflow = {
|
|
|
2657
2984
|
* upstream; this verb performs the mechanical move.
|
|
2658
2985
|
*/
|
|
2659
2986
|
setStage: async (args) => {
|
|
2660
|
-
const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args);
|
|
2987
|
+
const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
2661
2988
|
await reload(client, instanceId, tags);
|
|
2662
2989
|
const result = await setStage({
|
|
2663
2990
|
client,
|
|
2664
2991
|
instanceId,
|
|
2665
2992
|
targetStageId,
|
|
2666
2993
|
...reason !== void 0 ? { reason } : {},
|
|
2667
|
-
...actor !== void 0 ? {
|
|
2668
|
-
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr) : 0;
|
|
2994
|
+
options: { ...actor !== void 0 ? { actor } : {}, clock }
|
|
2995
|
+
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
2669
2996
|
return {
|
|
2670
2997
|
instance: await reload(client, instanceId, tags),
|
|
2671
2998
|
cascaded,
|
|
@@ -2681,6 +3008,16 @@ const workflow = {
|
|
|
2681
3008
|
const { client, tags, instanceId } = args;
|
|
2682
3009
|
return validateTags(tags), reload(client, instanceId, tags);
|
|
2683
3010
|
},
|
|
3011
|
+
guardsForInstance: async (args) => {
|
|
3012
|
+
const { client, tags, instanceId, resourceClients } = args;
|
|
3013
|
+
validateTags(tags);
|
|
3014
|
+
const instance = await reload(client, instanceId, tags);
|
|
3015
|
+
return guardsForInstance({
|
|
3016
|
+
client,
|
|
3017
|
+
clientForGdr: buildClientForGdr(client, resourceClients),
|
|
3018
|
+
instance
|
|
3019
|
+
});
|
|
3020
|
+
},
|
|
2684
3021
|
/**
|
|
2685
3022
|
* Run a caller-supplied GROQ query with the engine's tags bound as
|
|
2686
3023
|
* `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
|
|
@@ -2715,9 +3052,9 @@ const workflow = {
|
|
|
2715
3052
|
* without re-implementing hydration. Pure read — never writes.
|
|
2716
3053
|
*/
|
|
2717
3054
|
queryInScope: async (args) => {
|
|
2718
|
-
const { client, tags, instanceId, groq, params, resourceClients } = args;
|
|
3055
|
+
const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
|
|
2719
3056
|
validateTags(tags);
|
|
2720
|
-
const instance = await reload(client, instanceId, tags),
|
|
3057
|
+
const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams(instance, clock()), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
|
|
2721
3058
|
return await (await groqJs.evaluate(tree, {
|
|
2722
3059
|
dataset: snapshot.docs,
|
|
2723
3060
|
params: { ...reserved, ...params }
|
|
@@ -2760,7 +3097,7 @@ const workflow = {
|
|
|
2760
3097
|
validateTags(tags);
|
|
2761
3098
|
const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "workflow.history.spawned", ids = parent.history.filter(isSpawned).filter((h) => taskId === void 0 || h.taskId === taskId).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
|
|
2762
3099
|
return ids.length === 0 ? [] : client.fetch(
|
|
2763
|
-
|
|
3100
|
+
`*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
|
|
2764
3101
|
{ ids, engineTags: tags }
|
|
2765
3102
|
);
|
|
2766
3103
|
},
|
|
@@ -2779,7 +3116,7 @@ async function verifyDeployedDefinitionsInternal(args) {
|
|
|
2779
3116
|
const { client, tags, effectHandlers, missingHandler, logger } = args;
|
|
2780
3117
|
validateTags(tags);
|
|
2781
3118
|
const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
|
|
2782
|
-
|
|
3119
|
+
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
|
|
2783
3120
|
{ engineTags: tags }
|
|
2784
3121
|
), seen = [], missingByName = /* @__PURE__ */ new Map();
|
|
2785
3122
|
for (const def of definitions)
|
|
@@ -2901,7 +3238,8 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
|
|
|
2901
3238
|
};
|
|
2902
3239
|
try {
|
|
2903
3240
|
return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
|
|
2904
|
-
} catch {
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
if (!isRevisionConflict(error)) throw error;
|
|
2905
3243
|
return !1;
|
|
2906
3244
|
}
|
|
2907
3245
|
}
|
|
@@ -2929,6 +3267,89 @@ async function applyMissingHandler(policy, info, log) {
|
|
|
2929
3267
|
return "fail";
|
|
2930
3268
|
}
|
|
2931
3269
|
}
|
|
3270
|
+
function createInstanceSession(args) {
|
|
3271
|
+
const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
|
|
3272
|
+
let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
|
|
3273
|
+
const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
|
|
3274
|
+
const next = /* @__PURE__ */ new Map();
|
|
3275
|
+
for (const ld of docs) {
|
|
3276
|
+
const owned = { doc: structuredClone(ld.doc), resource: ld.resource }, uri = gdrFromResource(owned.resource, owned.doc._id);
|
|
3277
|
+
if (uri === selfUri()) {
|
|
3278
|
+
instance = asHeldInstance(owned.doc);
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
next.set(uri, owned);
|
|
3282
|
+
}
|
|
3283
|
+
overlay = next;
|
|
3284
|
+
}, access = () => resolveAccess(client, {
|
|
3285
|
+
...args.access !== void 0 ? { override: args.access } : {},
|
|
3286
|
+
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
3287
|
+
}), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
|
|
3288
|
+
const { actor, grants } = await access();
|
|
3289
|
+
return evaluateFromSnapshot({
|
|
3290
|
+
instance,
|
|
3291
|
+
definition: parseDefinitionSnapshot(instance),
|
|
3292
|
+
actor,
|
|
3293
|
+
snapshot: snapshotFrom(held),
|
|
3294
|
+
...clock !== void 0 ? { now: clock() } : {},
|
|
3295
|
+
...grants !== void 0 ? { grants } : {}
|
|
3296
|
+
});
|
|
3297
|
+
}, commit = async (run) => {
|
|
3298
|
+
committing = !0;
|
|
3299
|
+
const held = new Map(overlay);
|
|
3300
|
+
try {
|
|
3301
|
+
const { actor } = await access();
|
|
3302
|
+
return await run(actor, held);
|
|
3303
|
+
} finally {
|
|
3304
|
+
if (committing = !1, buffered !== void 0) {
|
|
3305
|
+
const next = buffered;
|
|
3306
|
+
buffered = void 0, applyUpdate(next);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
return {
|
|
3311
|
+
get subscriptionDocuments() {
|
|
3312
|
+
return subscriptionDocumentsForInstance(instance);
|
|
3313
|
+
},
|
|
3314
|
+
update(docs) {
|
|
3315
|
+
if (committing) {
|
|
3316
|
+
buffered = docs;
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
applyUpdate(docs);
|
|
3320
|
+
},
|
|
3321
|
+
evaluate() {
|
|
3322
|
+
return evaluateWith(overlay);
|
|
3323
|
+
},
|
|
3324
|
+
tick() {
|
|
3325
|
+
return commit(async (actor, held) => {
|
|
3326
|
+
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3327
|
+
return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
|
|
3328
|
+
});
|
|
3329
|
+
},
|
|
3330
|
+
fireAction({ taskId, action, params }) {
|
|
3331
|
+
return commit(async (actor, held) => {
|
|
3332
|
+
assertActionAllowed(await evaluateWith(held), taskId, action);
|
|
3333
|
+
const ranOps = await applyAction({
|
|
3334
|
+
client,
|
|
3335
|
+
instance,
|
|
3336
|
+
taskId,
|
|
3337
|
+
action,
|
|
3338
|
+
actor,
|
|
3339
|
+
...clock !== void 0 ? { clock } : {},
|
|
3340
|
+
...params !== void 0 ? { params } : {}
|
|
3341
|
+
}), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3342
|
+
return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
function asHeldInstance(doc) {
|
|
3348
|
+
const candidate = doc;
|
|
3349
|
+
if (candidate._type !== "workflow.instance" || typeof candidate.currentStageId != "string" || !Array.isArray(candidate.stages))
|
|
3350
|
+
throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
|
|
3351
|
+
return doc;
|
|
3352
|
+
}
|
|
2932
3353
|
const defaultLoggerFactory = (name) => ({
|
|
2933
3354
|
// info/warn/error all go to stderr — these are diagnostic, not
|
|
2934
3355
|
// program output. Keeps stdout reserved for the caller's own data
|
|
@@ -2946,13 +3367,14 @@ const defaultLoggerFactory = (name) => ({
|
|
|
2946
3367
|
}
|
|
2947
3368
|
};
|
|
2948
3369
|
function createEngine(args) {
|
|
2949
|
-
const { client, workflowResource, tags, resourceClients } = args;
|
|
3370
|
+
const { client, workflowResource, tags, resourceClients, clock } = args;
|
|
2950
3371
|
validateTags(tags);
|
|
2951
3372
|
const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
|
|
2952
3373
|
client,
|
|
2953
3374
|
tags,
|
|
2954
3375
|
workflowResource,
|
|
2955
3376
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3377
|
+
...clock !== void 0 ? { clock } : {},
|
|
2956
3378
|
...rest
|
|
2957
3379
|
});
|
|
2958
3380
|
return {
|
|
@@ -2970,6 +3392,23 @@ function createEngine(args) {
|
|
|
2970
3392
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
2971
3393
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
2972
3394
|
getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
|
|
3395
|
+
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3396
|
+
instance: (instanceDoc, opts) => createInstanceSession({
|
|
3397
|
+
client,
|
|
3398
|
+
tags,
|
|
3399
|
+
instance: instanceDoc,
|
|
3400
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3401
|
+
...clock !== void 0 ? { clock } : {},
|
|
3402
|
+
...opts?.access !== void 0 ? { access: opts.access } : {},
|
|
3403
|
+
...opts?.grantsFromPath !== void 0 ? { grantsFromPath: opts.grantsFromPath } : {}
|
|
3404
|
+
}),
|
|
3405
|
+
guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
|
|
3406
|
+
client,
|
|
3407
|
+
tags,
|
|
3408
|
+
workflowResource,
|
|
3409
|
+
instanceId,
|
|
3410
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3411
|
+
}),
|
|
2973
3412
|
children: ({ instanceId, taskId }) => workflow.children({
|
|
2974
3413
|
client,
|
|
2975
3414
|
tags,
|
|
@@ -2990,7 +3429,8 @@ function createEngine(args) {
|
|
|
2990
3429
|
workflowResource,
|
|
2991
3430
|
instanceId,
|
|
2992
3431
|
groq,
|
|
2993
|
-
...params !== void 0 ? { params } : {}
|
|
3432
|
+
...params !== void 0 ? { params } : {},
|
|
3433
|
+
...clock !== void 0 ? { clock } : {}
|
|
2994
3434
|
}),
|
|
2995
3435
|
listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
|
|
2996
3436
|
findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
|
|
@@ -3021,15 +3461,6 @@ function createEngine(args) {
|
|
|
3021
3461
|
})
|
|
3022
3462
|
};
|
|
3023
3463
|
}
|
|
3024
|
-
class MutationGuardDeniedError extends Error {
|
|
3025
|
-
denied;
|
|
3026
|
-
documentId;
|
|
3027
|
-
action;
|
|
3028
|
-
constructor(args) {
|
|
3029
|
-
const ids = args.denied.map((d) => d.guardId).join(", ");
|
|
3030
|
-
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;
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
3464
|
const HISTORY_DISPLAY = {
|
|
3034
3465
|
"workflow.history.stageEntered": {
|
|
3035
3466
|
title: "Stage entered",
|
|
@@ -3221,43 +3652,60 @@ function displayDescription(typeKey) {
|
|
|
3221
3652
|
if (typeKey)
|
|
3222
3653
|
return DISPLAY[typeKey]?.description;
|
|
3223
3654
|
}
|
|
3655
|
+
exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
|
|
3224
3656
|
exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
|
|
3225
3657
|
exports.ActionDisabledError = ActionDisabledError;
|
|
3226
3658
|
exports.ActionParamsInvalidError = ActionParamsInvalidError;
|
|
3659
|
+
exports.CascadeLimitError = CascadeLimitError;
|
|
3660
|
+
exports.ConcurrentFireActionError = ConcurrentFireActionError;
|
|
3227
3661
|
exports.DISPLAY = DISPLAY;
|
|
3228
3662
|
exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
|
|
3663
|
+
exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
|
|
3229
3664
|
exports.GUARD_LIFTED_PREDICATE = GUARD_LIFTED_PREDICATE;
|
|
3230
3665
|
exports.GUARD_OWNER = GUARD_OWNER;
|
|
3231
3666
|
exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
|
|
3232
3667
|
exports.MutationGuardDeniedError = MutationGuardDeniedError;
|
|
3233
3668
|
exports.OP_DISPLAY = OP_DISPLAY;
|
|
3234
3669
|
exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
|
|
3670
|
+
exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
|
|
3671
|
+
exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
|
|
3672
|
+
exports.buildSnapshot = buildSnapshot;
|
|
3235
3673
|
exports.canonicalTag = canonicalTag;
|
|
3236
3674
|
exports.compileGuard = compileGuard;
|
|
3675
|
+
exports.contentReleaseName = contentReleaseName;
|
|
3237
3676
|
exports.createEngine = createEngine;
|
|
3238
3677
|
exports.defaultLoggerFactory = defaultLoggerFactory;
|
|
3239
3678
|
exports.denyingGuards = denyingGuards;
|
|
3240
3679
|
exports.deployStageGuards = deployStageGuards;
|
|
3241
3680
|
exports.displayDescription = displayDescription;
|
|
3242
3681
|
exports.displayTitle = displayTitle;
|
|
3682
|
+
exports.evaluateFromSnapshot = evaluateFromSnapshot;
|
|
3243
3683
|
exports.evaluateMutationGuard = evaluateMutationGuard;
|
|
3244
3684
|
exports.extractDocumentId = extractDocumentId;
|
|
3245
3685
|
exports.gdrFromResource = gdrFromResource;
|
|
3246
3686
|
exports.gdrRef = gdrRef;
|
|
3247
3687
|
exports.gdrUri = gdrUri;
|
|
3248
3688
|
exports.guardMatches = guardMatches;
|
|
3689
|
+
exports.guardsForInstance = guardsForInstance;
|
|
3690
|
+
exports.guardsForResource = guardsForResource;
|
|
3691
|
+
exports.isDefinitionUnchanged = isDefinitionUnchanged;
|
|
3249
3692
|
exports.isGdr = isGdr;
|
|
3250
3693
|
exports.lakeGuardId = lakeGuardId;
|
|
3251
3694
|
exports.parseGdr = parseGdr;
|
|
3252
|
-
exports.
|
|
3695
|
+
exports.readsRaw = readsRaw;
|
|
3253
3696
|
exports.refCanvas = refCanvas;
|
|
3254
3697
|
exports.refDashboard = refDashboard;
|
|
3255
3698
|
exports.refDataset = refDataset;
|
|
3256
3699
|
exports.refMediaLibrary = refMediaLibrary;
|
|
3257
3700
|
exports.resolveAccess = resolveAccess;
|
|
3701
|
+
exports.resourceFromParsed = resourceFromParsed;
|
|
3258
3702
|
exports.retractStageGuards = retractStageGuards;
|
|
3259
3703
|
exports.silentLogger = silentLogger;
|
|
3704
|
+
exports.stripSystemFields = stripSystemFields;
|
|
3705
|
+
exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
|
|
3706
|
+
exports.tagScopeFilter = tagScopeFilter;
|
|
3260
3707
|
exports.validateDefinition = validateDefinition;
|
|
3261
3708
|
exports.validateTags = validateTags;
|
|
3709
|
+
exports.wallClock = wallClock;
|
|
3262
3710
|
exports.workflow = workflow;
|
|
3263
3711
|
//# sourceMappingURL=index.cjs.map
|