@sanity/workflow-engine 0.28.0 → 0.30.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/CHANGELOG.md +201 -0
- package/DATAMODEL.md +171 -19
- package/dist/_chunks-cjs/invariants.cjs +465 -175
- package/dist/_chunks-es/invariants.js +454 -176
- package/dist/define.d.cts +472 -281
- package/dist/define.d.ts +472 -281
- package/dist/index.cjs +1823 -1025
- package/dist/index.d.cts +1316 -1059
- package/dist/index.d.ts +1316 -1059
- package/dist/index.js +1817 -1048
- package/package.json +3 -3
|
@@ -41,79 +41,6 @@ function andConditions(parts) {
|
|
|
41
41
|
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
class WorkflowError extends Error {
|
|
45
|
-
kind;
|
|
46
|
-
constructor(kind, message, options) {
|
|
47
|
-
super(message, options), this.kind = kind;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
class ContractViolationError extends WorkflowError {
|
|
52
|
-
constructor(message) {
|
|
53
|
-
super("contract-violation", message), this.name = "ContractViolationError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
class InstanceNotFoundError extends WorkflowError {
|
|
58
|
-
instanceId;
|
|
59
|
-
constructor(args) {
|
|
60
|
-
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
61
|
-
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class DefinitionNotFoundError extends WorkflowError {
|
|
66
|
-
definition;
|
|
67
|
-
version;
|
|
68
|
-
constructor(args) {
|
|
69
|
-
super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
|
|
70
|
-
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
class SpawnContractsInvalidError extends WorkflowError {
|
|
75
|
-
issues;
|
|
76
|
-
constructor(args) {
|
|
77
|
-
super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
|
|
78
|
-
this.issues = args.issues;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
class DefinitionInUseError extends WorkflowError {
|
|
83
|
-
definition;
|
|
84
|
-
blockedBy;
|
|
85
|
-
constructor(args) {
|
|
86
|
-
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
87
|
-
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function definitionInUseMessage(definition, blockedBy) {
|
|
92
|
-
if (blockedBy.reason === "non-terminal-instances") {
|
|
93
|
-
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
94
|
-
return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
|
|
95
|
-
}
|
|
96
|
-
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
97
|
-
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
class EffectNotFoundError extends WorkflowError {
|
|
101
|
-
instanceId;
|
|
102
|
-
effectKey;
|
|
103
|
-
settled;
|
|
104
|
-
constructor(args) {
|
|
105
|
-
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
106
|
-
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function effectNotFoundMessage(args) {
|
|
111
|
-
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
112
|
-
if (args.settled === void 0) return base;
|
|
113
|
-
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
114
|
-
return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
44
|
const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
|
|
118
45
|
|
|
119
46
|
class VersionSpecificDatasetGdrError extends Error {
|
|
@@ -329,6 +256,79 @@ function isGdr(value) {
|
|
|
329
256
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
330
257
|
}
|
|
331
258
|
|
|
259
|
+
class WorkflowError extends Error {
|
|
260
|
+
kind;
|
|
261
|
+
constructor(kind, message, options) {
|
|
262
|
+
super(message, options), this.kind = kind;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class ContractViolationError extends WorkflowError {
|
|
267
|
+
constructor(message) {
|
|
268
|
+
super("contract-violation", message), this.name = "ContractViolationError";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
class InstanceNotFoundError extends WorkflowError {
|
|
273
|
+
instanceId;
|
|
274
|
+
constructor(args) {
|
|
275
|
+
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
276
|
+
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class DefinitionNotFoundError extends WorkflowError {
|
|
281
|
+
definition;
|
|
282
|
+
version;
|
|
283
|
+
constructor(args) {
|
|
284
|
+
super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
|
|
285
|
+
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
class SpawnContractsInvalidError extends WorkflowError {
|
|
290
|
+
issues;
|
|
291
|
+
constructor(args) {
|
|
292
|
+
super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
|
|
293
|
+
this.issues = args.issues;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class DefinitionInUseError extends WorkflowError {
|
|
298
|
+
definition;
|
|
299
|
+
blockedBy;
|
|
300
|
+
constructor(args) {
|
|
301
|
+
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
302
|
+
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function definitionInUseMessage(definition, blockedBy) {
|
|
307
|
+
if (blockedBy.reason === "non-terminal-instances") {
|
|
308
|
+
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
309
|
+
return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
|
|
310
|
+
}
|
|
311
|
+
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
312
|
+
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
class EffectNotFoundError extends WorkflowError {
|
|
316
|
+
instanceId;
|
|
317
|
+
effectKey;
|
|
318
|
+
settled;
|
|
319
|
+
constructor(args) {
|
|
320
|
+
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
321
|
+
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function effectNotFoundMessage(args) {
|
|
326
|
+
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
327
|
+
if (args.settled === void 0) return base;
|
|
328
|
+
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
329
|
+
return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
332
|
const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
|
|
333
333
|
|
|
334
334
|
function validateTag(tag) {
|
|
@@ -525,6 +525,7 @@ function desugarWorkflow(authoring) {
|
|
|
525
525
|
return {
|
|
526
526
|
...stripUndefined({
|
|
527
527
|
name: stage.name,
|
|
528
|
+
semantics: stage.semantics,
|
|
528
529
|
title: stage.title,
|
|
529
530
|
description: stage.description,
|
|
530
531
|
groups: stage.groups,
|
|
@@ -548,6 +549,7 @@ function desugarWorkflow(authoring) {
|
|
|
548
549
|
definition: {
|
|
549
550
|
...stripUndefined({
|
|
550
551
|
name: authoring.name,
|
|
552
|
+
semantics: authoring.semantics,
|
|
551
553
|
title: authoring.title,
|
|
552
554
|
description: authoring.description,
|
|
553
555
|
groups: authoring.groups,
|
|
@@ -652,6 +654,7 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
|
|
|
652
654
|
options: entry.options,
|
|
653
655
|
validation: entry.validation,
|
|
654
656
|
types: entry.types,
|
|
657
|
+
roles: entry.roles,
|
|
655
658
|
fields: entry.fields,
|
|
656
659
|
of: entry.of
|
|
657
660
|
})
|
|
@@ -774,6 +777,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
774
777
|
return {
|
|
775
778
|
...stripUndefined({
|
|
776
779
|
name: activity.name,
|
|
780
|
+
semantics: activity.semantics,
|
|
777
781
|
title: activity.title,
|
|
778
782
|
description: activity.description,
|
|
779
783
|
groups: activity.groups,
|
|
@@ -1245,7 +1249,66 @@ function isTerminalActivityStatus(status) {
|
|
|
1245
1249
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1246
1250
|
}
|
|
1247
1251
|
|
|
1248
|
-
const
|
|
1252
|
+
const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ];
|
|
1253
|
+
|
|
1254
|
+
function releaseDocId(releaseName) {
|
|
1255
|
+
return `_.releases.${releaseName}`;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1259
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1260
|
+
return {
|
|
1261
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1262
|
+
type: "system.release",
|
|
1263
|
+
releaseName: releaseName
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function isAlwaysArrayFieldKind(kind) {
|
|
1268
|
+
return kind === "doc.refs" || kind === "assignees" || kind === "array";
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function isSingleDocRefKind(kind) {
|
|
1272
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function refKindAcceptsTypes(kind) {
|
|
1276
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function assignmentKindAcceptsRoles(kind) {
|
|
1280
|
+
return kind === "assignee" || kind === "assignees";
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function isSingleDocRefEntry(entry) {
|
|
1284
|
+
return isSingleDocRefKind(entry._type);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function isAssignmentFieldEntry(entry) {
|
|
1288
|
+
return assignmentKindAcceptsRoles(entry._type);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function isTodoListItem(row) {
|
|
1292
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1293
|
+
const candidate = row, status = candidate.status;
|
|
1294
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function declaredRowColumns(entry) {
|
|
1298
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function isTodoListEntry(entry) {
|
|
1302
|
+
const columns = declaredRowColumns(entry);
|
|
1303
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function isNotesEntry(entry) {
|
|
1307
|
+
const columns = declaredRowColumns(entry);
|
|
1308
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const CONDITION_VARS = [ {
|
|
1249
1312
|
name: "self",
|
|
1250
1313
|
binding: "always",
|
|
1251
1314
|
label: "this workflow instance",
|
|
@@ -1320,6 +1383,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
|
|
|
1320
1383
|
binding: "caller",
|
|
1321
1384
|
label: "your permissions",
|
|
1322
1385
|
description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (fireAction-action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition `when`s, activity filters, cascade-fired actions' `when`/`filter`, effect bindings, where-op `where`s, and the spawn `forEach`/`with`/`context` sites."
|
|
1386
|
+
}, {
|
|
1387
|
+
name: "attributes",
|
|
1388
|
+
binding: "caller",
|
|
1389
|
+
label: "your attributes",
|
|
1390
|
+
description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
|
|
1323
1391
|
}, {
|
|
1324
1392
|
name: "row",
|
|
1325
1393
|
binding: "spawn",
|
|
@@ -1485,54 +1553,7 @@ function documentIdOf(doc) {
|
|
|
1485
1553
|
return "(unknown id)";
|
|
1486
1554
|
}
|
|
1487
1555
|
|
|
1488
|
-
const ACTOR_KINDS = [ "person", "agent", "system" ]
|
|
1489
|
-
|
|
1490
|
-
function releaseDocId(releaseName) {
|
|
1491
|
-
return `_.releases.${releaseName}`;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1495
|
-
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1496
|
-
return {
|
|
1497
|
-
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1498
|
-
type: "system.release",
|
|
1499
|
-
releaseName: releaseName
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
function isSingleDocRefKind(kind) {
|
|
1504
|
-
return kind === "doc.ref" || kind === "subject";
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function refKindAcceptsTypes(kind) {
|
|
1508
|
-
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function isSingleDocRefEntry(entry) {
|
|
1512
|
-
return isSingleDocRefKind(entry._type);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function isTodoListItem(row) {
|
|
1516
|
-
if (typeof row != "object" || row === null) return !1;
|
|
1517
|
-
const candidate = row, status = candidate.status;
|
|
1518
|
-
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function declaredRowColumns(entry) {
|
|
1522
|
-
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
function isTodoListEntry(entry) {
|
|
1526
|
-
const columns = declaredRowColumns(entry);
|
|
1527
|
-
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function isNotesEntry(entry) {
|
|
1531
|
-
const columns = declaredRowColumns(entry);
|
|
1532
|
-
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1556
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1536
1557
|
|
|
1537
1558
|
function classifyPrincipalId(id) {
|
|
1538
1559
|
if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
|
|
@@ -1575,6 +1596,10 @@ function lakePrincipalId(args) {
|
|
|
1575
1596
|
return args.localPrincipalId ?? args.actor.id;
|
|
1576
1597
|
}
|
|
1577
1598
|
|
|
1599
|
+
function isRecord(value) {
|
|
1600
|
+
return typeof value == "object" && value !== null && !Array.isArray(value);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1578
1603
|
class FieldValueShapeError extends WorkflowError {
|
|
1579
1604
|
entryType;
|
|
1580
1605
|
entryName;
|
|
@@ -1671,9 +1696,16 @@ const fieldValueSchemas = {
|
|
|
1671
1696
|
query: v.any()
|
|
1672
1697
|
};
|
|
1673
1698
|
|
|
1674
|
-
function shapeValueSchema(
|
|
1675
|
-
|
|
1676
|
-
if (shape.type === "
|
|
1699
|
+
function shapeValueSchema(args) {
|
|
1700
|
+
const {shape: shape, leaf: leaf} = args;
|
|
1701
|
+
if (shape.type === "object") return objectSchema({
|
|
1702
|
+
fields: shape.fields ?? [],
|
|
1703
|
+
leaf: leaf
|
|
1704
|
+
});
|
|
1705
|
+
if (shape.type === "array") return v.array(objectSchema({
|
|
1706
|
+
fields: shape.of ?? [],
|
|
1707
|
+
leaf: leaf
|
|
1708
|
+
}));
|
|
1677
1709
|
const schema = leaf[shape.type] ?? v.any();
|
|
1678
1710
|
return constrainedScalarSchema({
|
|
1679
1711
|
schema: schema,
|
|
@@ -1724,19 +1756,31 @@ function constrainedScalarSchema(args) {
|
|
|
1724
1756
|
}) ?? [] ].join("; ")));
|
|
1725
1757
|
}
|
|
1726
1758
|
|
|
1727
|
-
function objectSchema(
|
|
1728
|
-
const entries = /* @__PURE__ */ Object.create(null);
|
|
1729
|
-
for (const f of fields) entries[f.name] = v.optional(shapeValueSchema(
|
|
1759
|
+
function objectSchema(args) {
|
|
1760
|
+
const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
|
|
1761
|
+
for (const f of fields) entries[f.name] = v.optional(shapeValueSchema({
|
|
1762
|
+
shape: f,
|
|
1763
|
+
leaf: leaf
|
|
1764
|
+
}));
|
|
1730
1765
|
return v.looseObject(entries);
|
|
1731
1766
|
}
|
|
1732
1767
|
|
|
1733
1768
|
function wholeValueSchema(args) {
|
|
1734
1769
|
const {entryType: entryType, shape: shape, leaf: leaf} = args;
|
|
1735
|
-
return entryType === "object" ? v.union([ v.null(), objectSchema(
|
|
1770
|
+
return entryType === "object" ? v.union([ v.null(), objectSchema({
|
|
1771
|
+
fields: shape.fields ?? [],
|
|
1772
|
+
leaf: leaf
|
|
1773
|
+
}) ]) : entryType === "array" ? v.array(objectSchema({
|
|
1774
|
+
fields: shape.of ?? [],
|
|
1775
|
+
leaf: leaf
|
|
1776
|
+
})) : leaf[entryType];
|
|
1736
1777
|
}
|
|
1737
1778
|
|
|
1738
1779
|
function appendItemSchema(entryType, shape) {
|
|
1739
|
-
if (entryType === "array") return objectSchema(
|
|
1780
|
+
if (entryType === "array") return objectSchema({
|
|
1781
|
+
fields: shape.of ?? [],
|
|
1782
|
+
leaf: valueSchemas
|
|
1783
|
+
});
|
|
1740
1784
|
if (entryType === "doc.refs") return GdrShape;
|
|
1741
1785
|
if (entryType === "assignees") return AssigneeWriteShape;
|
|
1742
1786
|
}
|
|
@@ -1788,7 +1832,7 @@ function checkValueAgainst(args, leaf) {
|
|
|
1788
1832
|
entryType: args.entryType,
|
|
1789
1833
|
validation: args.validation,
|
|
1790
1834
|
value: args.value
|
|
1791
|
-
});
|
|
1835
|
+
}) ?? assignmentValueIssues(args, result.output);
|
|
1792
1836
|
return postIssues !== void 0 ? {
|
|
1793
1837
|
issues: postIssues
|
|
1794
1838
|
} : {
|
|
@@ -1796,6 +1840,142 @@ function checkValueAgainst(args, leaf) {
|
|
|
1796
1840
|
};
|
|
1797
1841
|
}
|
|
1798
1842
|
|
|
1843
|
+
function assignmentIdentity(assignee) {
|
|
1844
|
+
return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function assignmentCandidate(value) {
|
|
1848
|
+
const parsed = v.safeParse(AssigneeShape, value);
|
|
1849
|
+
return parsed.success ? parsed.output : void 0;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function assignmentValueIssues(args, value) {
|
|
1853
|
+
const retained = previousAssignmentIdentities(args), issues = [];
|
|
1854
|
+
return visitAssignmentEntries({
|
|
1855
|
+
entryType: args.entryType,
|
|
1856
|
+
value: value,
|
|
1857
|
+
roles: args.roles,
|
|
1858
|
+
fields: args.fields,
|
|
1859
|
+
of: args.of,
|
|
1860
|
+
path: "$"
|
|
1861
|
+
}, ({assignee: assignee, roles: roles, path: path}) => {
|
|
1862
|
+
consumeRetainedAssignment({
|
|
1863
|
+
retained: retained,
|
|
1864
|
+
path: path,
|
|
1865
|
+
identity: assignmentIdentity(assignee)
|
|
1866
|
+
}) || issues.push(...assignmentCandidateIssues({
|
|
1867
|
+
roles: roles,
|
|
1868
|
+
memberRoles: args.memberRoles,
|
|
1869
|
+
roleAliases: args.roleAliases
|
|
1870
|
+
}, assignee));
|
|
1871
|
+
}), issues.length === 0 ? void 0 : issues;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function consumeRetainedAssignment(args) {
|
|
1875
|
+
const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
|
|
1876
|
+
return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
|
|
1877
|
+
!0);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function previousAssignmentIdentities(args) {
|
|
1881
|
+
const retained = /* @__PURE__ */ new Map;
|
|
1882
|
+
if (args.previousValue === void 0 || args.entryType === "assignee") return retained;
|
|
1883
|
+
const schema = wholeValueSchema({
|
|
1884
|
+
entryType: args.entryType,
|
|
1885
|
+
shape: args,
|
|
1886
|
+
leaf: valueSchemas
|
|
1887
|
+
});
|
|
1888
|
+
if (schema === void 0) return retained;
|
|
1889
|
+
const previous = v.safeParse(schema, args.previousValue);
|
|
1890
|
+
return previous.success && visitAssignmentEntries({
|
|
1891
|
+
entryType: args.entryType,
|
|
1892
|
+
value: previous.output,
|
|
1893
|
+
roles: args.roles,
|
|
1894
|
+
fields: args.fields,
|
|
1895
|
+
of: args.of,
|
|
1896
|
+
path: "$"
|
|
1897
|
+
}, ({assignee: assignee, path: path}) => {
|
|
1898
|
+
const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
|
|
1899
|
+
identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
|
|
1900
|
+
}), retained;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function visitAssignmentEntries(args, visit) {
|
|
1904
|
+
if (assignmentKindAcceptsRoles(args.entryType)) {
|
|
1905
|
+
visitAssignmentCandidates(args, visit);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (args.entryType === "object") {
|
|
1909
|
+
visitAssignmentFields({
|
|
1910
|
+
fields: args.fields,
|
|
1911
|
+
value: args.value,
|
|
1912
|
+
path: args.path
|
|
1913
|
+
}, visit);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
|
|
1917
|
+
fields: args.of,
|
|
1918
|
+
value: row,
|
|
1919
|
+
path: `${args.path}[]`
|
|
1920
|
+
}, visit);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function visitAssignmentCandidates(args, visit) {
|
|
1924
|
+
if (args.roles === void 0) return;
|
|
1925
|
+
const indexedValues = args.entryType === "assignees" && Array.isArray(args.value) ? args.value.map(value => ({
|
|
1926
|
+
value: value,
|
|
1927
|
+
path: args.path
|
|
1928
|
+
})) : [ {
|
|
1929
|
+
value: args.value,
|
|
1930
|
+
path: args.path
|
|
1931
|
+
} ];
|
|
1932
|
+
for (const {value: value, path: path} of indexedValues) {
|
|
1933
|
+
const assignee = assignmentCandidate(value);
|
|
1934
|
+
assignee !== void 0 && visit({
|
|
1935
|
+
assignee: assignee,
|
|
1936
|
+
roles: args.roles,
|
|
1937
|
+
path: path
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function visitAssignmentFields(args, visit) {
|
|
1943
|
+
if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
|
|
1944
|
+
shape: field,
|
|
1945
|
+
value: args.value[field.name],
|
|
1946
|
+
path: `${args.path}.${field.name}`
|
|
1947
|
+
}, visit);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function visitAssignmentShape(args, visit) {
|
|
1951
|
+
visitAssignmentEntries({
|
|
1952
|
+
entryType: args.shape.type,
|
|
1953
|
+
value: args.value,
|
|
1954
|
+
roles: args.shape.roles,
|
|
1955
|
+
fields: args.shape.fields,
|
|
1956
|
+
of: args.shape.of,
|
|
1957
|
+
path: args.path
|
|
1958
|
+
}, visit);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function assignmentCandidateIssues(args, assignee) {
|
|
1962
|
+
if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
|
|
1963
|
+
const held = args.memberRoles?.[assignee.id];
|
|
1964
|
+
return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
|
|
1965
|
+
actorRoles: held,
|
|
1966
|
+
required: required,
|
|
1967
|
+
aliases: args.roleAliases
|
|
1968
|
+
})) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function collectiveRoleIssues(args, role) {
|
|
1972
|
+
return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function formatEligibleRoles(roles) {
|
|
1976
|
+
return roles.map(role => `"${role}"`).join(", ");
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1799
1979
|
function issuesOf(check) {
|
|
1800
1980
|
return "issues" in check ? check.issues : void 0;
|
|
1801
1981
|
}
|
|
@@ -1855,6 +2035,20 @@ function validateFieldAppendItem(args) {
|
|
|
1855
2035
|
issues: formatIssues(result.issues),
|
|
1856
2036
|
mode: "item"
|
|
1857
2037
|
});
|
|
2038
|
+
const assignmentArgs = {
|
|
2039
|
+
entryType: appendAssignmentEntryType(args.entryType),
|
|
2040
|
+
value: result.output,
|
|
2041
|
+
roles: args.roles,
|
|
2042
|
+
fields: args.entryType === "array" ? args.of : void 0,
|
|
2043
|
+
memberRoles: args.memberRoles,
|
|
2044
|
+
roleAliases: args.roleAliases
|
|
2045
|
+
}, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
|
|
2046
|
+
if (assignmentIssues !== void 0) throw new FieldValueShapeError({
|
|
2047
|
+
entryType: args.entryType,
|
|
2048
|
+
entryName: args.entryName,
|
|
2049
|
+
issues: assignmentIssues,
|
|
2050
|
+
mode: "item"
|
|
2051
|
+
});
|
|
1858
2052
|
const typeIssues = refTypeIssues({
|
|
1859
2053
|
entryType: args.entryType,
|
|
1860
2054
|
types: args.types,
|
|
@@ -1869,6 +2063,10 @@ function validateFieldAppendItem(args) {
|
|
|
1869
2063
|
return result.output;
|
|
1870
2064
|
}
|
|
1871
2065
|
|
|
2066
|
+
function appendAssignmentEntryType(entryType) {
|
|
2067
|
+
return entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1872
2070
|
function formatIssues(issues, formatMessage = issue => issue.message) {
|
|
1873
2071
|
const formatOne = (issue, prefix) => {
|
|
1874
2072
|
const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
|
|
@@ -1961,6 +2159,10 @@ function opSchemas(targetSchema) {
|
|
|
1961
2159
|
type: v.literal("field.set"),
|
|
1962
2160
|
target: targetSchema,
|
|
1963
2161
|
value: ValueExprSchema
|
|
2162
|
+
}), v.strictObject({
|
|
2163
|
+
type: v.literal("field.setIfMissing"),
|
|
2164
|
+
target: targetSchema,
|
|
2165
|
+
value: ValueExprSchema
|
|
1964
2166
|
}), v.strictObject({
|
|
1965
2167
|
type: v.literal("field.unset"),
|
|
1966
2168
|
target: targetSchema
|
|
@@ -1968,6 +2170,14 @@ function opSchemas(targetSchema) {
|
|
|
1968
2170
|
type: v.literal("field.append"),
|
|
1969
2171
|
target: targetSchema,
|
|
1970
2172
|
value: ValueExprSchema
|
|
2173
|
+
}), v.strictObject({
|
|
2174
|
+
type: v.literal("field.inc"),
|
|
2175
|
+
target: targetSchema,
|
|
2176
|
+
value: v.optional(ValueExprSchema)
|
|
2177
|
+
}), v.strictObject({
|
|
2178
|
+
type: v.literal("field.dec"),
|
|
2179
|
+
target: targetSchema,
|
|
2180
|
+
value: v.optional(ValueExprSchema)
|
|
1971
2181
|
}), v.strictObject({
|
|
1972
2182
|
type: v.literal("field.updateWhere"),
|
|
1973
2183
|
target: targetSchema,
|
|
@@ -2049,16 +2259,17 @@ function compositeChecked(entries) {
|
|
|
2049
2259
|
return v.pipe(v.strictObject(entries), v.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
|
|
2050
2260
|
}
|
|
2051
2261
|
|
|
2052
|
-
const FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
|
|
2262
|
+
const AssignmentRolesSchema = v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
|
|
2053
2263
|
type: FieldValueKindSchema,
|
|
2054
2264
|
name: FieldEntryName,
|
|
2055
2265
|
title: v.optional(v.string()),
|
|
2056
2266
|
description: v.optional(v.string()),
|
|
2057
2267
|
options: v.optional(ChoiceOptionsSchema),
|
|
2058
2268
|
validation: v.optional(ScalarValidationSchema),
|
|
2269
|
+
roles: v.optional(AssignmentRolesSchema),
|
|
2059
2270
|
fields: v.optional(v.array(FieldShapeSchema)),
|
|
2060
2271
|
of: v.optional(v.array(FieldShapeSchema))
|
|
2061
|
-
}), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
|
|
2272
|
+
}), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
|
|
2062
2273
|
|
|
2063
2274
|
function fieldBase(editable, group) {
|
|
2064
2275
|
return {
|
|
@@ -2079,6 +2290,7 @@ function fieldEntryFields({editable: editable, group: group, kind: kind = FieldK
|
|
|
2079
2290
|
options: v.optional(ChoiceOptionsSchema),
|
|
2080
2291
|
validation: v.optional(ScalarValidationSchema),
|
|
2081
2292
|
types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
|
|
2293
|
+
roles: v.optional(AssignmentRolesSchema),
|
|
2082
2294
|
fields: v.optional(v.array(FieldShapeSchema)),
|
|
2083
2295
|
of: v.optional(v.array(FieldShapeSchema))
|
|
2084
2296
|
};
|
|
@@ -2092,7 +2304,8 @@ function literalSeedIssues(entry) {
|
|
|
2092
2304
|
fields: entry.fields,
|
|
2093
2305
|
of: entry.of,
|
|
2094
2306
|
options: entry.options,
|
|
2095
|
-
validation: entry.validation
|
|
2307
|
+
validation: entry.validation,
|
|
2308
|
+
roles: entry.roles
|
|
2096
2309
|
});
|
|
2097
2310
|
}
|
|
2098
2311
|
|
|
@@ -2104,6 +2317,10 @@ function refTypesCheck() {
|
|
|
2104
2317
|
return v.check(entry => entry.types === void 0 || refKindAcceptsTypes(entry.type), issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` / \`subject\` entries, not "${issue.input.type}"`);
|
|
2105
2318
|
}
|
|
2106
2319
|
|
|
2320
|
+
function assignmentRolesCheck() {
|
|
2321
|
+
return v.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2107
2324
|
function choiceOptionsCheck() {
|
|
2108
2325
|
return v.check(entry => checkChoiceList({
|
|
2109
2326
|
entryType: entry.type,
|
|
@@ -2138,11 +2355,11 @@ function scalarValidationDeclarationIssues(entry) {
|
|
|
2138
2355
|
const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
|
|
2139
2356
|
editable: StoredEditableSchema,
|
|
2140
2357
|
group: StoredGroupMembershipSchema
|
|
2141
|
-
})), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
|
|
2358
|
+
})), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
|
|
2142
2359
|
editable: AuthoringEditableSchema,
|
|
2143
2360
|
group: AuthoringGroupMembershipSchema,
|
|
2144
2361
|
kind: AuthoringRawFieldKindSchema
|
|
2145
|
-
})), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
|
|
2362
|
+
})), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
|
|
2146
2363
|
type: v.literal("claim"),
|
|
2147
2364
|
name: FieldEntryName,
|
|
2148
2365
|
title: v.optional(v.string()),
|
|
@@ -2184,17 +2401,27 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
|
|
|
2184
2401
|
required: v.optional(v.boolean()),
|
|
2185
2402
|
options: v.optional(ChoiceOptionsSchema),
|
|
2186
2403
|
validation: v.optional(ScalarValidationSchema)
|
|
2187
|
-
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2404
|
+
}), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
|
|
2405
|
+
|
|
2406
|
+
function semanticNamespace(semantic) {
|
|
2407
|
+
return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
|
|
2408
|
+
}
|
|
2188
2409
|
|
|
2189
2410
|
function hasUniqueSemanticNamespaces(semantics) {
|
|
2190
|
-
const namespaces = semantics.map(
|
|
2411
|
+
const namespaces = semantics.map(semanticNamespace);
|
|
2191
2412
|
return new Set(namespaces).size === namespaces.length;
|
|
2192
2413
|
}
|
|
2193
2414
|
|
|
2415
|
+
function semanticsFieldSchema(semantic) {
|
|
2416
|
+
return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
|
|
2420
|
+
|
|
2194
2421
|
function actionFields(op, group) {
|
|
2195
2422
|
return {
|
|
2196
2423
|
name: NonEmpty,
|
|
2197
|
-
semantics:
|
|
2424
|
+
semantics: ActionSemanticsFieldSchema,
|
|
2198
2425
|
title: v.optional(v.string()),
|
|
2199
2426
|
description: v.optional(v.string()),
|
|
2200
2427
|
group: v.optional(group),
|
|
@@ -2241,6 +2468,7 @@ const StoredActionSchema = pinned()(v.strictObject({
|
|
|
2241
2468
|
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
2242
2469
|
return {
|
|
2243
2470
|
name: NonEmpty,
|
|
2471
|
+
semantics: SemanticsFieldSchema,
|
|
2244
2472
|
title: v.optional(v.string()),
|
|
2245
2473
|
description: v.optional(v.string()),
|
|
2246
2474
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2314,6 +2542,7 @@ const GuardSchema = v.strictObject(guardFields(NonEmpty)), AuthoringGuardSchema
|
|
|
2314
2542
|
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
2315
2543
|
return {
|
|
2316
2544
|
name: NonEmpty,
|
|
2545
|
+
semantics: SemanticsFieldSchema,
|
|
2317
2546
|
title: v.optional(v.string()),
|
|
2318
2547
|
description: v.optional(v.string()),
|
|
2319
2548
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2352,6 +2581,7 @@ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KIN
|
|
|
2352
2581
|
function workflowFields({field: field, stage: stage, start: start}) {
|
|
2353
2582
|
return {
|
|
2354
2583
|
name: NonEmpty,
|
|
2584
|
+
semantics: SemanticsFieldSchema,
|
|
2355
2585
|
title: NonEmpty,
|
|
2356
2586
|
description: v.optional(v.string()),
|
|
2357
2587
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2548,11 +2778,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
2548
2778
|
});
|
|
2549
2779
|
}
|
|
2550
2780
|
|
|
2781
|
+
function actionSites(def) {
|
|
2782
|
+
return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
|
|
2783
|
+
action: action,
|
|
2784
|
+
activity: activity,
|
|
2785
|
+
stage: stage,
|
|
2786
|
+
path: [ "stages", i, "activities", j, "actions", a ]
|
|
2787
|
+
}))));
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2551
2790
|
function effectNameSites(def) {
|
|
2552
2791
|
const sites = [];
|
|
2553
|
-
for (const
|
|
2792
|
+
for (const {action: action, path: path} of actionSites(def)) collectEffects({
|
|
2554
2793
|
effects: action.effects,
|
|
2555
|
-
path: [
|
|
2794
|
+
path: [ ...path, "effects" ],
|
|
2556
2795
|
sites: sites
|
|
2557
2796
|
});
|
|
2558
2797
|
return sites;
|
|
@@ -2683,13 +2922,22 @@ function checkUnboundConditionVars(def, issues) {
|
|
|
2683
2922
|
}
|
|
2684
2923
|
}
|
|
2685
2924
|
|
|
2925
|
+
const SOFT_GATE_VARS = {
|
|
2926
|
+
can: {
|
|
2927
|
+
noun: "the caller's grants"
|
|
2928
|
+
},
|
|
2929
|
+
attributes: {
|
|
2930
|
+
noun: "the caller's org-level user attributes"
|
|
2931
|
+
}
|
|
2932
|
+
}, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
|
|
2933
|
+
|
|
2686
2934
|
function unboundVarsAt(site) {
|
|
2687
2935
|
const callerVars = unboundCallerVars(site.policy);
|
|
2688
2936
|
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2689
2937
|
}
|
|
2690
2938
|
|
|
2691
2939
|
function unboundCallerVars(policy) {
|
|
2692
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [
|
|
2940
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
|
|
2693
2941
|
}
|
|
2694
2942
|
|
|
2695
2943
|
const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
|
|
@@ -2699,7 +2947,12 @@ function rowVarMessage(site) {
|
|
|
2699
2947
|
}
|
|
2700
2948
|
|
|
2701
2949
|
function callerVarMessage(site, name) {
|
|
2702
|
-
|
|
2950
|
+
if (site.policy === "cascade") return `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\``;
|
|
2951
|
+
if (site.policy === "caller-bound") return `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote`;
|
|
2952
|
+
if (site.policy === "triggered-payload" && name === "params") return `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead`;
|
|
2953
|
+
const softGate = SOFT_GATE_VARS[name];
|
|
2954
|
+
if (softGate !== void 0) return `${site.label} reads $${name} — $${name} (${softGate.noun}) is bound only in the caller-bound projection (action filters, requirements, editable predicates); this site never binds it. Move the check to one of those sites or drop $${name}`;
|
|
2955
|
+
throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
|
|
2703
2956
|
}
|
|
2704
2957
|
|
|
2705
2958
|
function conditionSites(def) {
|
|
@@ -2954,9 +3207,9 @@ function belowScopeNestedSites(args) {
|
|
|
2954
3207
|
}
|
|
2955
3208
|
|
|
2956
3209
|
function checkLevelKindEffectOutputs(def, issues) {
|
|
2957
|
-
for (const
|
|
3210
|
+
for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
|
|
2958
3211
|
action: action,
|
|
2959
|
-
path:
|
|
3212
|
+
path: path,
|
|
2960
3213
|
issues: issues
|
|
2961
3214
|
});
|
|
2962
3215
|
}
|
|
@@ -3038,18 +3291,18 @@ function storedRolesIssue(action) {
|
|
|
3038
3291
|
}
|
|
3039
3292
|
|
|
3040
3293
|
function checkStoredRolesPlacement(def, issues) {
|
|
3041
|
-
for (const
|
|
3294
|
+
for (const {action: action, path: path} of actionSites(def)) {
|
|
3042
3295
|
const message = storedRolesIssue(action);
|
|
3043
3296
|
message !== void 0 && issues.push({
|
|
3044
|
-
path: [
|
|
3297
|
+
path: [ ...path, "roles" ],
|
|
3045
3298
|
message: message
|
|
3046
3299
|
});
|
|
3047
3300
|
}
|
|
3048
3301
|
}
|
|
3049
3302
|
|
|
3050
3303
|
function checkTriggeredActionParams(def, issues) {
|
|
3051
|
-
for (const
|
|
3052
|
-
path: [
|
|
3304
|
+
for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
3305
|
+
path: [ ...path, "params" ],
|
|
3053
3306
|
message: `action "${action.name}" declares params but is cascade-fired (\`when\`) — no caller ever supplies args to a trigger. Drop the params, or drop \`when\` to make it a fireAction-fired action`
|
|
3054
3307
|
});
|
|
3055
3308
|
}
|
|
@@ -3094,13 +3347,20 @@ function checkSingleSubjectRequirements(def, issues) {
|
|
|
3094
3347
|
|
|
3095
3348
|
function checkStartFilterReads(def, issues) {
|
|
3096
3349
|
const filter = def.start?.filter;
|
|
3097
|
-
filter
|
|
3350
|
+
if (filter === void 0) return;
|
|
3351
|
+
const params = conditionParameterNames(filter);
|
|
3352
|
+
params.has("fields") && issues.push({
|
|
3098
3353
|
path: [ "start", "filter" ],
|
|
3099
3354
|
message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to a start requirement, the start-time readiness predicate that binds $fields and is enforced by startInstance"
|
|
3100
|
-
})
|
|
3355
|
+
});
|
|
3356
|
+
for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
|
|
3357
|
+
path: [ "start", "filter" ],
|
|
3358
|
+
message: `start.filter reads $${name} — start.filter is a read-side document-visibility rule (definitionsForDocument / start controls); startInstance never evaluates it, and no caller bag binds there, so the read is GROQ null and the filter can never match. Drop $${name} from start.filter. A "who may start" rule belongs in a start requirement, not in the browse-time filter`
|
|
3359
|
+
});
|
|
3360
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
3101
3361
|
path: [ "start", "filter" ],
|
|
3102
3362
|
message: "start.filter reads the candidate document (its root), but the definition declares no `subject` entry — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a `subject` entry (the document the workflow is about), or gate on the dataset instead"
|
|
3103
|
-
})
|
|
3363
|
+
});
|
|
3104
3364
|
}
|
|
3105
3365
|
|
|
3106
3366
|
function checkStartRequirementReads(def, issues) {
|
|
@@ -3396,15 +3656,13 @@ function seedEarlierSiblingTarget(args) {
|
|
|
3396
3656
|
}
|
|
3397
3657
|
|
|
3398
3658
|
function opSites(def) {
|
|
3399
|
-
|
|
3400
|
-
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) sites.push({
|
|
3659
|
+
return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
|
|
3401
3660
|
ops: action.ops,
|
|
3402
|
-
path: [
|
|
3661
|
+
path: [ ...path, "ops" ],
|
|
3403
3662
|
label: `action "${action.name}"`,
|
|
3404
3663
|
stage: stage,
|
|
3405
3664
|
activity: activity
|
|
3406
|
-
});
|
|
3407
|
-
return sites;
|
|
3665
|
+
}));
|
|
3408
3666
|
}
|
|
3409
3667
|
|
|
3410
3668
|
function checkFieldReadOpValues(def, issues) {
|
|
@@ -3421,7 +3679,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
3421
3679
|
}
|
|
3422
3680
|
|
|
3423
3681
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
3424
|
-
for (const [o, op] of (ops ?? []).entries()) if ("value" in op) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3682
|
+
for (const [o, op] of (ops ?? []).entries()) if (!(!("value" in op) || op.value === void 0)) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3425
3683
|
...ctx,
|
|
3426
3684
|
read: read,
|
|
3427
3685
|
path: readPath,
|
|
@@ -3472,35 +3730,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
|
3472
3730
|
return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty. Known: ${known.join(", ") || "(none)"}`;
|
|
3473
3731
|
}
|
|
3474
3732
|
|
|
3475
|
-
function
|
|
3733
|
+
function checkFieldTargetOps(def, issues) {
|
|
3476
3734
|
const workflow = def.fields ?? [];
|
|
3477
3735
|
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
workflow: workflow,
|
|
3481
|
-
stage: site.stage.fields ?? [],
|
|
3482
|
-
activity: site.activity.fields ?? []
|
|
3483
|
-
};
|
|
3484
|
-
checkUpdateWhereTargetKind({
|
|
3736
|
+
const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
|
|
3737
|
+
rule !== void 0 && "target" in op && checkTargetKind({
|
|
3485
3738
|
op: op,
|
|
3486
|
-
path: [ ...
|
|
3739
|
+
path: [ ...path, "target" ],
|
|
3487
3740
|
label: site.label,
|
|
3488
|
-
scopes:
|
|
3741
|
+
scopes: opTargetScopes(workflow, site),
|
|
3742
|
+
rule: rule,
|
|
3489
3743
|
issues: issues
|
|
3490
|
-
}), checkUpdateWhereMergeKeys({
|
|
3744
|
+
}), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
|
|
3491
3745
|
op: op,
|
|
3492
|
-
path:
|
|
3746
|
+
path: path,
|
|
3493
3747
|
label: site.label,
|
|
3494
3748
|
issues: issues
|
|
3495
3749
|
});
|
|
3496
3750
|
}
|
|
3497
3751
|
}
|
|
3498
3752
|
|
|
3499
|
-
|
|
3753
|
+
const arithmeticTargetRule = {
|
|
3754
|
+
accepts: target => target.type === "number",
|
|
3755
|
+
issue: () => "arithmetic ops target `number` entries only"
|
|
3756
|
+
}, TARGET_KIND_RULES = {
|
|
3757
|
+
"field.inc": arithmeticTargetRule,
|
|
3758
|
+
"field.dec": arithmeticTargetRule,
|
|
3759
|
+
"field.setIfMissing": {
|
|
3760
|
+
accepts: target => !isAlwaysArrayFieldKind(target.type),
|
|
3761
|
+
issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
|
|
3762
|
+
},
|
|
3763
|
+
"field.updateWhere": {
|
|
3764
|
+
accepts: target => target.type === "array",
|
|
3765
|
+
issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3768
|
+
|
|
3769
|
+
function opTargetScopes(workflow, site) {
|
|
3770
|
+
return {
|
|
3771
|
+
workflow: workflow,
|
|
3772
|
+
stage: site.stage.fields ?? [],
|
|
3773
|
+
activity: site.activity.fields ?? []
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
|
|
3500
3778
|
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3501
|
-
target === void 0 || target
|
|
3502
|
-
path:
|
|
3503
|
-
message: `${label}
|
|
3779
|
+
target === void 0 || rule.accepts(target) || issues.push({
|
|
3780
|
+
path: path,
|
|
3781
|
+
message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
|
|
3504
3782
|
});
|
|
3505
3783
|
}
|
|
3506
3784
|
|
|
@@ -3732,11 +4010,11 @@ function checkWorkflowInvariants(def) {
|
|
|
3732
4010
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
3733
4011
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3734
4012
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3735
|
-
checkFieldReadOpValues(def, issues),
|
|
4013
|
+
checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3736
4014
|
checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
|
|
3737
4015
|
checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
|
|
3738
4016
|
checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
|
|
3739
4017
|
checkStoredRolesPlacement(def, issues), checkGroups(def, issues), issues;
|
|
3740
4018
|
}
|
|
3741
4019
|
|
|
3742
|
-
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|
|
4020
|
+
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assignmentKindAcceptsRoles, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isAlwaysArrayFieldKind, isAssignmentFieldEntry, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isRecord, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|