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