@sanity/workflow-engine 0.8.0 → 0.9.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 +68 -21
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +68 -21
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +81 -18
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +535 -80
- package/dist/define.d.ts +535 -80
- package/dist/define.js +82 -19
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +430 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +968 -127
- package/dist/index.d.ts +968 -127
- package/dist/index.js +431 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse, evaluate } from "groq-js";
|
|
2
|
-
import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
|
|
2
|
+
import { actorFulfillsRole, isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, andConditions, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
|
|
3
3
|
import { isStartableDefinition } from "./_chunks-es/schema.js";
|
|
4
4
|
import * as v from "valibot";
|
|
5
5
|
function findOpenStageEntry(host) {
|
|
@@ -140,11 +140,11 @@ function scopedStateOverlay(instance, snapshot, taskName) {
|
|
|
140
140
|
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
141
141
|
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
142
142
|
}
|
|
143
|
-
function assignedFor(instance, taskName, actor) {
|
|
143
|
+
function assignedFor(instance, taskName, actor, roleAliases) {
|
|
144
144
|
if (actor === void 0) return !1;
|
|
145
145
|
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
146
146
|
return entry === void 0 ? !1 : entry.value.some(
|
|
147
|
-
(a) => a.type === "user" ? a.id === actor.id : (actor.roles
|
|
147
|
+
(a) => a.type === "user" ? a.id === actor.id : actorFulfillsRole(actor.roles, a.role, roleAliases)
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
function effectsContextMap(instance) {
|
|
@@ -663,6 +663,17 @@ function isRevisionConflict(error) {
|
|
|
663
663
|
const { statusCode, message } = error;
|
|
664
664
|
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
665
665
|
}
|
|
666
|
+
class ConcurrentEditStateError extends Error {
|
|
667
|
+
instanceId;
|
|
668
|
+
target;
|
|
669
|
+
attempts;
|
|
670
|
+
constructor(args) {
|
|
671
|
+
const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
|
|
672
|
+
super(
|
|
673
|
+
`Edit of "${args.target.scope}:${where}" (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.`
|
|
674
|
+
), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
666
677
|
class CascadeLimitError extends Error {
|
|
667
678
|
instanceId;
|
|
668
679
|
limit;
|
|
@@ -1282,6 +1293,12 @@ function coerceToGdr(raw, workflowResource) {
|
|
|
1282
1293
|
function gdrToBareId(uri) {
|
|
1283
1294
|
return uri.includes(":") ? extractDocumentId(uri) : uri;
|
|
1284
1295
|
}
|
|
1296
|
+
function loadCallContext(client, instanceId, options) {
|
|
1297
|
+
return loadContext(client, instanceId, {
|
|
1298
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1299
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1285
1302
|
async function loadContext(client, instanceId, options) {
|
|
1286
1303
|
const instance = await client.getDocument(instanceId);
|
|
1287
1304
|
if (!instance)
|
|
@@ -1302,7 +1319,7 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1302
1319
|
...base,
|
|
1303
1320
|
state,
|
|
1304
1321
|
actor: opts?.actor,
|
|
1305
|
-
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
|
|
1322
|
+
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
|
|
1306
1323
|
...opts?.vars
|
|
1307
1324
|
};
|
|
1308
1325
|
return { ...await evaluatePredicates({
|
|
@@ -2033,22 +2050,27 @@ function runOps(args) {
|
|
|
2033
2050
|
const summaries = [];
|
|
2034
2051
|
for (const op of ops) {
|
|
2035
2052
|
const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
|
|
2036
|
-
summaries.push(summary), mutation.history.push({
|
|
2037
|
-
_key: randomKey(),
|
|
2038
|
-
_type: "opApplied",
|
|
2039
|
-
at: now,
|
|
2040
|
-
stage,
|
|
2041
|
-
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2042
|
-
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2043
|
-
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2044
|
-
opType: summary.opType,
|
|
2045
|
-
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2046
|
-
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2047
|
-
...actor !== void 0 ? { actor } : {}
|
|
2048
|
-
});
|
|
2053
|
+
summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
|
|
2049
2054
|
}
|
|
2050
2055
|
return summaries;
|
|
2051
2056
|
}
|
|
2057
|
+
function opAppliedEntry(args) {
|
|
2058
|
+
const { origin, summary, stage, actor, now } = args;
|
|
2059
|
+
return {
|
|
2060
|
+
_key: randomKey(),
|
|
2061
|
+
_type: "opApplied",
|
|
2062
|
+
at: now,
|
|
2063
|
+
stage,
|
|
2064
|
+
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2065
|
+
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2066
|
+
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2067
|
+
...origin.edit === !0 ? { edit: !0 } : {},
|
|
2068
|
+
opType: summary.opType,
|
|
2069
|
+
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2070
|
+
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2071
|
+
...actor !== void 0 ? { actor } : {}
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2052
2074
|
function applyOp(op, ctx) {
|
|
2053
2075
|
switch (op.type) {
|
|
2054
2076
|
case "state.set":
|
|
@@ -2682,6 +2704,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2682
2704
|
return;
|
|
2683
2705
|
}
|
|
2684
2706
|
}
|
|
2707
|
+
function effectiveEditable(baseline, override) {
|
|
2708
|
+
if (baseline === void 0) return;
|
|
2709
|
+
if (override === void 0) return baseline;
|
|
2710
|
+
if (baseline === !0 && override === !0) return !0;
|
|
2711
|
+
const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
|
|
2712
|
+
return andConditions(parts) ?? !0;
|
|
2713
|
+
}
|
|
2714
|
+
function slotSites(definition, stage) {
|
|
2715
|
+
const sites = [];
|
|
2716
|
+
for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
|
|
2717
|
+
for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
|
|
2718
|
+
for (const task of stage.tasks ?? [])
|
|
2719
|
+
for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
|
|
2720
|
+
return sites;
|
|
2721
|
+
}
|
|
2722
|
+
function toResolvedSlot(site, stage) {
|
|
2723
|
+
return {
|
|
2724
|
+
scope: site.scope,
|
|
2725
|
+
...site.task !== void 0 ? { task: site.task } : {},
|
|
2726
|
+
name: site.entry.name,
|
|
2727
|
+
type: site.entry.type,
|
|
2728
|
+
...site.entry.title !== void 0 ? { title: site.entry.title } : {},
|
|
2729
|
+
ref: { scope: site.scope, state: site.entry.name },
|
|
2730
|
+
effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
function editableSlotsInStage(definition, stage) {
|
|
2734
|
+
return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
|
|
2735
|
+
}
|
|
2736
|
+
function editTargetMatches(slot, target) {
|
|
2737
|
+
return slot.name !== target.state ? !1 : target.task !== void 0 ? slot.scope === "task" && slot.task === target.task : target.scope !== void 0 ? slot.scope === target.scope : !0;
|
|
2738
|
+
}
|
|
2739
|
+
const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
|
|
2740
|
+
function resolveEditTarget(args) {
|
|
2741
|
+
const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
|
|
2742
|
+
(site) => editTargetMatches(
|
|
2743
|
+
{
|
|
2744
|
+
scope: site.scope,
|
|
2745
|
+
name: site.entry.name,
|
|
2746
|
+
...site.task !== void 0 ? { task: site.task } : {}
|
|
2747
|
+
},
|
|
2748
|
+
target
|
|
2749
|
+
)
|
|
2750
|
+
).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
2751
|
+
return found ? toResolvedSlot(found, stage) : void 0;
|
|
2752
|
+
}
|
|
2753
|
+
function slotWindowOpen(instance, slot) {
|
|
2754
|
+
if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
|
|
2755
|
+
if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
|
|
2756
|
+
if (slot.scope !== "task") return { open: !0 };
|
|
2757
|
+
const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
|
|
2758
|
+
return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
|
|
2759
|
+
}
|
|
2760
|
+
function readSlotValue(instance, slot) {
|
|
2761
|
+
return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
|
|
2762
|
+
}
|
|
2763
|
+
function slotStateHost(instance, slot) {
|
|
2764
|
+
if (slot.scope === "workflow") return instance.state;
|
|
2765
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
2766
|
+
return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
|
|
2767
|
+
}
|
|
2768
|
+
function slotProvenance(instance, ref) {
|
|
2769
|
+
let latest;
|
|
2770
|
+
for (const entry of instance.history)
|
|
2771
|
+
entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
|
|
2772
|
+
return latest === void 0 ? {} : {
|
|
2773
|
+
...latest.actor !== void 0 ? { setBy: latest.actor } : {},
|
|
2774
|
+
setAt: latest.at
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function editDisabledReason(args) {
|
|
2778
|
+
const { effective, instance, window, guardDenial, predicateSatisfied } = args;
|
|
2779
|
+
if (effective === void 0) return { kind: "not-editable" };
|
|
2780
|
+
if (!window.open)
|
|
2781
|
+
return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
|
|
2782
|
+
if (guardDenial !== void 0) return guardDenial;
|
|
2783
|
+
if (!predicateSatisfied)
|
|
2784
|
+
return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
|
|
2785
|
+
}
|
|
2685
2786
|
async function evaluateInstance(args) {
|
|
2686
2787
|
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2687
2788
|
validateTag(tag);
|
|
@@ -2715,6 +2816,7 @@ async function evaluateFromSnapshot(args) {
|
|
|
2715
2816
|
actor,
|
|
2716
2817
|
snapshot,
|
|
2717
2818
|
scope,
|
|
2819
|
+
roleAliases: definition.roleAliases,
|
|
2718
2820
|
stageHasExits: !isTerminalStage(stage),
|
|
2719
2821
|
guardDenial
|
|
2720
2822
|
})
|
|
@@ -2736,16 +2838,65 @@ async function evaluateFromSnapshot(args) {
|
|
|
2736
2838
|
stage,
|
|
2737
2839
|
tasks: taskEvaluations,
|
|
2738
2840
|
transitions: transitionEvaluations
|
|
2739
|
-
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed))
|
|
2841
|
+
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableSlots = await evaluateEditableSlots({
|
|
2842
|
+
instance,
|
|
2843
|
+
definition,
|
|
2844
|
+
stage,
|
|
2845
|
+
actor,
|
|
2846
|
+
snapshot,
|
|
2847
|
+
scope,
|
|
2848
|
+
guardDenial: editGuardDenial
|
|
2849
|
+
});
|
|
2740
2850
|
return {
|
|
2741
2851
|
instance,
|
|
2742
2852
|
definition,
|
|
2743
2853
|
actor,
|
|
2744
2854
|
currentStage,
|
|
2745
2855
|
pendingOnYou,
|
|
2746
|
-
canInteract
|
|
2856
|
+
canInteract,
|
|
2857
|
+
editableSlots
|
|
2747
2858
|
};
|
|
2748
2859
|
}
|
|
2860
|
+
async function evaluateEditableSlots(args) {
|
|
2861
|
+
const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
|
|
2862
|
+
for (const slot of editableSlotsInStage(definition, stage)) {
|
|
2863
|
+
const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
|
|
2864
|
+
slot,
|
|
2865
|
+
window,
|
|
2866
|
+
guardDenial,
|
|
2867
|
+
instance,
|
|
2868
|
+
snapshot,
|
|
2869
|
+
scope,
|
|
2870
|
+
actor,
|
|
2871
|
+
roleAliases: definition.roleAliases
|
|
2872
|
+
}), reason = editDisabledReason({
|
|
2873
|
+
effective: slot.effective,
|
|
2874
|
+
instance,
|
|
2875
|
+
window,
|
|
2876
|
+
guardDenial,
|
|
2877
|
+
predicateSatisfied
|
|
2878
|
+
}), value = readSlotValue(instance, slot);
|
|
2879
|
+
slots.push({
|
|
2880
|
+
scope: slot.scope,
|
|
2881
|
+
...slot.task !== void 0 ? { task: slot.task } : {},
|
|
2882
|
+
name: slot.name,
|
|
2883
|
+
type: slot.type,
|
|
2884
|
+
...slot.title !== void 0 ? { title: slot.title } : {},
|
|
2885
|
+
value,
|
|
2886
|
+
editable: reason === void 0,
|
|
2887
|
+
...reason !== void 0 ? { disabledReason: reason } : {},
|
|
2888
|
+
...slotProvenance(instance, slot.ref)
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
return slots;
|
|
2892
|
+
}
|
|
2893
|
+
async function editPredicateSatisfied(args) {
|
|
2894
|
+
const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
|
|
2895
|
+
if (!window.open || guardDenial !== void 0) return !1;
|
|
2896
|
+
if (slot.effective === !0) return !0;
|
|
2897
|
+
const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
|
|
2898
|
+
return evaluateCondition({ condition: slot.effective, snapshot, params });
|
|
2899
|
+
}
|
|
2749
2900
|
async function renderScope(args) {
|
|
2750
2901
|
const { instance, definition, actor, grants, snapshot, now } = args, params = {
|
|
2751
2902
|
...buildParams({ instance, now, snapshot }),
|
|
@@ -2771,15 +2922,36 @@ async function advisoryCan(instance, actor, grants) {
|
|
|
2771
2922
|
});
|
|
2772
2923
|
return can;
|
|
2773
2924
|
}
|
|
2774
|
-
|
|
2775
|
-
const {
|
|
2925
|
+
function taskScopeFor(args) {
|
|
2926
|
+
const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
|
|
2927
|
+
return {
|
|
2776
2928
|
...scope,
|
|
2777
2929
|
state: {
|
|
2778
2930
|
...scope.state,
|
|
2779
|
-
...scopedStateOverlay(instance, snapshot,
|
|
2931
|
+
...scopedStateOverlay(instance, snapshot, taskName)
|
|
2780
2932
|
},
|
|
2781
|
-
assigned
|
|
2782
|
-
}
|
|
2933
|
+
assigned: assignedFor(instance, taskName, actor, roleAliases)
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
async function evaluateTask(args) {
|
|
2937
|
+
const {
|
|
2938
|
+
task,
|
|
2939
|
+
statusEntry,
|
|
2940
|
+
instance,
|
|
2941
|
+
actor,
|
|
2942
|
+
snapshot,
|
|
2943
|
+
scope,
|
|
2944
|
+
roleAliases,
|
|
2945
|
+
stageHasExits,
|
|
2946
|
+
guardDenial
|
|
2947
|
+
} = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
|
|
2948
|
+
scope,
|
|
2949
|
+
instance,
|
|
2950
|
+
snapshot,
|
|
2951
|
+
actor,
|
|
2952
|
+
taskName: task.name,
|
|
2953
|
+
roleAliases
|
|
2954
|
+
}), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
|
|
2783
2955
|
requirements: task.requirements,
|
|
2784
2956
|
snapshot,
|
|
2785
2957
|
params: taskScope
|
|
@@ -3006,10 +3178,7 @@ async function commitAbort(ctx, reason, actor) {
|
|
|
3006
3178
|
async function fireAction(args) {
|
|
3007
3179
|
const { client, instanceId, task, action, params, options } = args;
|
|
3008
3180
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3009
|
-
const ctx = await
|
|
3010
|
-
...options?.clock ? { clock: options.clock } : {},
|
|
3011
|
-
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
3012
|
-
});
|
|
3181
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3013
3182
|
try {
|
|
3014
3183
|
return await commitAction(ctx, task, action, params, options);
|
|
3015
3184
|
} catch (error) {
|
|
@@ -3104,6 +3273,114 @@ function formatDisabledReason(task, action, reason) {
|
|
|
3104
3273
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
3105
3274
|
return `Action "${task}:${action}" is not allowed: ${detail}`;
|
|
3106
3275
|
}
|
|
3276
|
+
class EditStateDeniedError extends Error {
|
|
3277
|
+
reason;
|
|
3278
|
+
target;
|
|
3279
|
+
constructor(args) {
|
|
3280
|
+
super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const editDisabledReasonDetail = {
|
|
3284
|
+
"not-editable": () => "slot is not declared editable",
|
|
3285
|
+
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
3286
|
+
"edit-window-closed": (r) => `edit window closed (${r.detail})`,
|
|
3287
|
+
"editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
|
|
3288
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
|
|
3289
|
+
};
|
|
3290
|
+
function formatEditDisabledReason(target, reason) {
|
|
3291
|
+
const detail = editDisabledReasonDetail[reason.kind](
|
|
3292
|
+
reason
|
|
3293
|
+
), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3294
|
+
return `State "${target.scope}:${where}" is not editable: ${detail}`;
|
|
3295
|
+
}
|
|
3296
|
+
async function editState(args) {
|
|
3297
|
+
const { client, instanceId, target, mode = "set", value, options } = args;
|
|
3298
|
+
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3299
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3300
|
+
try {
|
|
3301
|
+
return await commitEdit(ctx, target, mode, value, options);
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
if (!isRevisionConflict(error)) throw error;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
throw new ConcurrentEditStateError({
|
|
3307
|
+
instanceId,
|
|
3308
|
+
target: labelTarget(target),
|
|
3309
|
+
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
async function commitEdit(ctx, target, mode, value, options) {
|
|
3313
|
+
const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
|
|
3314
|
+
if (slot === void 0)
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
`No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
3317
|
+
);
|
|
3318
|
+
await assertSlotEditable(ctx, slot, options);
|
|
3319
|
+
const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
|
|
3320
|
+
ops: [op],
|
|
3321
|
+
mutation,
|
|
3322
|
+
stage: stage.name,
|
|
3323
|
+
origin: {
|
|
3324
|
+
edit: !0,
|
|
3325
|
+
...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
|
|
3326
|
+
},
|
|
3327
|
+
params: {},
|
|
3328
|
+
actor,
|
|
3329
|
+
self: selfGdr(ctx.instance),
|
|
3330
|
+
now: ctx.now
|
|
3331
|
+
});
|
|
3332
|
+
return await persistThenDeploy(
|
|
3333
|
+
ctx,
|
|
3334
|
+
mutation,
|
|
3335
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
|
|
3336
|
+
), {
|
|
3337
|
+
edited: !0,
|
|
3338
|
+
target: slotTarget(slot),
|
|
3339
|
+
...ranOps.length > 0 ? { ranOps } : {}
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
async function assertSlotEditable(ctx, slot, options) {
|
|
3343
|
+
const actor = options?.actor, window = slotWindowOpen(ctx.instance, slot), can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0, predicateSatisfied = window.open && slot.effective !== !0 && slot.effective !== void 0 ? await ctxEvaluateCondition(ctx, slot.effective, {
|
|
3344
|
+
...slot.task !== void 0 ? { taskName: slot.task } : {},
|
|
3345
|
+
...actor !== void 0 ? { actor } : {},
|
|
3346
|
+
...can !== void 0 ? { vars: { can } } : {}
|
|
3347
|
+
}) : slot.effective === !0, reason = editDisabledReason({
|
|
3348
|
+
effective: slot.effective,
|
|
3349
|
+
instance: ctx.instance,
|
|
3350
|
+
window,
|
|
3351
|
+
guardDenial: void 0,
|
|
3352
|
+
predicateSatisfied
|
|
3353
|
+
});
|
|
3354
|
+
if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
|
|
3355
|
+
}
|
|
3356
|
+
function buildEditOp(slot, mode, value) {
|
|
3357
|
+
if (mode === "unset") return { type: "state.unset", target: slot.ref };
|
|
3358
|
+
if (value === void 0)
|
|
3359
|
+
throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
|
|
3360
|
+
return {
|
|
3361
|
+
type: mode === "append" ? "state.append" : "state.set",
|
|
3362
|
+
target: slot.ref,
|
|
3363
|
+
value: { type: "literal", value }
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
function slotTarget(slot) {
|
|
3367
|
+
return {
|
|
3368
|
+
scope: slot.scope,
|
|
3369
|
+
state: slot.name,
|
|
3370
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
function labelTarget(target) {
|
|
3374
|
+
return {
|
|
3375
|
+
scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
|
|
3376
|
+
state: target.state,
|
|
3377
|
+
...target.task !== void 0 ? { task: target.task } : {}
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
function describeTarget(target) {
|
|
3381
|
+
const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3382
|
+
return target.scope !== void 0 ? `${target.scope}:${where}` : where;
|
|
3383
|
+
}
|
|
3107
3384
|
function bareIdFromSpawnRef(uri) {
|
|
3108
3385
|
if (!uri.includes(":")) return [uri];
|
|
3109
3386
|
try {
|
|
@@ -3150,18 +3427,60 @@ function buildClientForGdr(defaultClient, resolver) {
|
|
|
3150
3427
|
function toEffectsContextEntries(ctx) {
|
|
3151
3428
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
3152
3429
|
}
|
|
3430
|
+
async function dispatchGatedWrite(args) {
|
|
3431
|
+
const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
|
|
3432
|
+
client,
|
|
3433
|
+
tag,
|
|
3434
|
+
instanceId,
|
|
3435
|
+
access,
|
|
3436
|
+
clock: args.clock,
|
|
3437
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3438
|
+
}), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
|
|
3439
|
+
return {
|
|
3440
|
+
instance: await reload(client, instanceId, tag),
|
|
3441
|
+
cascaded,
|
|
3442
|
+
fired: !0,
|
|
3443
|
+
...ranOps !== void 0 ? { ranOps } : {}
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3153
3446
|
function assertActionAllowed(evaluation, task, action) {
|
|
3154
3447
|
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
|
|
3155
3448
|
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
3156
3449
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
3157
3450
|
}
|
|
3158
|
-
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3451
|
+
function assertEditAllowed(evaluation, target) {
|
|
3452
|
+
const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
3453
|
+
if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
|
|
3454
|
+
throw new EditStateDeniedError({
|
|
3455
|
+
target: {
|
|
3456
|
+
scope: slot.scope,
|
|
3457
|
+
state: slot.name,
|
|
3458
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3459
|
+
},
|
|
3460
|
+
reason: slot.disabledReason
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
async function applyEdit(args) {
|
|
3464
|
+
const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
|
|
3465
|
+
return (await editState({
|
|
3466
|
+
client,
|
|
3467
|
+
instanceId: instance._id,
|
|
3468
|
+
target,
|
|
3469
|
+
...mode !== void 0 ? { mode } : {},
|
|
3470
|
+
...value !== void 0 ? { value } : {},
|
|
3471
|
+
options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
|
|
3472
|
+
})).ranOps;
|
|
3473
|
+
}
|
|
3474
|
+
function buildEngineCallOptions(args) {
|
|
3475
|
+
return {
|
|
3476
|
+
actor: args.actor,
|
|
3477
|
+
...args.grants !== void 0 ? { grants: args.grants } : {},
|
|
3478
|
+
...args.clock !== void 0 ? { clock: args.clock } : {},
|
|
3479
|
+
...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
|
|
3164
3480
|
};
|
|
3481
|
+
}
|
|
3482
|
+
async function applyAction(args) {
|
|
3483
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
|
|
3165
3484
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
3166
3485
|
client,
|
|
3167
3486
|
instanceId: instance._id,
|
|
@@ -3377,34 +3696,68 @@ const workflow = {
|
|
|
3377
3696
|
idempotent,
|
|
3378
3697
|
resourceClients
|
|
3379
3698
|
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3380
|
-
|
|
3381
|
-
return { instance: before, cascaded: 0, fired: !1 };
|
|
3382
|
-
const evaluation = await evaluateInstance({
|
|
3699
|
+
return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
|
|
3383
3700
|
client,
|
|
3384
3701
|
tag,
|
|
3385
3702
|
instanceId,
|
|
3386
3703
|
access,
|
|
3704
|
+
actor,
|
|
3705
|
+
clientForGdr,
|
|
3387
3706
|
clock,
|
|
3388
|
-
|
|
3707
|
+
before,
|
|
3708
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3709
|
+
apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
|
|
3710
|
+
client,
|
|
3711
|
+
instance,
|
|
3712
|
+
task,
|
|
3713
|
+
action,
|
|
3714
|
+
actor,
|
|
3715
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3716
|
+
clock,
|
|
3717
|
+
clientForGdr,
|
|
3718
|
+
...params !== void 0 ? { params } : {}
|
|
3719
|
+
}))
|
|
3389
3720
|
});
|
|
3390
|
-
|
|
3391
|
-
|
|
3721
|
+
},
|
|
3722
|
+
/**
|
|
3723
|
+
* Edit a declared-editable state slot directly — reassign, reschedule,
|
|
3724
|
+
* claim-by-hand, append to a running log — through the generic edit seam,
|
|
3725
|
+
* instead of a bespoke action per field. Soft-gates on the slot's declared
|
|
3726
|
+
* editability (the same projection a UI renders), applies the edit as a
|
|
3727
|
+
* `state.*` op (so provenance + history are stamped by the op path),
|
|
3728
|
+
* refreshes the stage's guards, then cascades — an edit to a value a
|
|
3729
|
+
* transition reads can and should move the instance. Advisory like every
|
|
3730
|
+
* engine gate; the lake ACL + guards are the only hard locks.
|
|
3731
|
+
*
|
|
3732
|
+
* Each call is a discrete COMMIT (a history entry, a guard refresh, a
|
|
3733
|
+
* cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
|
|
3734
|
+
* UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
|
|
3735
|
+
* never an `onChange` per keystroke.
|
|
3736
|
+
*/
|
|
3737
|
+
editState: async (args) => {
|
|
3738
|
+
const { client, tag, instanceId, target, mode, value, resourceClients } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3739
|
+
return dispatchGatedWrite({
|
|
3392
3740
|
client,
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3741
|
+
tag,
|
|
3742
|
+
instanceId,
|
|
3743
|
+
access,
|
|
3396
3744
|
actor,
|
|
3397
|
-
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3398
|
-
clock,
|
|
3399
3745
|
clientForGdr,
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3746
|
+
clock,
|
|
3747
|
+
before,
|
|
3748
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3749
|
+
apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
|
|
3750
|
+
client,
|
|
3751
|
+
instance,
|
|
3752
|
+
target,
|
|
3753
|
+
...mode !== void 0 ? { mode } : {},
|
|
3754
|
+
...value !== void 0 ? { value } : {},
|
|
3755
|
+
actor,
|
|
3756
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3757
|
+
clock,
|
|
3758
|
+
clientForGdr
|
|
3759
|
+
}))
|
|
3760
|
+
});
|
|
3408
3761
|
},
|
|
3409
3762
|
/**
|
|
3410
3763
|
* Report a queued effect's outcome. Drains it from `pendingEffects`,
|
|
@@ -3822,6 +4175,9 @@ function createInstanceSession(args) {
|
|
|
3822
4175
|
...clock !== void 0 ? { now: clock() } : {},
|
|
3823
4176
|
...grants !== void 0 ? { grants } : {}
|
|
3824
4177
|
});
|
|
4178
|
+
}, settleAfterApply = async (actor, held, ranOps) => {
|
|
4179
|
+
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
4180
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3825
4181
|
}, commit = async (run) => {
|
|
3826
4182
|
committing = !0;
|
|
3827
4183
|
const held = new Map(overlay);
|
|
@@ -3872,8 +4228,25 @@ function createInstanceSession(args) {
|
|
|
3872
4228
|
...grants !== void 0 ? { grants } : {},
|
|
3873
4229
|
...clock !== void 0 ? { clock } : {},
|
|
3874
4230
|
...params !== void 0 ? { params } : {}
|
|
3875
|
-
})
|
|
3876
|
-
return
|
|
4231
|
+
});
|
|
4232
|
+
return settleAfterApply(actor, held, ranOps);
|
|
4233
|
+
});
|
|
4234
|
+
},
|
|
4235
|
+
editState({ target, mode, value }) {
|
|
4236
|
+
return commit(async (actor, held) => {
|
|
4237
|
+
assertEditAllowed(await evaluateWith(held, heldGuards), target);
|
|
4238
|
+
const { grants } = await access(), ranOps = await applyEdit({
|
|
4239
|
+
client,
|
|
4240
|
+
instance,
|
|
4241
|
+
target,
|
|
4242
|
+
...mode !== void 0 ? { mode } : {},
|
|
4243
|
+
...value !== void 0 ? { value } : {},
|
|
4244
|
+
actor,
|
|
4245
|
+
clientForGdr,
|
|
4246
|
+
...grants !== void 0 ? { grants } : {},
|
|
4247
|
+
...clock !== void 0 ? { clock } : {}
|
|
4248
|
+
});
|
|
4249
|
+
return settleAfterApply(actor, held, ranOps);
|
|
3877
4250
|
});
|
|
3878
4251
|
}
|
|
3879
4252
|
};
|
|
@@ -3921,6 +4294,7 @@ function createEngine(args) {
|
|
|
3921
4294
|
deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
|
|
3922
4295
|
startInstance: (rest) => workflow.startInstance(bind(rest)),
|
|
3923
4296
|
fireAction: (rest) => workflow.fireAction(bind(rest)),
|
|
4297
|
+
editState: (rest) => workflow.editState(bind(rest)),
|
|
3924
4298
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3925
4299
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3926
4300
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
@@ -4216,10 +4590,12 @@ export {
|
|
|
4216
4590
|
ActionDisabledError,
|
|
4217
4591
|
ActionParamsInvalidError,
|
|
4218
4592
|
CascadeLimitError,
|
|
4593
|
+
ConcurrentEditStateError,
|
|
4219
4594
|
ConcurrentFireActionError,
|
|
4220
4595
|
DEFAULT_CONTENT_PERSPECTIVE,
|
|
4221
4596
|
DISPLAY,
|
|
4222
4597
|
EFFECTS_CONTEXT_DISPLAY,
|
|
4598
|
+
EditStateDeniedError,
|
|
4223
4599
|
GUARD_DOC_TYPE,
|
|
4224
4600
|
GUARD_LIFTED_PREDICATE,
|
|
4225
4601
|
GUARD_OWNER,
|