@sanity/workflow-engine 0.1.0 → 0.2.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/define.cjs +271 -249
- 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 +255 -249
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +626 -341
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5862 -3151
- package/dist/index.d.ts +5862 -3151
- package/dist/index.js +610 -341
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/define.js
CHANGED
|
@@ -1,67 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
|
|
3
|
-
"create",
|
|
4
|
-
"update",
|
|
5
|
-
"delete",
|
|
6
|
-
"publish",
|
|
7
|
-
"unpublish"
|
|
8
|
-
];
|
|
1
|
+
import * as v from "valibot";
|
|
9
2
|
function knownList(ids) {
|
|
10
3
|
return [...ids].map((s) => `"${s}"`).join(", ");
|
|
11
4
|
}
|
|
12
|
-
function checkStagesAndTasks(def,
|
|
5
|
+
function checkStagesAndTasks(def, issues) {
|
|
13
6
|
const stageIds = /* @__PURE__ */ new Set();
|
|
14
7
|
for (const [i, stage] of def.stages.entries())
|
|
15
|
-
stageIds.has(stage.id) &&
|
|
16
|
-
code: "custom",
|
|
8
|
+
stageIds.has(stage.id) && issues.push({
|
|
17
9
|
path: ["stages", i, "id"],
|
|
18
10
|
message: `duplicate stage id "${stage.id}" (already declared earlier)`
|
|
19
|
-
}), stageIds.add(stage.id), stage.kind === "terminal" && stage.transitions && stage.transitions.length > 0 &&
|
|
20
|
-
code: "custom",
|
|
11
|
+
}), stageIds.add(stage.id), stage.kind === "terminal" && stage.transitions && stage.transitions.length > 0 && issues.push({
|
|
21
12
|
path: ["stages", i, "transitions"],
|
|
22
13
|
message: `terminal stage "${stage.id}" must not declare transitions`
|
|
23
|
-
}), checkDuplicateTaskIds(stage, i,
|
|
14
|
+
}), checkDuplicateTaskIds(stage, i, issues);
|
|
24
15
|
return stageIds;
|
|
25
16
|
}
|
|
26
|
-
function checkDuplicateTaskIds(stage, i,
|
|
17
|
+
function checkDuplicateTaskIds(stage, i, issues) {
|
|
27
18
|
const taskIds = /* @__PURE__ */ new Set();
|
|
28
19
|
for (const [j, task] of (stage.tasks ?? []).entries())
|
|
29
|
-
taskIds.has(task.id) &&
|
|
30
|
-
code: "custom",
|
|
20
|
+
taskIds.has(task.id) && issues.push({
|
|
31
21
|
path: ["stages", i, "tasks", j, "id"],
|
|
32
22
|
message: `duplicate task id "${task.id}" in stage "${stage.id}"`
|
|
33
23
|
}), taskIds.add(task.id);
|
|
34
24
|
}
|
|
35
|
-
function checkInitialStage(def, stageIds,
|
|
36
|
-
stageIds.has(def.initialStageId) ||
|
|
37
|
-
code: "custom",
|
|
25
|
+
function checkInitialStage(def, stageIds, issues) {
|
|
26
|
+
stageIds.has(def.initialStageId) || issues.push({
|
|
38
27
|
path: ["initialStageId"],
|
|
39
28
|
message: `initialStageId "${def.initialStageId}" is not a declared stage. Known stages: ${knownList(stageIds)}`
|
|
40
29
|
});
|
|
41
30
|
}
|
|
42
|
-
function checkTransitionTargets(def, stageIds,
|
|
31
|
+
function checkTransitionTargets(def, stageIds, issues) {
|
|
43
32
|
for (const [i, stage] of def.stages.entries())
|
|
44
33
|
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
45
|
-
stageIds.has(t.to) ||
|
|
46
|
-
code: "custom",
|
|
34
|
+
stageIds.has(t.to) || issues.push({
|
|
47
35
|
path: ["stages", i, "transitions", k, "to"],
|
|
48
36
|
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageIds)}`
|
|
49
37
|
});
|
|
50
38
|
}
|
|
51
|
-
function collectPredicateIds(def,
|
|
39
|
+
function collectPredicateIds(def, issues) {
|
|
52
40
|
const predicateIds = /* @__PURE__ */ new Set();
|
|
53
41
|
for (const [i, p] of (def.predicates ?? []).entries())
|
|
54
|
-
predicateIds.has(p.id) &&
|
|
55
|
-
code: "custom",
|
|
42
|
+
predicateIds.has(p.id) && issues.push({
|
|
56
43
|
path: ["predicates", i, "id"],
|
|
57
44
|
message: `duplicate predicate id "${p.id}"`
|
|
58
45
|
}), predicateIds.add(p.id);
|
|
59
46
|
return predicateIds;
|
|
60
47
|
}
|
|
61
|
-
function checkFilterRefs(def, predicateIds,
|
|
48
|
+
function checkFilterRefs(def, predicateIds, issues) {
|
|
62
49
|
const visit = (g, path) => {
|
|
63
|
-
g === void 0 || typeof g == "string" || predicateIds.has(g.ref) ||
|
|
64
|
-
code: "custom",
|
|
50
|
+
g === void 0 || typeof g == "string" || predicateIds.has(g.ref) || issues.push({
|
|
65
51
|
path: [...path, "ref"],
|
|
66
52
|
message: `Filter references predicate "${g.ref}" which is not declared. Known predicates: ${knownList(predicateIds) || "(none)"}`
|
|
67
53
|
});
|
|
@@ -77,112 +63,127 @@ function checkFilterRefs(def, predicateIds, ctx) {
|
|
|
77
63
|
visit(t.filter, ["stages", i, "transitions", k, "filter"]);
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
|
-
function checkWorkflowInvariants(def
|
|
81
|
-
const stageIds = checkStagesAndTasks(def,
|
|
82
|
-
checkInitialStage(def, stageIds,
|
|
83
|
-
const predicateIds = collectPredicateIds(def,
|
|
84
|
-
checkFilterRefs(def, predicateIds,
|
|
66
|
+
function checkWorkflowInvariants(def) {
|
|
67
|
+
const issues = [], stageIds = checkStagesAndTasks(def, issues);
|
|
68
|
+
checkInitialStage(def, stageIds, issues), checkTransitionTargets(def, stageIds, issues);
|
|
69
|
+
const predicateIds = collectPredicateIds(def, issues);
|
|
70
|
+
return checkFilterRefs(def, predicateIds, issues), issues;
|
|
85
71
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
|
|
73
|
+
"create",
|
|
74
|
+
"update",
|
|
75
|
+
"delete",
|
|
76
|
+
"publish",
|
|
77
|
+
"unpublish"
|
|
78
|
+
], NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1));
|
|
79
|
+
function picklist(options) {
|
|
80
|
+
return v.picklist(
|
|
81
|
+
options,
|
|
82
|
+
`Invalid option: expected one of ${options.map((o) => `"${o}"`).join("|")}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const GdrSchema = v.strictObject({
|
|
86
|
+
id: v.pipe(
|
|
87
|
+
v.string(),
|
|
88
|
+
v.regex(
|
|
89
|
+
/^(dataset|canvas|media-library|dashboard):/,
|
|
90
|
+
"must be a GDR URI of form '<scheme>:<...id-parts>' where scheme is one of: dataset, canvas, media-library, dashboard"
|
|
91
|
+
)
|
|
90
92
|
),
|
|
91
93
|
type: NonEmpty
|
|
92
|
-
})
|
|
93
|
-
() =>
|
|
94
|
-
|
|
95
|
-
source:
|
|
96
|
-
value:
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
source:
|
|
105
|
-
scope:
|
|
94
|
+
}), SourceSchema = v.lazy(
|
|
95
|
+
() => v.union([
|
|
96
|
+
v.strictObject({
|
|
97
|
+
source: v.literal("literal"),
|
|
98
|
+
value: v.union([v.string(), v.number(), v.boolean(), v.null()])
|
|
99
|
+
}),
|
|
100
|
+
v.strictObject({ source: v.literal("param"), paramId: NonEmpty }),
|
|
101
|
+
v.strictObject({ source: v.literal("actor") }),
|
|
102
|
+
v.strictObject({ source: v.literal("now") }),
|
|
103
|
+
v.strictObject({ source: v.literal("self") }),
|
|
104
|
+
v.strictObject({ source: v.literal("stageId") }),
|
|
105
|
+
v.strictObject({
|
|
106
|
+
source: v.literal("stateRead"),
|
|
107
|
+
scope: picklist(STATE_SCOPES),
|
|
106
108
|
slotId: NonEmpty,
|
|
107
|
-
path:
|
|
108
|
-
})
|
|
109
|
+
path: v.optional(v.string())
|
|
110
|
+
}),
|
|
109
111
|
// effectsContext lookup — reads from `instance.effectsContext[id === ID].value`.
|
|
110
112
|
// Used by chained effects where one handler's output feeds another's binding.
|
|
111
|
-
|
|
112
|
-
source:
|
|
113
|
+
v.strictObject({
|
|
114
|
+
source: v.literal("effectOutput"),
|
|
113
115
|
contextId: NonEmpty,
|
|
114
|
-
path:
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
source:
|
|
116
|
+
path: v.optional(v.string())
|
|
117
|
+
}),
|
|
118
|
+
v.strictObject({ source: v.literal("row"), path: v.optional(v.string()) }),
|
|
119
|
+
v.strictObject({
|
|
120
|
+
source: v.literal("parentState"),
|
|
119
121
|
slotId: NonEmpty,
|
|
120
|
-
path:
|
|
121
|
-
})
|
|
122
|
+
path: v.optional(v.string())
|
|
123
|
+
}),
|
|
122
124
|
// Compound object source — resolves each field's Source recursively
|
|
123
125
|
// and packages the result into a JSON object. Lets ops append rich
|
|
124
126
|
// rows to `checklist` / `notes` / `assignees` slots, e.g.
|
|
125
127
|
// `{ source: "object", fields: { signer: { source: "actor" },
|
|
126
128
|
// at: { source: "now" } } }`.
|
|
127
|
-
|
|
128
|
-
source:
|
|
129
|
-
fields:
|
|
130
|
-
})
|
|
129
|
+
v.strictObject({
|
|
130
|
+
source: v.literal("object"),
|
|
131
|
+
fields: v.record(NonEmpty, SourceSchema)
|
|
132
|
+
})
|
|
131
133
|
])
|
|
132
|
-
), ScopeKeySchema =
|
|
133
|
-
scope:
|
|
134
|
+
), ScopeKeySchema = v.strictObject({
|
|
135
|
+
scope: picklist(STATE_SCOPES),
|
|
134
136
|
slotId: NonEmpty
|
|
135
|
-
})
|
|
136
|
-
() =>
|
|
137
|
-
|
|
137
|
+
}), OpPredicateSchema = v.lazy(
|
|
138
|
+
() => v.union([
|
|
139
|
+
v.strictObject({
|
|
138
140
|
field: NonEmpty,
|
|
139
141
|
equals: SourceSchema
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
}),
|
|
143
|
+
v.strictObject({ all: v.array(OpPredicateSchema) }),
|
|
144
|
+
v.strictObject({ any: v.array(OpPredicateSchema) })
|
|
143
145
|
])
|
|
144
|
-
), OpSchema =
|
|
145
|
-
|
|
146
|
-
type:
|
|
146
|
+
), OpSchema = v.variant("type", [
|
|
147
|
+
v.strictObject({
|
|
148
|
+
type: v.literal("workflow.op.state.set"),
|
|
147
149
|
target: ScopeKeySchema,
|
|
148
150
|
value: SourceSchema
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
type:
|
|
151
|
+
}),
|
|
152
|
+
v.strictObject({
|
|
153
|
+
type: v.literal("workflow.op.state.append"),
|
|
152
154
|
target: ScopeKeySchema,
|
|
153
155
|
item: SourceSchema
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
type:
|
|
156
|
+
}),
|
|
157
|
+
v.strictObject({
|
|
158
|
+
type: v.literal("workflow.op.state.updateWhere"),
|
|
157
159
|
target: ScopeKeySchema,
|
|
158
160
|
where: OpPredicateSchema,
|
|
159
|
-
merge:
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
type:
|
|
161
|
+
merge: v.record(NonEmpty, SourceSchema)
|
|
162
|
+
}),
|
|
163
|
+
v.strictObject({
|
|
164
|
+
type: v.literal("workflow.op.state.removeWhere"),
|
|
163
165
|
target: ScopeKeySchema,
|
|
164
166
|
where: OpPredicateSchema
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
type:
|
|
167
|
+
}),
|
|
168
|
+
v.strictObject({
|
|
169
|
+
type: v.literal("workflow.op.status.set"),
|
|
168
170
|
taskId: NonEmpty,
|
|
169
|
-
status:
|
|
170
|
-
})
|
|
171
|
-
]), StateSlotSourceSchema =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
kind:
|
|
171
|
+
status: picklist(TASK_STATUSES)
|
|
172
|
+
})
|
|
173
|
+
]), StateSlotSourceSchema = v.union([
|
|
174
|
+
v.strictObject({ kind: v.literal("init") }),
|
|
175
|
+
v.strictObject({
|
|
176
|
+
kind: v.literal("query"),
|
|
175
177
|
query: NonEmpty,
|
|
176
|
-
resource:
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
z.object({ kind: z.literal("computed"), compute: NonEmpty }).strict(),
|
|
178
|
+
resource: v.optional(v.union([v.literal("workflow"), v.literal("subject"), GdrSchema]))
|
|
179
|
+
}),
|
|
180
|
+
v.strictObject({ kind: v.literal("write") }),
|
|
180
181
|
/**
|
|
181
182
|
* Pre-populate the slot at resolution time with a constant value.
|
|
182
183
|
* Useful for stage- or task-scope slots that should start non-empty
|
|
183
184
|
* (e.g. a default headline, a starting tally).
|
|
184
185
|
*/
|
|
185
|
-
|
|
186
|
+
v.strictObject({ kind: v.literal("literal"), value: v.unknown() }),
|
|
186
187
|
/**
|
|
187
188
|
* Pre-populate by reading another already-resolved slot. `scope:
|
|
188
189
|
* "workflow"` reads from the workflow-scope `state[]` (resolved at
|
|
@@ -191,18 +192,13 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
191
192
|
* Optional `path` selects a nested field (e.g. `path: "id"` on a
|
|
192
193
|
* doc.ref slot).
|
|
193
194
|
*/
|
|
194
|
-
|
|
195
|
-
kind:
|
|
196
|
-
scope:
|
|
195
|
+
v.strictObject({
|
|
196
|
+
kind: v.literal("stateRead"),
|
|
197
|
+
scope: v.union([v.literal("workflow"), v.literal("stage")]),
|
|
197
198
|
slotId: NonEmpty,
|
|
198
|
-
path:
|
|
199
|
-
})
|
|
200
|
-
]),
|
|
201
|
-
id: NonEmpty,
|
|
202
|
-
title: z.string().optional(),
|
|
203
|
-
description: z.string().optional(),
|
|
204
|
-
source: StateSlotSourceSchema
|
|
205
|
-
}, StateSlotKindSchema = z.enum([
|
|
199
|
+
path: v.optional(v.string())
|
|
200
|
+
})
|
|
201
|
+
]), StateSlotKindSchema = picklist([
|
|
206
202
|
"workflow.state.doc.ref",
|
|
207
203
|
"workflow.state.doc.refs",
|
|
208
204
|
// Release reference. A workflow declares this slot to say "I target a
|
|
@@ -220,47 +216,48 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
220
216
|
"workflow.state.checklist",
|
|
221
217
|
"workflow.state.notes",
|
|
222
218
|
"workflow.state.assignees"
|
|
223
|
-
]), StateSlotSchema =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
228
|
-
|
|
219
|
+
]), StateSlotSchema = v.strictObject({
|
|
220
|
+
type: StateSlotKindSchema,
|
|
221
|
+
id: NonEmpty,
|
|
222
|
+
title: v.optional(v.string()),
|
|
223
|
+
description: v.optional(v.string()),
|
|
224
|
+
source: StateSlotSourceSchema
|
|
225
|
+
}), FilterSchema = v.union([
|
|
229
226
|
// Inline GROQ — only reserved params allowed at runtime.
|
|
230
227
|
NonEmpty,
|
|
231
228
|
// Reference to a named predicate, optionally with named args.
|
|
232
|
-
|
|
229
|
+
v.strictObject({
|
|
233
230
|
ref: NonEmpty,
|
|
234
|
-
args:
|
|
235
|
-
})
|
|
236
|
-
]), PredicateParamSchema =
|
|
231
|
+
args: v.optional(v.record(v.string(), v.unknown()))
|
|
232
|
+
})
|
|
233
|
+
]), PredicateParamSchema = v.strictObject({
|
|
237
234
|
id: NonEmpty,
|
|
238
|
-
title:
|
|
239
|
-
description:
|
|
240
|
-
type:
|
|
241
|
-
enum:
|
|
242
|
-
})
|
|
243
|
-
type:
|
|
235
|
+
title: v.optional(v.string()),
|
|
236
|
+
description: v.optional(v.string()),
|
|
237
|
+
type: picklist(["string", "number", "boolean", "string[]", "reference"]),
|
|
238
|
+
enum: v.optional(v.array(v.string()))
|
|
239
|
+
}), PredicateSchema = v.strictObject({
|
|
240
|
+
type: v.optional(v.literal("workflow.predicate")),
|
|
244
241
|
id: NonEmpty,
|
|
245
|
-
title:
|
|
246
|
-
description:
|
|
242
|
+
title: v.optional(v.string()),
|
|
243
|
+
description: v.optional(v.string()),
|
|
247
244
|
groq: NonEmpty,
|
|
248
|
-
params:
|
|
249
|
-
})
|
|
250
|
-
type:
|
|
245
|
+
params: v.optional(v.array(PredicateParamSchema))
|
|
246
|
+
}), EffectSchema = v.strictObject({
|
|
247
|
+
type: v.optional(v.literal("workflow.effect")),
|
|
251
248
|
id: NonEmpty,
|
|
252
|
-
title:
|
|
253
|
-
description:
|
|
249
|
+
title: v.optional(v.string()),
|
|
250
|
+
description: v.optional(v.string()),
|
|
254
251
|
// Bindings resolve typed Source values at commit time, producing
|
|
255
252
|
// concrete JSON in the queued effect's `params`. NO GROQ.
|
|
256
|
-
bindings:
|
|
257
|
-
input:
|
|
258
|
-
})
|
|
259
|
-
type:
|
|
253
|
+
bindings: v.optional(v.record(v.string(), SourceSchema)),
|
|
254
|
+
input: v.optional(v.record(v.string(), v.unknown()))
|
|
255
|
+
}), TerminalTaskStatus = picklist(["done", "skipped", "failed"]), PermissionSchema = picklist(DOCUMENT_VALUE_PERMISSIONS), ActionParamSchema = v.strictObject({
|
|
256
|
+
type: v.optional(v.literal("workflow.action.param")),
|
|
260
257
|
id: NonEmpty,
|
|
261
|
-
title:
|
|
262
|
-
description:
|
|
263
|
-
paramType:
|
|
258
|
+
title: v.optional(v.string()),
|
|
259
|
+
description: v.optional(v.string()),
|
|
260
|
+
paramType: picklist([
|
|
264
261
|
"string",
|
|
265
262
|
"number",
|
|
266
263
|
"boolean",
|
|
@@ -271,15 +268,15 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
271
268
|
"doc.refs",
|
|
272
269
|
"json"
|
|
273
270
|
]),
|
|
274
|
-
required:
|
|
275
|
-
})
|
|
276
|
-
type:
|
|
271
|
+
required: v.optional(v.boolean())
|
|
272
|
+
}), ActionSchema = v.strictObject({
|
|
273
|
+
type: v.optional(v.literal("workflow.action")),
|
|
277
274
|
id: NonEmpty,
|
|
278
|
-
title:
|
|
279
|
-
description:
|
|
280
|
-
filter:
|
|
275
|
+
title: v.optional(v.string()),
|
|
276
|
+
description: v.optional(v.string()),
|
|
277
|
+
filter: v.optional(FilterSchema),
|
|
281
278
|
/** Caller-supplied params, validated before ops/effects run. */
|
|
282
|
-
params:
|
|
279
|
+
params: v.optional(v.array(ActionParamSchema)),
|
|
283
280
|
/**
|
|
284
281
|
* Status the task flips to when this action fires. Omit to leave
|
|
285
282
|
* the task status unchanged — used by intermediate `claim` /
|
|
@@ -289,7 +286,7 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
289
286
|
* mid-flip back to `active` outside of the `workflow.op.status.set`
|
|
290
287
|
* op explicit path.
|
|
291
288
|
*/
|
|
292
|
-
setStatus:
|
|
289
|
+
setStatus: v.optional(TerminalTaskStatus),
|
|
293
290
|
/**
|
|
294
291
|
* Inline state-mutation primitives executed during the action commit.
|
|
295
292
|
* Each op runs against the in-memory mutation (deterministic, no
|
|
@@ -297,72 +294,74 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
297
294
|
* op applies. Logged on `history[]` as `workflow.history.opApplied`
|
|
298
295
|
* and surfaced on the `ActionResult.ranOps`.
|
|
299
296
|
*/
|
|
300
|
-
ops:
|
|
301
|
-
effects:
|
|
297
|
+
ops: v.optional(v.array(OpSchema)),
|
|
298
|
+
effects: v.optional(v.array(EffectSchema)),
|
|
302
299
|
/**
|
|
303
300
|
* Role gating — OR-match against `Actor.roles[]`. If omitted, no role
|
|
304
301
|
* gate. `"*"` in Actor.roles always matches (bench / all-access).
|
|
305
302
|
*/
|
|
306
|
-
roles:
|
|
303
|
+
roles: v.optional(v.array(NonEmpty)),
|
|
307
304
|
/**
|
|
308
305
|
* When true, the actor must appear in the task's `assignees[]` (as a
|
|
309
306
|
* user-kind assignee matching by id) for the action to be allowed.
|
|
310
307
|
*/
|
|
311
|
-
requireAssignment:
|
|
308
|
+
requireAssignment: v.optional(v.boolean()),
|
|
312
309
|
/**
|
|
313
310
|
* Which permission on the workflow.instance document the actor must
|
|
314
311
|
* hold (per supplied `grants`). Defaults to "update". Only checked
|
|
315
312
|
* when grants are supplied at evaluate/fireAction time.
|
|
316
313
|
*/
|
|
317
|
-
requiredPermission:
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
kind:
|
|
323
|
-
n:
|
|
324
|
-
})
|
|
325
|
-
]), LogicalDefinitionRefSchema =
|
|
314
|
+
requiredPermission: v.optional(PermissionSchema)
|
|
315
|
+
}), CompletionPolicySchema = v.union([
|
|
316
|
+
v.strictObject({ kind: v.literal("all") }),
|
|
317
|
+
v.strictObject({ kind: v.literal("any") }),
|
|
318
|
+
v.strictObject({
|
|
319
|
+
kind: v.literal("count"),
|
|
320
|
+
n: PositiveInt
|
|
321
|
+
})
|
|
322
|
+
]), LogicalDefinitionRefSchema = v.strictObject({
|
|
326
323
|
workflowId: NonEmpty,
|
|
327
|
-
version:
|
|
328
|
-
})
|
|
324
|
+
version: v.optional(v.union([PositiveInt, v.literal("latest")]))
|
|
325
|
+
}), SpawnForEachSchema = v.strictObject({
|
|
329
326
|
groq: NonEmpty,
|
|
330
|
-
as:
|
|
331
|
-
initialState:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
as: v.strictObject({
|
|
328
|
+
initialState: v.optional(
|
|
329
|
+
v.array(
|
|
330
|
+
v.strictObject({
|
|
331
|
+
type: StateSlotKindSchema,
|
|
332
|
+
id: NonEmpty,
|
|
333
|
+
value: SourceSchema
|
|
334
|
+
})
|
|
335
|
+
)
|
|
336
|
+
),
|
|
337
|
+
effectsContext: v.optional(v.record(v.string(), SourceSchema))
|
|
338
|
+
})
|
|
339
|
+
}), SpawnSchema = v.strictObject({
|
|
340
|
+
type: v.optional(v.literal("workflow.spawn")),
|
|
341
|
+
id: v.optional(v.string()),
|
|
342
|
+
title: v.optional(v.string()),
|
|
343
|
+
description: v.optional(v.string()),
|
|
345
344
|
definitionRef: LogicalDefinitionRefSchema,
|
|
346
345
|
forEach: SpawnForEachSchema,
|
|
347
|
-
completionPolicy:
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
]), TaskSchema =
|
|
352
|
-
type:
|
|
346
|
+
completionPolicy: v.optional(CompletionPolicySchema)
|
|
347
|
+
}), AssigneeSchema = v.variant("kind", [
|
|
348
|
+
v.strictObject({ kind: v.literal("user"), id: NonEmpty }),
|
|
349
|
+
v.strictObject({ kind: v.literal("role"), role: NonEmpty })
|
|
350
|
+
]), TaskSchema = v.strictObject({
|
|
351
|
+
type: v.optional(v.literal("workflow.task")),
|
|
353
352
|
id: NonEmpty,
|
|
354
|
-
title:
|
|
355
|
-
description:
|
|
356
|
-
assignees:
|
|
357
|
-
filter:
|
|
358
|
-
invokeOn:
|
|
353
|
+
title: v.optional(v.string()),
|
|
354
|
+
description: v.optional(v.string()),
|
|
355
|
+
assignees: v.optional(v.array(AssigneeSchema)),
|
|
356
|
+
filter: v.optional(FilterSchema),
|
|
357
|
+
invokeOn: v.optional(picklist(["stageEnter", "manual"])),
|
|
359
358
|
/**
|
|
360
359
|
* Auto-completion predicate — same shape as `transition.filter`. When
|
|
361
360
|
* truthy at activation or on any subsequent cascade, the engine
|
|
362
361
|
* flips the task to `done` with a `{ kind: "system", id: "engine.completeWhen" }`
|
|
363
362
|
* actor. A full filter, not just a wall-clock deadline.
|
|
364
363
|
*/
|
|
365
|
-
completeWhen:
|
|
364
|
+
completeWhen: v.optional(FilterSchema),
|
|
366
365
|
/**
|
|
367
366
|
* Auto-failure predicate — symmetric to `completeWhen`. Same shape,
|
|
368
367
|
* same evaluation cadence (activation + every cascade), but flips
|
|
@@ -372,72 +371,75 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
372
371
|
* wins — failure is the more notable signal and should not be
|
|
373
372
|
* silently swallowed.
|
|
374
373
|
*/
|
|
375
|
-
failWhen:
|
|
376
|
-
effects:
|
|
377
|
-
actions:
|
|
378
|
-
spawns:
|
|
379
|
-
contentRefs:
|
|
374
|
+
failWhen: v.optional(FilterSchema),
|
|
375
|
+
effects: v.optional(v.array(EffectSchema)),
|
|
376
|
+
actions: v.optional(v.array(ActionSchema)),
|
|
377
|
+
spawns: v.optional(SpawnSchema),
|
|
378
|
+
contentRefs: v.optional(v.array(v.unknown())),
|
|
380
379
|
/** Task-scoped state slots. Resolved at task activation time. */
|
|
381
|
-
state:
|
|
382
|
-
})
|
|
383
|
-
type:
|
|
380
|
+
state: v.optional(v.array(StateSlotSchema))
|
|
381
|
+
}), TransitionSchema = v.strictObject({
|
|
382
|
+
type: v.optional(v.literal("workflow.transition")),
|
|
384
383
|
id: NonEmpty,
|
|
385
|
-
title:
|
|
386
|
-
description:
|
|
384
|
+
title: v.optional(v.string()),
|
|
385
|
+
description: v.optional(v.string()),
|
|
387
386
|
to: NonEmpty,
|
|
388
|
-
on:
|
|
389
|
-
filter:
|
|
390
|
-
effects:
|
|
391
|
-
})
|
|
387
|
+
on: v.optional(v.string()),
|
|
388
|
+
filter: v.optional(FilterSchema),
|
|
389
|
+
effects: v.optional(v.array(EffectSchema))
|
|
390
|
+
}), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardMatchSchema = v.strictObject({
|
|
392
391
|
/** Subject `_type`(s); empty matches any type. */
|
|
393
|
-
types:
|
|
392
|
+
types: v.optional(v.array(NonEmpty)),
|
|
394
393
|
/** Target docs as `Source`s resolved at deploy to bare ids + the resource. */
|
|
395
|
-
idRefs:
|
|
394
|
+
idRefs: v.optional(v.array(SourceSchema)),
|
|
396
395
|
/** Glob id patterns (bare, resource-local). */
|
|
397
|
-
idPatterns:
|
|
398
|
-
actions:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
396
|
+
idPatterns: v.optional(v.array(NonEmpty)),
|
|
397
|
+
actions: v.pipe(
|
|
398
|
+
v.array(GuardActionSchema),
|
|
399
|
+
v.minLength(1, "a guard must match at least one action")
|
|
400
|
+
)
|
|
401
|
+
}), GuardSchema = v.strictObject({
|
|
402
|
+
name: v.optional(v.string()),
|
|
403
|
+
description: v.optional(v.string()),
|
|
402
404
|
match: GuardMatchSchema,
|
|
403
405
|
/**
|
|
404
406
|
* Lake GROQ predicate. Reads `document.before`/`document.after`, `mutation`,
|
|
405
407
|
* `guard`, and `identity()`. Bare ids/fields only — no GDRs. Omitted or
|
|
406
408
|
* empty means UNCONDITIONAL DENY.
|
|
407
409
|
*/
|
|
408
|
-
predicate:
|
|
410
|
+
predicate: v.optional(v.string()),
|
|
409
411
|
/**
|
|
410
412
|
* Projected workflow state the predicate reads as `guard.metadata.*`. Values
|
|
411
413
|
* are `Source`s resolved at deploy and re-synced by the engine. Copied data,
|
|
412
414
|
* never a cross-resource pointer.
|
|
413
415
|
*/
|
|
414
|
-
metadata:
|
|
415
|
-
})
|
|
416
|
-
type:
|
|
416
|
+
metadata: v.optional(v.record(NonEmpty, SourceSchema))
|
|
417
|
+
}), StageKindSchema = picklist(["initial", "intermediate", "terminal", "waiting"]), StageSchema = v.strictObject({
|
|
418
|
+
type: v.optional(v.literal("workflow.stage")),
|
|
417
419
|
id: NonEmpty,
|
|
418
|
-
title:
|
|
419
|
-
description:
|
|
420
|
-
kind:
|
|
421
|
-
tasks:
|
|
422
|
-
transitions:
|
|
423
|
-
completion:
|
|
424
|
-
onEnter:
|
|
425
|
-
onExit:
|
|
420
|
+
title: v.optional(v.string()),
|
|
421
|
+
description: v.optional(v.string()),
|
|
422
|
+
kind: v.optional(StageKindSchema),
|
|
423
|
+
tasks: v.optional(v.array(TaskSchema)),
|
|
424
|
+
transitions: v.optional(v.array(TransitionSchema)),
|
|
425
|
+
completion: v.optional(FilterSchema),
|
|
426
|
+
onEnter: v.optional(v.array(EffectSchema)),
|
|
427
|
+
onExit: v.optional(v.array(EffectSchema)),
|
|
426
428
|
/**
|
|
427
429
|
* Lake mutation guards active while this stage holds. Each compiles to a
|
|
428
430
|
* `temp.system.guard` doc deployed on stage entry and retracted on exit.
|
|
429
431
|
* Plural because one stage's intent may span resources (one guard each).
|
|
430
432
|
*/
|
|
431
|
-
guards:
|
|
432
|
-
contentRefs:
|
|
433
|
+
guards: v.optional(v.array(GuardSchema)),
|
|
434
|
+
contentRefs: v.optional(v.array(v.unknown())),
|
|
433
435
|
/** Stage-scoped state slots. Resolved at stage entry and persisted on
|
|
434
436
|
* the StageEntry. */
|
|
435
|
-
state:
|
|
436
|
-
})
|
|
437
|
+
state: v.optional(v.array(StateSlotSchema))
|
|
438
|
+
}), WorkflowDefinitionSchema = v.strictObject({
|
|
437
439
|
workflowId: NonEmpty,
|
|
438
|
-
version:
|
|
440
|
+
version: PositiveInt,
|
|
439
441
|
title: NonEmpty,
|
|
440
|
-
description:
|
|
442
|
+
description: v.optional(v.string()),
|
|
441
443
|
initialStageId: NonEmpty,
|
|
442
444
|
/**
|
|
443
445
|
* Workflow-level state slots. Persist for the instance lifetime.
|
|
@@ -445,16 +447,22 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
|
|
|
445
447
|
* `*[_id == $self][0].state[key == "<key>"][0].value`. Init-sourced
|
|
446
448
|
* slots are supplied at `startInstance` via `initialState`.
|
|
447
449
|
*/
|
|
448
|
-
state:
|
|
449
|
-
stages:
|
|
450
|
-
predicates:
|
|
451
|
-
})
|
|
452
|
-
function
|
|
453
|
-
const lines =
|
|
454
|
-
return `${label} failed validation (${
|
|
450
|
+
state: v.optional(v.array(StateSlotSchema)),
|
|
451
|
+
stages: v.pipe(v.array(StageSchema), v.minLength(1, "must declare at least one stage")),
|
|
452
|
+
predicates: v.optional(v.array(PredicateSchema))
|
|
453
|
+
});
|
|
454
|
+
function formatValidationError(label, issues) {
|
|
455
|
+
const lines = issues.map((issue) => ` - ${issue.path.length === 0 ? "(root)" : formatPath(issue.path)}: ${issue.message}`);
|
|
456
|
+
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):
|
|
455
457
|
${lines.join(`
|
|
456
458
|
`)}`;
|
|
457
459
|
}
|
|
460
|
+
function issuesFromValibot(issues) {
|
|
461
|
+
return issues.map((issue) => ({
|
|
462
|
+
path: issue.path ? issue.path.map((item) => item.key) : [],
|
|
463
|
+
message: issue.message
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
458
466
|
function formatPath(path) {
|
|
459
467
|
let out = "";
|
|
460
468
|
for (const seg of path)
|
|
@@ -462,11 +470,9 @@ function formatPath(path) {
|
|
|
462
470
|
return out;
|
|
463
471
|
}
|
|
464
472
|
function defineWorkflow(definition) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
typeof definition.workflowId == "string" ? `defineWorkflow("${definition.workflowId}")` : "defineWorkflow"
|
|
469
|
-
);
|
|
473
|
+
const workflowId = definition.workflowId, label = typeof workflowId == "string" ? `defineWorkflow("${workflowId}")` : "defineWorkflow", parsed = parseOrThrow(WorkflowDefinitionSchema, definition, label), issues = checkWorkflowInvariants(parsed);
|
|
474
|
+
if (issues.length > 0) throw new Error(formatValidationError(label, issues));
|
|
475
|
+
return parsed;
|
|
470
476
|
}
|
|
471
477
|
function defineStage(stage) {
|
|
472
478
|
return parseOrThrow(StageSchema, stage, labelFor("defineStage", stage, "id"));
|
|
@@ -501,15 +507,15 @@ function defineEffectDescriptor(descriptor) {
|
|
|
501
507
|
return descriptor;
|
|
502
508
|
}
|
|
503
509
|
function parseOrThrow(schema, input, label) {
|
|
504
|
-
const result =
|
|
510
|
+
const result = v.safeParse(schema, input);
|
|
505
511
|
if (!result.success)
|
|
506
|
-
throw new Error(
|
|
507
|
-
return result.
|
|
512
|
+
throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
|
|
513
|
+
return result.output;
|
|
508
514
|
}
|
|
509
515
|
function labelFor(fn, value, key) {
|
|
510
516
|
if (value && typeof value == "object" && key in value) {
|
|
511
|
-
const
|
|
512
|
-
if (typeof
|
|
517
|
+
const raw = value[key];
|
|
518
|
+
if (typeof raw == "string") return `${fn}("${raw}")`;
|
|
513
519
|
}
|
|
514
520
|
return fn;
|
|
515
521
|
}
|