@sanity/workflow-engine 0.15.0 → 0.17.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.js CHANGED
@@ -1,611 +1,6 @@
1
- import { printGuardRead, andConditions, expandRequiredRoles, normalizeRoleAliases, parseOrThrow, checkWorkflowInvariants, formatValidationError, AuthoringWorkflowSchema, labelFor, WorkflowConfigSchema, AuthoringStageSchema, AuthoringActivitySchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GroupSchema, AuthoringGuardSchema, EffectSchema } from "./_chunks-es/invariants.js";
1
+ import { parseOrThrow, desugarWorkflow, checkWorkflowInvariants, formatValidationError, AuthoringWorkflowSchema, labelFor, WorkflowConfigSchema, AuthoringStageSchema, AuthoringActivitySchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GroupSchema, AuthoringGuardSchema, EffectSchema } from "./_chunks-es/invariants.js";
2
2
 
3
- import { CONDITION_VARS, FILTER_SCOPE_VARS, GUARD_PREDICATE_VARS, RESERVED_CONDITION_VARS } from "./_chunks-es/invariants.js";
4
-
5
- function groq(strings, ...values) {
6
- return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
7
- }
8
-
9
- function serializeGroqValue(value) {
10
- const serialized = JSON.stringify(value);
11
- if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
12
- return serialized;
13
- }
14
-
15
- function desugarWorkflow(authoring) {
16
- const issues = [];
17
- checkReservedRoleAliasKeys(authoring.roleAliases, issues);
18
- const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
19
- issues: issues,
20
- claimFields: /* @__PURE__ */ new Map,
21
- claimedFields: /* @__PURE__ */ new Set,
22
- roleAliases: roleAliases
23
- }, workflowFields = desugarFieldEntries({
24
- entries: authoring.fields,
25
- path: [ "fields" ],
26
- ctx: ctx
27
- }), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
28
- const path = [ "stages", i ], stageFields = desugarFieldEntries({
29
- entries: stage.fields,
30
- path: [ ...path, "fields" ],
31
- ctx: ctx
32
- }), stageEnv = {
33
- layers: [ {
34
- scope: "stage",
35
- entries: layerOf(stageFields)
36
- }, {
37
- scope: "workflow",
38
- entries: workflowLayer
39
- } ]
40
- }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
41
- activity: activity,
42
- path: [ ...path, "activities", j ],
43
- stageEnv: stageEnv,
44
- ctx: ctx
45
- })), transitions = (stage.transitions ?? []).map((transition, k) => desugarTransition({
46
- transition: transition,
47
- path: [ ...path, "transitions", k ],
48
- stageEnv: stageEnv,
49
- ctx: ctx
50
- })), editable = desugarStageEditable({
51
- overrides: stage.editable,
52
- inScope: editableFieldNames({
53
- workflowFields: workflowFields,
54
- stageFields: stageFields,
55
- activities: activities
56
- }),
57
- path: [ ...path, "editable" ],
58
- ctx: ctx
59
- });
60
- return {
61
- ...stripUndefined({
62
- name: stage.name,
63
- title: stage.title,
64
- description: stage.description,
65
- groups: stage.groups,
66
- guards: stage.guards?.map(desugarGuard)
67
- }),
68
- ...stageFields ? {
69
- fields: stageFields
70
- } : {},
71
- ...activities.length > 0 ? {
72
- activities: activities
73
- } : {},
74
- ...transitions.length > 0 ? {
75
- transitions: transitions
76
- } : {},
77
- ...editable ? {
78
- editable: editable
79
- } : {}
80
- };
81
- });
82
- return checkUnclaimedClaimFields(ctx), {
83
- definition: {
84
- ...stripUndefined({
85
- name: authoring.name,
86
- title: authoring.title,
87
- description: authoring.description,
88
- groups: authoring.groups,
89
- lifecycle: authoring.lifecycle,
90
- applicableWhen: authoring.applicableWhen,
91
- initialStage: authoring.initialStage,
92
- predicates: authoring.predicates,
93
- roleAliases: roleAliases
94
- }),
95
- ...workflowFields ? {
96
- fields: workflowFields
97
- } : {},
98
- stages: stages
99
- },
100
- issues: ctx.issues
101
- };
102
- }
103
-
104
- function checkReservedRoleAliasKeys(aliases, issues) {
105
- for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
106
- path: [ "roleAliases", key ],
107
- message: `role alias key "${key}" uses the reserved "$" prefix — that namespace is the engine's stored spelling for the universal fulfiller. Use "*" to mean "fulfills any gate", or rename the role.`
108
- });
109
- }
110
-
111
- const TODOLIST_OF = [ {
112
- type: "string",
113
- name: "label",
114
- title: "Label"
115
- }, {
116
- type: "string",
117
- name: "status",
118
- title: "Status"
119
- }, {
120
- type: "assignee",
121
- name: "assignee",
122
- title: "Assignee"
123
- }, {
124
- type: "date",
125
- name: "dueDate",
126
- title: "Due date"
127
- } ], NOTES_OF = [ {
128
- type: "text",
129
- name: "body",
130
- title: "Body"
131
- }, {
132
- type: "actor",
133
- name: "actor",
134
- title: "Actor"
135
- }, {
136
- type: "datetime",
137
- name: "at",
138
- title: "At"
139
- } ];
140
-
141
- function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
142
- return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
143
- entry: entry,
144
- path: [ ...path, i ],
145
- ctx: ctx
146
- }));
147
- }
148
-
149
- function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
150
- if (entry.type === "claim") return desugarClaimField({
151
- entry: entry,
152
- path: path,
153
- ctx: ctx
154
- });
155
- if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
156
- entry: entry,
157
- path: path,
158
- ctx: ctx
159
- });
160
- const editable = normalizeEditable({
161
- editable: entry.editable,
162
- path: [ ...path, "editable" ],
163
- ctx: ctx
164
- });
165
- return {
166
- ...stripUndefined({
167
- type: entry.type,
168
- name: entry.name,
169
- title: entry.title,
170
- description: entry.description,
171
- group: normalizeGroup(entry.group),
172
- required: entry.required,
173
- initialValue: entry.initialValue,
174
- editable: editable,
175
- types: entry.types,
176
- fields: entry.fields,
177
- of: entry.of
178
- })
179
- };
180
- }
181
-
182
- function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
183
- const desugared = {
184
- ...stripUndefined({
185
- name: entry.name,
186
- title: entry.title,
187
- description: entry.description,
188
- group: normalizeGroup(entry.group)
189
- }),
190
- type: "actor"
191
- };
192
- return ctx.claimFields.set(desugared, {
193
- name: entry.name,
194
- path: path
195
- }), desugared;
196
- }
197
-
198
- function desugarListField({entry: entry, path: path, ctx: ctx}) {
199
- const editable = normalizeEditable({
200
- editable: entry.editable,
201
- path: [ ...path, "editable" ],
202
- ctx: ctx
203
- });
204
- return {
205
- ...stripUndefined({
206
- name: entry.name,
207
- title: entry.title,
208
- description: entry.description,
209
- group: normalizeGroup(entry.group),
210
- required: entry.required,
211
- initialValue: entry.initialValue,
212
- editable: editable
213
- }),
214
- type: "array",
215
- of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
216
- };
217
- }
218
-
219
- function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
220
- if (editable === void 0 || editable === !0) return editable;
221
- if (Array.isArray(editable)) {
222
- const condition = rolesCondition(editable, ctx.roleAliases);
223
- if (condition === void 0) {
224
- ctx.issues.push({
225
- path: path,
226
- message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
227
- });
228
- return;
229
- }
230
- return condition;
231
- }
232
- return editable;
233
- }
234
-
235
- function editableFieldNames({workflowFields: workflowFields, stageFields: stageFields, activities: activities}) {
236
- const names = /* @__PURE__ */ new Set, collect = entries => {
237
- for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
238
- };
239
- collect(workflowFields), collect(stageFields);
240
- for (const activity of activities) collect(activity.fields);
241
- return names;
242
- }
243
-
244
- function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
245
- if (overrides === void 0) return;
246
- const out = {};
247
- for (const [name, value] of Object.entries(overrides)) {
248
- if (!inScope.has(name)) {
249
- ctx.issues.push({
250
- path: [ ...path, name ],
251
- message: `stage editable override "${name}" does not narrow an editable field in scope — name a workflow/stage/activity field of this stage that declares \`editable\``
252
- });
253
- continue;
254
- }
255
- const normalized = normalizeEditable({
256
- editable: value,
257
- path: [ ...path, name ],
258
- ctx: ctx
259
- });
260
- normalized !== void 0 && (out[name] = normalized);
261
- }
262
- return Object.keys(out).length > 0 ? out : void 0;
263
- }
264
-
265
- function layerOf(entries) {
266
- return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
267
- }
268
-
269
- function normalizeGroup(group) {
270
- return group === void 0 || Array.isArray(group) ? group : [ group ];
271
- }
272
-
273
- function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
274
- const activityFields = desugarFieldEntries({
275
- entries: activity.fields,
276
- path: [ ...path, "fields" ],
277
- ctx: ctx
278
- }), env = {
279
- layers: [ {
280
- scope: "activity",
281
- entries: layerOf(activityFields)
282
- }, ...stageEnv.layers ]
283
- }, ops = desugarOps({
284
- ops: activity.ops,
285
- path: [ ...path, "ops" ],
286
- env: env,
287
- firingActivity: activity.name,
288
- ctx: ctx
289
- }), actions = (activity.actions ?? []).map((action, a) => desugarAction({
290
- action: action,
291
- path: [ ...path, "actions", a ],
292
- env: env,
293
- activityName: activity.name,
294
- ctx: ctx
295
- })), target = desugarTarget({
296
- target: activity.target,
297
- env: env,
298
- path: [ ...path, "target" ],
299
- ctx: ctx
300
- });
301
- return {
302
- ...stripUndefined({
303
- name: activity.name,
304
- title: activity.title,
305
- description: activity.description,
306
- groups: activity.groups,
307
- group: normalizeGroup(activity.group),
308
- kind: activity.kind,
309
- filter: activity.filter,
310
- requirements: activity.requirements,
311
- completeWhen: activity.completeWhen,
312
- failWhen: activity.failWhen,
313
- effects: activity.effects,
314
- subworkflows: activity.subworkflows
315
- }),
316
- activation: activity.activation ?? "manual",
317
- ...target ? {
318
- target: target
319
- } : {},
320
- ...activityFields ? {
321
- fields: activityFields
322
- } : {},
323
- ...ops ? {
324
- ops: ops
325
- } : {},
326
- ...actions.length > 0 ? {
327
- actions: actions
328
- } : {}
329
- };
330
- }
331
-
332
- const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
333
-
334
- function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
335
- if (target === void 0 || target.type === "url") return target;
336
- const ref = typeof target.field == "string" ? {
337
- field: target.field
338
- } : target.field, field = resolveRef({
339
- ref: ref,
340
- env: env,
341
- path: [ ...path, "field" ],
342
- ctx: ctx
343
- }) ?? fallbackRef(ref), entry = entryAt(env, field);
344
- return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
345
- path: [ ...path, "field" ],
346
- message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
347
- }), {
348
- type: "field",
349
- field: field
350
- };
351
- }
352
-
353
- function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
354
- if ("type" in action) return desugarClaimAction({
355
- action: action,
356
- path: path,
357
- env: env,
358
- ctx: ctx
359
- });
360
- const filter = andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
361
- ops: action.ops,
362
- path: [ ...path, "ops" ],
363
- env: env,
364
- firingActivity: activityName,
365
- ctx: ctx
366
- }) ?? [];
367
- return action.status !== void 0 && ops.push({
368
- type: "status.set",
369
- activity: activityName,
370
- status: action.status
371
- }), {
372
- ...stripUndefined({
373
- name: action.name,
374
- title: action.title,
375
- description: action.description,
376
- group: normalizeGroup(action.group),
377
- params: action.params,
378
- effects: action.effects
379
- }),
380
- ...filter ? {
381
- filter: filter
382
- } : {},
383
- ...ops.length > 0 ? {
384
- ops: ops
385
- } : {}
386
- };
387
- }
388
-
389
- function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
390
- const ref = typeof action.field == "string" ? {
391
- field: action.field
392
- } : action.field, resolved = resolveRef({
393
- ref: ref,
394
- env: env,
395
- path: [ ...path, "field" ],
396
- ctx: ctx
397
- });
398
- if (resolved) {
399
- const entry = entryAt(env, resolved);
400
- entry && entry.type !== "actor" && ctx.issues.push({
401
- path: [ ...path, "field" ],
402
- message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" — a claim pair needs an actor-valued entry (a "claim" or "actor" field declaration)`
403
- }), checkShadowedClaimTarget({
404
- actionName: action.name,
405
- field: ref.field,
406
- resolved: resolved,
407
- env: env,
408
- path: [ ...path, "field" ],
409
- ctx: ctx
410
- }), entry && ctx.claimedFields.add(entry);
411
- }
412
- const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
413
- type: "field.set",
414
- target: resolved,
415
- value: {
416
- type: "actor"
417
- }
418
- } ] : [];
419
- return {
420
- ...stripUndefined({
421
- name: action.name,
422
- title: action.title,
423
- description: action.description,
424
- group: normalizeGroup(action.group),
425
- params: action.params,
426
- effects: action.effects
427
- }),
428
- filter: filter,
429
- ...ops.length > 0 ? {
430
- ops: ops
431
- } : {}
432
- };
433
- }
434
-
435
- function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
436
- const nearest = env.layers.find(l => l.entries.has(field));
437
- nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
438
- path: path,
439
- message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields — the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
440
- });
441
- }
442
-
443
- function checkUnclaimedClaimFields(ctx) {
444
- for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
445
- path: path,
446
- message: `claim field "${name}" is never referenced by a claim action — you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw actor entry`
447
- });
448
- }
449
-
450
- function desugarTransition({transition: transition, path: path, stageEnv: stageEnv, ctx: ctx}) {
451
- const ops = desugarOps({
452
- ops: transition.ops,
453
- path: [ ...path, "ops" ],
454
- env: stageEnv,
455
- firingActivity: void 0,
456
- ctx: ctx
457
- });
458
- return {
459
- ...stripUndefined({
460
- name: transition.name,
461
- title: transition.title,
462
- description: transition.description,
463
- to: transition.to,
464
- effects: transition.effects
465
- }),
466
- filter: transition.filter ?? "$allActivitiesDone",
467
- ...ops ? {
468
- ops: ops
469
- } : {}
470
- };
471
- }
472
-
473
- function desugarGuard(guard) {
474
- const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
475
- return {
476
- ...rest,
477
- match: {
478
- ...matchRest,
479
- ...idRefs !== void 0 ? {
480
- idRefs: idRefs.map(printGuardRead)
481
- } : {}
482
- },
483
- ...metadata !== void 0 ? {
484
- metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
485
- } : {}
486
- };
487
- }
488
-
489
- function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
490
- if (ops) return ops.map((op, i) => desugarOp({
491
- op: op,
492
- path: [ ...path, i ],
493
- env: env,
494
- firingActivity: firingActivity,
495
- ctx: ctx
496
- }));
497
- }
498
-
499
- function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
500
- if (op.type === "status.set") {
501
- const activity = op.activity ?? firingActivity;
502
- return activity === void 0 && ctx.issues.push({
503
- path: [ ...path, "activity" ],
504
- message: "status.set outside an action must name its target activity"
505
- }), {
506
- type: "status.set",
507
- activity: activity ?? "",
508
- status: op.status
509
- };
510
- }
511
- if (op.type === "audit") return desugarAuditOp({
512
- op: op,
513
- path: path,
514
- env: env,
515
- ctx: ctx
516
- });
517
- const target = resolveRef({
518
- ref: op.target,
519
- env: env,
520
- path: [ ...path, "target" ],
521
- ctx: ctx
522
- }) ?? fallbackRef(op.target);
523
- return {
524
- ...op,
525
- target: target
526
- };
527
- }
528
-
529
- const AUDIT_STAMPS = {
530
- actor: {
531
- type: "actor"
532
- },
533
- at: {
534
- type: "now"
535
- }
536
- };
537
-
538
- function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
539
- const target = resolveRef({
540
- ref: op.target,
541
- env: env,
542
- path: [ ...path, "target" ],
543
- ctx: ctx
544
- }) ?? fallbackRef(op.target);
545
- if (op.value.type !== "object") return ctx.issues.push({
546
- path: [ ...path, "value" ],
547
- message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
548
- }), {
549
- type: "field.append",
550
- target: target,
551
- value: op.value
552
- };
553
- const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
554
- ...op.value.fields
555
- };
556
- for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
557
- if (stamp in fields) {
558
- ctx.issues.push({
559
- path: [ ...path, "value", "fields", stamp ],
560
- message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
561
- });
562
- continue;
563
- }
564
- fields[stamp] = source;
565
- }
566
- return {
567
- type: "field.append",
568
- target: target,
569
- value: {
570
- type: "object",
571
- fields: fields
572
- }
573
- };
574
- }
575
-
576
- function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
577
- const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
578
- for (const layer of layers) if (layer.entries.has(ref.field)) return {
579
- scope: layer.scope,
580
- field: ref.field
581
- };
582
- const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
583
- ctx.issues.push({
584
- path: path,
585
- message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
586
- });
587
- }
588
-
589
- function entryAt(env, ref) {
590
- return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
591
- }
592
-
593
- function fallbackRef(ref) {
594
- return {
595
- scope: ref.scope ?? "workflow",
596
- field: ref.field
597
- };
598
- }
599
-
600
- function rolesCondition(roles, aliases) {
601
- if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
602
- }
603
-
604
- function stripUndefined(obj) {
605
- const out = {};
606
- for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
607
- return out;
608
- }
3
+ import { CONDITION_VARS, FILTER_SCOPE_VARS, GUARD_PREDICATE_VARS, RESERVED_CONDITION_VARS, groq } from "./_chunks-es/invariants.js";
609
4
 
610
5
  function defineWorkflow(definition) {
611
6
  const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow({