@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
|
@@ -55,79 +55,6 @@ function andConditions(parts) {
|
|
|
55
55
|
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
class WorkflowError extends Error {
|
|
59
|
-
kind;
|
|
60
|
-
constructor(kind, message, options) {
|
|
61
|
-
super(message, options), this.kind = kind;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class ContractViolationError extends WorkflowError {
|
|
66
|
-
constructor(message) {
|
|
67
|
-
super("contract-violation", message), this.name = "ContractViolationError";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class InstanceNotFoundError extends WorkflowError {
|
|
72
|
-
instanceId;
|
|
73
|
-
constructor(args) {
|
|
74
|
-
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
75
|
-
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
class DefinitionNotFoundError extends WorkflowError {
|
|
80
|
-
definition;
|
|
81
|
-
version;
|
|
82
|
-
constructor(args) {
|
|
83
|
-
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`),
|
|
84
|
-
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
class SpawnContractsInvalidError extends WorkflowError {
|
|
89
|
-
issues;
|
|
90
|
-
constructor(args) {
|
|
91
|
-
super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
|
|
92
|
-
this.issues = args.issues;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
class DefinitionInUseError extends WorkflowError {
|
|
97
|
-
definition;
|
|
98
|
-
blockedBy;
|
|
99
|
-
constructor(args) {
|
|
100
|
-
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
101
|
-
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function definitionInUseMessage(definition, blockedBy) {
|
|
106
|
-
if (blockedBy.reason === "non-terminal-instances") {
|
|
107
|
-
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
108
|
-
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.`;
|
|
109
|
-
}
|
|
110
|
-
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
111
|
-
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
class EffectNotFoundError extends WorkflowError {
|
|
115
|
-
instanceId;
|
|
116
|
-
effectKey;
|
|
117
|
-
settled;
|
|
118
|
-
constructor(args) {
|
|
119
|
-
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
120
|
-
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function effectNotFoundMessage(args) {
|
|
125
|
-
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
126
|
-
if (args.settled === void 0) return base;
|
|
127
|
-
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
128
|
-
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}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
58
|
const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
|
|
132
59
|
|
|
133
60
|
class VersionSpecificDatasetGdrError extends Error {
|
|
@@ -343,6 +270,79 @@ function isGdr(value) {
|
|
|
343
270
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
344
271
|
}
|
|
345
272
|
|
|
273
|
+
class WorkflowError extends Error {
|
|
274
|
+
kind;
|
|
275
|
+
constructor(kind, message, options) {
|
|
276
|
+
super(message, options), this.kind = kind;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class ContractViolationError extends WorkflowError {
|
|
281
|
+
constructor(message) {
|
|
282
|
+
super("contract-violation", message), this.name = "ContractViolationError";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
class InstanceNotFoundError extends WorkflowError {
|
|
287
|
+
instanceId;
|
|
288
|
+
constructor(args) {
|
|
289
|
+
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
290
|
+
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class DefinitionNotFoundError extends WorkflowError {
|
|
295
|
+
definition;
|
|
296
|
+
version;
|
|
297
|
+
constructor(args) {
|
|
298
|
+
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`),
|
|
299
|
+
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class SpawnContractsInvalidError extends WorkflowError {
|
|
304
|
+
issues;
|
|
305
|
+
constructor(args) {
|
|
306
|
+
super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
|
|
307
|
+
this.issues = args.issues;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class DefinitionInUseError extends WorkflowError {
|
|
312
|
+
definition;
|
|
313
|
+
blockedBy;
|
|
314
|
+
constructor(args) {
|
|
315
|
+
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
316
|
+
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function definitionInUseMessage(definition, blockedBy) {
|
|
321
|
+
if (blockedBy.reason === "non-terminal-instances") {
|
|
322
|
+
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
323
|
+
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.`;
|
|
324
|
+
}
|
|
325
|
+
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
326
|
+
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
class EffectNotFoundError extends WorkflowError {
|
|
330
|
+
instanceId;
|
|
331
|
+
effectKey;
|
|
332
|
+
settled;
|
|
333
|
+
constructor(args) {
|
|
334
|
+
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
335
|
+
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function effectNotFoundMessage(args) {
|
|
340
|
+
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
341
|
+
if (args.settled === void 0) return base;
|
|
342
|
+
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
343
|
+
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}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
346
|
const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
|
|
347
347
|
|
|
348
348
|
function validateTag(tag) {
|
|
@@ -539,6 +539,7 @@ function desugarWorkflow(authoring) {
|
|
|
539
539
|
return {
|
|
540
540
|
...stripUndefined({
|
|
541
541
|
name: stage.name,
|
|
542
|
+
semantics: stage.semantics,
|
|
542
543
|
title: stage.title,
|
|
543
544
|
description: stage.description,
|
|
544
545
|
groups: stage.groups,
|
|
@@ -562,6 +563,7 @@ function desugarWorkflow(authoring) {
|
|
|
562
563
|
definition: {
|
|
563
564
|
...stripUndefined({
|
|
564
565
|
name: authoring.name,
|
|
566
|
+
semantics: authoring.semantics,
|
|
565
567
|
title: authoring.title,
|
|
566
568
|
description: authoring.description,
|
|
567
569
|
groups: authoring.groups,
|
|
@@ -666,6 +668,7 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
|
|
|
666
668
|
options: entry.options,
|
|
667
669
|
validation: entry.validation,
|
|
668
670
|
types: entry.types,
|
|
671
|
+
roles: entry.roles,
|
|
669
672
|
fields: entry.fields,
|
|
670
673
|
of: entry.of
|
|
671
674
|
})
|
|
@@ -788,6 +791,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
788
791
|
return {
|
|
789
792
|
...stripUndefined({
|
|
790
793
|
name: activity.name,
|
|
794
|
+
semantics: activity.semantics,
|
|
791
795
|
title: activity.title,
|
|
792
796
|
description: activity.description,
|
|
793
797
|
groups: activity.groups,
|
|
@@ -1259,7 +1263,66 @@ function isTerminalActivityStatus(status) {
|
|
|
1259
1263
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1260
1264
|
}
|
|
1261
1265
|
|
|
1262
|
-
const
|
|
1266
|
+
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" ];
|
|
1267
|
+
|
|
1268
|
+
function releaseDocId(releaseName) {
|
|
1269
|
+
return `_.releases.${releaseName}`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1273
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1274
|
+
return {
|
|
1275
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1276
|
+
type: "system.release",
|
|
1277
|
+
releaseName: releaseName
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function isAlwaysArrayFieldKind(kind) {
|
|
1282
|
+
return kind === "doc.refs" || kind === "assignees" || kind === "array";
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function isSingleDocRefKind(kind) {
|
|
1286
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function refKindAcceptsTypes(kind) {
|
|
1290
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function assignmentKindAcceptsRoles(kind) {
|
|
1294
|
+
return kind === "assignee" || kind === "assignees";
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function isSingleDocRefEntry(entry) {
|
|
1298
|
+
return isSingleDocRefKind(entry._type);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function isAssignmentFieldEntry(entry) {
|
|
1302
|
+
return assignmentKindAcceptsRoles(entry._type);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function isTodoListItem(row) {
|
|
1306
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1307
|
+
const candidate = row, status = candidate.status;
|
|
1308
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function declaredRowColumns(entry) {
|
|
1312
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function isTodoListEntry(entry) {
|
|
1316
|
+
const columns = declaredRowColumns(entry);
|
|
1317
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function isNotesEntry(entry) {
|
|
1321
|
+
const columns = declaredRowColumns(entry);
|
|
1322
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const CONDITION_VARS = [ {
|
|
1263
1326
|
name: "self",
|
|
1264
1327
|
binding: "always",
|
|
1265
1328
|
label: "this workflow instance",
|
|
@@ -1334,6 +1397,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
|
|
|
1334
1397
|
binding: "caller",
|
|
1335
1398
|
label: "your permissions",
|
|
1336
1399
|
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."
|
|
1400
|
+
}, {
|
|
1401
|
+
name: "attributes",
|
|
1402
|
+
binding: "caller",
|
|
1403
|
+
label: "your attributes",
|
|
1404
|
+
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."
|
|
1337
1405
|
}, {
|
|
1338
1406
|
name: "row",
|
|
1339
1407
|
binding: "spawn",
|
|
@@ -1499,54 +1567,7 @@ function documentIdOf(doc) {
|
|
|
1499
1567
|
return "(unknown id)";
|
|
1500
1568
|
}
|
|
1501
1569
|
|
|
1502
|
-
const ACTOR_KINDS = [ "person", "agent", "system" ]
|
|
1503
|
-
|
|
1504
|
-
function releaseDocId(releaseName) {
|
|
1505
|
-
return `_.releases.${releaseName}`;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1509
|
-
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1510
|
-
return {
|
|
1511
|
-
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1512
|
-
type: "system.release",
|
|
1513
|
-
releaseName: releaseName
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function isSingleDocRefKind(kind) {
|
|
1518
|
-
return kind === "doc.ref" || kind === "subject";
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function refKindAcceptsTypes(kind) {
|
|
1522
|
-
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
function isSingleDocRefEntry(entry) {
|
|
1526
|
-
return isSingleDocRefKind(entry._type);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
function isTodoListItem(row) {
|
|
1530
|
-
if (typeof row != "object" || row === null) return !1;
|
|
1531
|
-
const candidate = row, status = candidate.status;
|
|
1532
|
-
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function declaredRowColumns(entry) {
|
|
1536
|
-
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
function isTodoListEntry(entry) {
|
|
1540
|
-
const columns = declaredRowColumns(entry);
|
|
1541
|
-
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
function isNotesEntry(entry) {
|
|
1545
|
-
const columns = declaredRowColumns(entry);
|
|
1546
|
-
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1570
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1550
1571
|
|
|
1551
1572
|
function classifyPrincipalId(id) {
|
|
1552
1573
|
if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
|
|
@@ -1589,6 +1610,10 @@ function lakePrincipalId(args) {
|
|
|
1589
1610
|
return args.localPrincipalId ?? args.actor.id;
|
|
1590
1611
|
}
|
|
1591
1612
|
|
|
1613
|
+
function isRecord(value) {
|
|
1614
|
+
return typeof value == "object" && value !== null && !Array.isArray(value);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1592
1617
|
class FieldValueShapeError extends WorkflowError {
|
|
1593
1618
|
entryType;
|
|
1594
1619
|
entryName;
|
|
@@ -1685,9 +1710,16 @@ const fieldValueSchemas = {
|
|
|
1685
1710
|
query: v__namespace.any()
|
|
1686
1711
|
};
|
|
1687
1712
|
|
|
1688
|
-
function shapeValueSchema(
|
|
1689
|
-
|
|
1690
|
-
if (shape.type === "
|
|
1713
|
+
function shapeValueSchema(args) {
|
|
1714
|
+
const {shape: shape, leaf: leaf} = args;
|
|
1715
|
+
if (shape.type === "object") return objectSchema({
|
|
1716
|
+
fields: shape.fields ?? [],
|
|
1717
|
+
leaf: leaf
|
|
1718
|
+
});
|
|
1719
|
+
if (shape.type === "array") return v__namespace.array(objectSchema({
|
|
1720
|
+
fields: shape.of ?? [],
|
|
1721
|
+
leaf: leaf
|
|
1722
|
+
}));
|
|
1691
1723
|
const schema = leaf[shape.type] ?? v__namespace.any();
|
|
1692
1724
|
return constrainedScalarSchema({
|
|
1693
1725
|
schema: schema,
|
|
@@ -1738,19 +1770,31 @@ function constrainedScalarSchema(args) {
|
|
|
1738
1770
|
}) ?? [] ].join("; ")));
|
|
1739
1771
|
}
|
|
1740
1772
|
|
|
1741
|
-
function objectSchema(
|
|
1742
|
-
const entries = /* @__PURE__ */ Object.create(null);
|
|
1743
|
-
for (const f of fields) entries[f.name] = v__namespace.optional(shapeValueSchema(
|
|
1773
|
+
function objectSchema(args) {
|
|
1774
|
+
const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
|
|
1775
|
+
for (const f of fields) entries[f.name] = v__namespace.optional(shapeValueSchema({
|
|
1776
|
+
shape: f,
|
|
1777
|
+
leaf: leaf
|
|
1778
|
+
}));
|
|
1744
1779
|
return v__namespace.looseObject(entries);
|
|
1745
1780
|
}
|
|
1746
1781
|
|
|
1747
1782
|
function wholeValueSchema(args) {
|
|
1748
1783
|
const {entryType: entryType, shape: shape, leaf: leaf} = args;
|
|
1749
|
-
return entryType === "object" ? v__namespace.union([ v__namespace.null(), objectSchema(
|
|
1784
|
+
return entryType === "object" ? v__namespace.union([ v__namespace.null(), objectSchema({
|
|
1785
|
+
fields: shape.fields ?? [],
|
|
1786
|
+
leaf: leaf
|
|
1787
|
+
}) ]) : entryType === "array" ? v__namespace.array(objectSchema({
|
|
1788
|
+
fields: shape.of ?? [],
|
|
1789
|
+
leaf: leaf
|
|
1790
|
+
})) : leaf[entryType];
|
|
1750
1791
|
}
|
|
1751
1792
|
|
|
1752
1793
|
function appendItemSchema(entryType, shape) {
|
|
1753
|
-
if (entryType === "array") return objectSchema(
|
|
1794
|
+
if (entryType === "array") return objectSchema({
|
|
1795
|
+
fields: shape.of ?? [],
|
|
1796
|
+
leaf: valueSchemas
|
|
1797
|
+
});
|
|
1754
1798
|
if (entryType === "doc.refs") return GdrShape;
|
|
1755
1799
|
if (entryType === "assignees") return AssigneeWriteShape;
|
|
1756
1800
|
}
|
|
@@ -1802,7 +1846,7 @@ function checkValueAgainst(args, leaf) {
|
|
|
1802
1846
|
entryType: args.entryType,
|
|
1803
1847
|
validation: args.validation,
|
|
1804
1848
|
value: args.value
|
|
1805
|
-
});
|
|
1849
|
+
}) ?? assignmentValueIssues(args, result.output);
|
|
1806
1850
|
return postIssues !== void 0 ? {
|
|
1807
1851
|
issues: postIssues
|
|
1808
1852
|
} : {
|
|
@@ -1810,6 +1854,142 @@ function checkValueAgainst(args, leaf) {
|
|
|
1810
1854
|
};
|
|
1811
1855
|
}
|
|
1812
1856
|
|
|
1857
|
+
function assignmentIdentity(assignee) {
|
|
1858
|
+
return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function assignmentCandidate(value) {
|
|
1862
|
+
const parsed = v__namespace.safeParse(AssigneeShape, value);
|
|
1863
|
+
return parsed.success ? parsed.output : void 0;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function assignmentValueIssues(args, value) {
|
|
1867
|
+
const retained = previousAssignmentIdentities(args), issues = [];
|
|
1868
|
+
return visitAssignmentEntries({
|
|
1869
|
+
entryType: args.entryType,
|
|
1870
|
+
value: value,
|
|
1871
|
+
roles: args.roles,
|
|
1872
|
+
fields: args.fields,
|
|
1873
|
+
of: args.of,
|
|
1874
|
+
path: "$"
|
|
1875
|
+
}, ({assignee: assignee, roles: roles, path: path}) => {
|
|
1876
|
+
consumeRetainedAssignment({
|
|
1877
|
+
retained: retained,
|
|
1878
|
+
path: path,
|
|
1879
|
+
identity: assignmentIdentity(assignee)
|
|
1880
|
+
}) || issues.push(...assignmentCandidateIssues({
|
|
1881
|
+
roles: roles,
|
|
1882
|
+
memberRoles: args.memberRoles,
|
|
1883
|
+
roleAliases: args.roleAliases
|
|
1884
|
+
}, assignee));
|
|
1885
|
+
}), issues.length === 0 ? void 0 : issues;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function consumeRetainedAssignment(args) {
|
|
1889
|
+
const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
|
|
1890
|
+
return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
|
|
1891
|
+
!0);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function previousAssignmentIdentities(args) {
|
|
1895
|
+
const retained = /* @__PURE__ */ new Map;
|
|
1896
|
+
if (args.previousValue === void 0 || args.entryType === "assignee") return retained;
|
|
1897
|
+
const schema = wholeValueSchema({
|
|
1898
|
+
entryType: args.entryType,
|
|
1899
|
+
shape: args,
|
|
1900
|
+
leaf: valueSchemas
|
|
1901
|
+
});
|
|
1902
|
+
if (schema === void 0) return retained;
|
|
1903
|
+
const previous = v__namespace.safeParse(schema, args.previousValue);
|
|
1904
|
+
return previous.success && visitAssignmentEntries({
|
|
1905
|
+
entryType: args.entryType,
|
|
1906
|
+
value: previous.output,
|
|
1907
|
+
roles: args.roles,
|
|
1908
|
+
fields: args.fields,
|
|
1909
|
+
of: args.of,
|
|
1910
|
+
path: "$"
|
|
1911
|
+
}, ({assignee: assignee, path: path}) => {
|
|
1912
|
+
const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
|
|
1913
|
+
identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
|
|
1914
|
+
}), retained;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function visitAssignmentEntries(args, visit) {
|
|
1918
|
+
if (assignmentKindAcceptsRoles(args.entryType)) {
|
|
1919
|
+
visitAssignmentCandidates(args, visit);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (args.entryType === "object") {
|
|
1923
|
+
visitAssignmentFields({
|
|
1924
|
+
fields: args.fields,
|
|
1925
|
+
value: args.value,
|
|
1926
|
+
path: args.path
|
|
1927
|
+
}, visit);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
|
|
1931
|
+
fields: args.of,
|
|
1932
|
+
value: row,
|
|
1933
|
+
path: `${args.path}[]`
|
|
1934
|
+
}, visit);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function visitAssignmentCandidates(args, visit) {
|
|
1938
|
+
if (args.roles === void 0) return;
|
|
1939
|
+
const indexedValues = args.entryType === "assignees" && Array.isArray(args.value) ? args.value.map(value => ({
|
|
1940
|
+
value: value,
|
|
1941
|
+
path: args.path
|
|
1942
|
+
})) : [ {
|
|
1943
|
+
value: args.value,
|
|
1944
|
+
path: args.path
|
|
1945
|
+
} ];
|
|
1946
|
+
for (const {value: value, path: path} of indexedValues) {
|
|
1947
|
+
const assignee = assignmentCandidate(value);
|
|
1948
|
+
assignee !== void 0 && visit({
|
|
1949
|
+
assignee: assignee,
|
|
1950
|
+
roles: args.roles,
|
|
1951
|
+
path: path
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function visitAssignmentFields(args, visit) {
|
|
1957
|
+
if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
|
|
1958
|
+
shape: field,
|
|
1959
|
+
value: args.value[field.name],
|
|
1960
|
+
path: `${args.path}.${field.name}`
|
|
1961
|
+
}, visit);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function visitAssignmentShape(args, visit) {
|
|
1965
|
+
visitAssignmentEntries({
|
|
1966
|
+
entryType: args.shape.type,
|
|
1967
|
+
value: args.value,
|
|
1968
|
+
roles: args.shape.roles,
|
|
1969
|
+
fields: args.shape.fields,
|
|
1970
|
+
of: args.shape.of,
|
|
1971
|
+
path: args.path
|
|
1972
|
+
}, visit);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function assignmentCandidateIssues(args, assignee) {
|
|
1976
|
+
if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
|
|
1977
|
+
const held = args.memberRoles?.[assignee.id];
|
|
1978
|
+
return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
|
|
1979
|
+
actorRoles: held,
|
|
1980
|
+
required: required,
|
|
1981
|
+
aliases: args.roleAliases
|
|
1982
|
+
})) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function collectiveRoleIssues(args, role) {
|
|
1986
|
+
return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function formatEligibleRoles(roles) {
|
|
1990
|
+
return roles.map(role => `"${role}"`).join(", ");
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1813
1993
|
function issuesOf(check) {
|
|
1814
1994
|
return "issues" in check ? check.issues : void 0;
|
|
1815
1995
|
}
|
|
@@ -1869,6 +2049,20 @@ function validateFieldAppendItem(args) {
|
|
|
1869
2049
|
issues: formatIssues(result.issues),
|
|
1870
2050
|
mode: "item"
|
|
1871
2051
|
});
|
|
2052
|
+
const assignmentArgs = {
|
|
2053
|
+
entryType: appendAssignmentEntryType(args.entryType),
|
|
2054
|
+
value: result.output,
|
|
2055
|
+
roles: args.roles,
|
|
2056
|
+
fields: args.entryType === "array" ? args.of : void 0,
|
|
2057
|
+
memberRoles: args.memberRoles,
|
|
2058
|
+
roleAliases: args.roleAliases
|
|
2059
|
+
}, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
|
|
2060
|
+
if (assignmentIssues !== void 0) throw new FieldValueShapeError({
|
|
2061
|
+
entryType: args.entryType,
|
|
2062
|
+
entryName: args.entryName,
|
|
2063
|
+
issues: assignmentIssues,
|
|
2064
|
+
mode: "item"
|
|
2065
|
+
});
|
|
1872
2066
|
const typeIssues = refTypeIssues({
|
|
1873
2067
|
entryType: args.entryType,
|
|
1874
2068
|
types: args.types,
|
|
@@ -1883,6 +2077,10 @@ function validateFieldAppendItem(args) {
|
|
|
1883
2077
|
return result.output;
|
|
1884
2078
|
}
|
|
1885
2079
|
|
|
2080
|
+
function appendAssignmentEntryType(entryType) {
|
|
2081
|
+
return entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1886
2084
|
function formatIssues(issues, formatMessage = issue => issue.message) {
|
|
1887
2085
|
const formatOne = (issue, prefix) => {
|
|
1888
2086
|
const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
|
|
@@ -1975,6 +2173,10 @@ function opSchemas(targetSchema) {
|
|
|
1975
2173
|
type: v__namespace.literal("field.set"),
|
|
1976
2174
|
target: targetSchema,
|
|
1977
2175
|
value: ValueExprSchema
|
|
2176
|
+
}), v__namespace.strictObject({
|
|
2177
|
+
type: v__namespace.literal("field.setIfMissing"),
|
|
2178
|
+
target: targetSchema,
|
|
2179
|
+
value: ValueExprSchema
|
|
1978
2180
|
}), v__namespace.strictObject({
|
|
1979
2181
|
type: v__namespace.literal("field.unset"),
|
|
1980
2182
|
target: targetSchema
|
|
@@ -1982,6 +2184,14 @@ function opSchemas(targetSchema) {
|
|
|
1982
2184
|
type: v__namespace.literal("field.append"),
|
|
1983
2185
|
target: targetSchema,
|
|
1984
2186
|
value: ValueExprSchema
|
|
2187
|
+
}), v__namespace.strictObject({
|
|
2188
|
+
type: v__namespace.literal("field.inc"),
|
|
2189
|
+
target: targetSchema,
|
|
2190
|
+
value: v__namespace.optional(ValueExprSchema)
|
|
2191
|
+
}), v__namespace.strictObject({
|
|
2192
|
+
type: v__namespace.literal("field.dec"),
|
|
2193
|
+
target: targetSchema,
|
|
2194
|
+
value: v__namespace.optional(ValueExprSchema)
|
|
1985
2195
|
}), v__namespace.strictObject({
|
|
1986
2196
|
type: v__namespace.literal("field.updateWhere"),
|
|
1987
2197
|
target: targetSchema,
|
|
@@ -2063,16 +2273,17 @@ function compositeChecked(entries) {
|
|
|
2063
2273
|
return v__namespace.pipe(v__namespace.strictObject(entries), v__namespace.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v__namespace.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
|
|
2064
2274
|
}
|
|
2065
2275
|
|
|
2066
|
-
const FieldShapeSchema = v__namespace.lazy(() => v__namespace.pipe(compositeChecked({
|
|
2276
|
+
const AssignmentRolesSchema = v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v__namespace.lazy(() => v__namespace.pipe(compositeChecked({
|
|
2067
2277
|
type: FieldValueKindSchema,
|
|
2068
2278
|
name: FieldEntryName,
|
|
2069
2279
|
title: v__namespace.optional(v__namespace.string()),
|
|
2070
2280
|
description: v__namespace.optional(v__namespace.string()),
|
|
2071
2281
|
options: v__namespace.optional(ChoiceOptionsSchema),
|
|
2072
2282
|
validation: v__namespace.optional(ScalarValidationSchema),
|
|
2283
|
+
roles: v__namespace.optional(AssignmentRolesSchema),
|
|
2073
2284
|
fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
|
|
2074
2285
|
of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
|
|
2075
|
-
}), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
|
|
2286
|
+
}), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
|
|
2076
2287
|
|
|
2077
2288
|
function fieldBase(editable, group) {
|
|
2078
2289
|
return {
|
|
@@ -2093,6 +2304,7 @@ function fieldEntryFields({editable: editable, group: group, kind: kind = FieldK
|
|
|
2093
2304
|
options: v__namespace.optional(ChoiceOptionsSchema),
|
|
2094
2305
|
validation: v__namespace.optional(ScalarValidationSchema),
|
|
2095
2306
|
types: v__namespace.optional(v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
|
|
2307
|
+
roles: v__namespace.optional(AssignmentRolesSchema),
|
|
2096
2308
|
fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
|
|
2097
2309
|
of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
|
|
2098
2310
|
};
|
|
@@ -2106,7 +2318,8 @@ function literalSeedIssues(entry) {
|
|
|
2106
2318
|
fields: entry.fields,
|
|
2107
2319
|
of: entry.of,
|
|
2108
2320
|
options: entry.options,
|
|
2109
|
-
validation: entry.validation
|
|
2321
|
+
validation: entry.validation,
|
|
2322
|
+
roles: entry.roles
|
|
2110
2323
|
});
|
|
2111
2324
|
}
|
|
2112
2325
|
|
|
@@ -2118,6 +2331,10 @@ function refTypesCheck() {
|
|
|
2118
2331
|
return v__namespace.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}"`);
|
|
2119
2332
|
}
|
|
2120
2333
|
|
|
2334
|
+
function assignmentRolesCheck() {
|
|
2335
|
+
return v__namespace.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2121
2338
|
function choiceOptionsCheck() {
|
|
2122
2339
|
return v__namespace.check(entry => checkChoiceList({
|
|
2123
2340
|
entryType: entry.type,
|
|
@@ -2152,11 +2369,11 @@ function scalarValidationDeclarationIssues(entry) {
|
|
|
2152
2369
|
const FieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
|
|
2153
2370
|
editable: StoredEditableSchema,
|
|
2154
2371
|
group: StoredGroupMembershipSchema
|
|
2155
|
-
})), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
|
|
2372
|
+
})), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
|
|
2156
2373
|
editable: AuthoringEditableSchema,
|
|
2157
2374
|
group: AuthoringGroupMembershipSchema,
|
|
2158
2375
|
kind: AuthoringRawFieldKindSchema
|
|
2159
|
-
})), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
|
|
2376
|
+
})), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
|
|
2160
2377
|
type: v__namespace.literal("claim"),
|
|
2161
2378
|
name: FieldEntryName,
|
|
2162
2379
|
title: v__namespace.optional(v__namespace.string()),
|
|
@@ -2198,17 +2415,27 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
|
|
|
2198
2415
|
required: v__namespace.optional(v__namespace.boolean()),
|
|
2199
2416
|
options: v__namespace.optional(ChoiceOptionsSchema),
|
|
2200
2417
|
validation: v__namespace.optional(ScalarValidationSchema)
|
|
2201
|
-
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2418
|
+
}), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v__namespace.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v__namespace.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v__namespace.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
|
|
2419
|
+
|
|
2420
|
+
function semanticNamespace(semantic) {
|
|
2421
|
+
return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
|
|
2422
|
+
}
|
|
2202
2423
|
|
|
2203
2424
|
function hasUniqueSemanticNamespaces(semantics) {
|
|
2204
|
-
const namespaces = semantics.map(
|
|
2425
|
+
const namespaces = semantics.map(semanticNamespace);
|
|
2205
2426
|
return new Set(namespaces).size === namespaces.length;
|
|
2206
2427
|
}
|
|
2207
2428
|
|
|
2429
|
+
function semanticsFieldSchema(semantic) {
|
|
2430
|
+
return v__namespace.optional(v__namespace.pipe(v__namespace.array(semantic), v__namespace.minLength(1, "declare at least one semantic, or omit `semantics`"), v__namespace.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
|
|
2434
|
+
|
|
2208
2435
|
function actionFields(op, group) {
|
|
2209
2436
|
return {
|
|
2210
2437
|
name: NonEmpty,
|
|
2211
|
-
semantics:
|
|
2438
|
+
semantics: ActionSemanticsFieldSchema,
|
|
2212
2439
|
title: v__namespace.optional(v__namespace.string()),
|
|
2213
2440
|
description: v__namespace.optional(v__namespace.string()),
|
|
2214
2441
|
group: v__namespace.optional(group),
|
|
@@ -2255,6 +2482,7 @@ const StoredActionSchema = pinned()(v__namespace.strictObject({
|
|
|
2255
2482
|
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
2256
2483
|
return {
|
|
2257
2484
|
name: NonEmpty,
|
|
2485
|
+
semantics: SemanticsFieldSchema,
|
|
2258
2486
|
title: v__namespace.optional(v__namespace.string()),
|
|
2259
2487
|
description: v__namespace.optional(v__namespace.string()),
|
|
2260
2488
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2328,6 +2556,7 @@ const GuardSchema = v__namespace.strictObject(guardFields(NonEmpty)), AuthoringG
|
|
|
2328
2556
|
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
2329
2557
|
return {
|
|
2330
2558
|
name: NonEmpty,
|
|
2559
|
+
semantics: SemanticsFieldSchema,
|
|
2331
2560
|
title: v__namespace.optional(v__namespace.string()),
|
|
2332
2561
|
description: v__namespace.optional(v__namespace.string()),
|
|
2333
2562
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2366,6 +2595,7 @@ const StoredStartSchema = pinned()(v__namespace.strictObject(startFields(picklis
|
|
|
2366
2595
|
function workflowFields({field: field, stage: stage, start: start}) {
|
|
2367
2596
|
return {
|
|
2368
2597
|
name: NonEmpty,
|
|
2598
|
+
semantics: SemanticsFieldSchema,
|
|
2369
2599
|
title: NonEmpty,
|
|
2370
2600
|
description: v__namespace.optional(v__namespace.string()),
|
|
2371
2601
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2562,11 +2792,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
2562
2792
|
});
|
|
2563
2793
|
}
|
|
2564
2794
|
|
|
2795
|
+
function actionSites(def) {
|
|
2796
|
+
return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
|
|
2797
|
+
action: action,
|
|
2798
|
+
activity: activity,
|
|
2799
|
+
stage: stage,
|
|
2800
|
+
path: [ "stages", i, "activities", j, "actions", a ]
|
|
2801
|
+
}))));
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2565
2804
|
function effectNameSites(def) {
|
|
2566
2805
|
const sites = [];
|
|
2567
|
-
for (const
|
|
2806
|
+
for (const {action: action, path: path} of actionSites(def)) collectEffects({
|
|
2568
2807
|
effects: action.effects,
|
|
2569
|
-
path: [
|
|
2808
|
+
path: [ ...path, "effects" ],
|
|
2570
2809
|
sites: sites
|
|
2571
2810
|
});
|
|
2572
2811
|
return sites;
|
|
@@ -2697,13 +2936,22 @@ function checkUnboundConditionVars(def, issues) {
|
|
|
2697
2936
|
}
|
|
2698
2937
|
}
|
|
2699
2938
|
|
|
2939
|
+
const SOFT_GATE_VARS = {
|
|
2940
|
+
can: {
|
|
2941
|
+
noun: "the caller's grants"
|
|
2942
|
+
},
|
|
2943
|
+
attributes: {
|
|
2944
|
+
noun: "the caller's org-level user attributes"
|
|
2945
|
+
}
|
|
2946
|
+
}, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
|
|
2947
|
+
|
|
2700
2948
|
function unboundVarsAt(site) {
|
|
2701
2949
|
const callerVars = unboundCallerVars(site.policy);
|
|
2702
2950
|
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2703
2951
|
}
|
|
2704
2952
|
|
|
2705
2953
|
function unboundCallerVars(policy) {
|
|
2706
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [
|
|
2954
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
|
|
2707
2955
|
}
|
|
2708
2956
|
|
|
2709
2957
|
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";
|
|
@@ -2713,7 +2961,12 @@ function rowVarMessage(site) {
|
|
|
2713
2961
|
}
|
|
2714
2962
|
|
|
2715
2963
|
function callerVarMessage(site, name) {
|
|
2716
|
-
|
|
2964
|
+
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\``;
|
|
2965
|
+
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`;
|
|
2966
|
+
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`;
|
|
2967
|
+
const softGate = SOFT_GATE_VARS[name];
|
|
2968
|
+
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}`;
|
|
2969
|
+
throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
|
|
2717
2970
|
}
|
|
2718
2971
|
|
|
2719
2972
|
function conditionSites(def) {
|
|
@@ -2968,9 +3221,9 @@ function belowScopeNestedSites(args) {
|
|
|
2968
3221
|
}
|
|
2969
3222
|
|
|
2970
3223
|
function checkLevelKindEffectOutputs(def, issues) {
|
|
2971
|
-
for (const
|
|
3224
|
+
for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
|
|
2972
3225
|
action: action,
|
|
2973
|
-
path:
|
|
3226
|
+
path: path,
|
|
2974
3227
|
issues: issues
|
|
2975
3228
|
});
|
|
2976
3229
|
}
|
|
@@ -3052,18 +3305,18 @@ function storedRolesIssue(action) {
|
|
|
3052
3305
|
}
|
|
3053
3306
|
|
|
3054
3307
|
function checkStoredRolesPlacement(def, issues) {
|
|
3055
|
-
for (const
|
|
3308
|
+
for (const {action: action, path: path} of actionSites(def)) {
|
|
3056
3309
|
const message = storedRolesIssue(action);
|
|
3057
3310
|
message !== void 0 && issues.push({
|
|
3058
|
-
path: [
|
|
3311
|
+
path: [ ...path, "roles" ],
|
|
3059
3312
|
message: message
|
|
3060
3313
|
});
|
|
3061
3314
|
}
|
|
3062
3315
|
}
|
|
3063
3316
|
|
|
3064
3317
|
function checkTriggeredActionParams(def, issues) {
|
|
3065
|
-
for (const
|
|
3066
|
-
path: [
|
|
3318
|
+
for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
3319
|
+
path: [ ...path, "params" ],
|
|
3067
3320
|
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`
|
|
3068
3321
|
});
|
|
3069
3322
|
}
|
|
@@ -3108,13 +3361,20 @@ function checkSingleSubjectRequirements(def, issues) {
|
|
|
3108
3361
|
|
|
3109
3362
|
function checkStartFilterReads(def, issues) {
|
|
3110
3363
|
const filter = def.start?.filter;
|
|
3111
|
-
filter
|
|
3364
|
+
if (filter === void 0) return;
|
|
3365
|
+
const params = conditionParameterNames(filter);
|
|
3366
|
+
params.has("fields") && issues.push({
|
|
3112
3367
|
path: [ "start", "filter" ],
|
|
3113
3368
|
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"
|
|
3114
|
-
})
|
|
3369
|
+
});
|
|
3370
|
+
for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
|
|
3371
|
+
path: [ "start", "filter" ],
|
|
3372
|
+
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`
|
|
3373
|
+
});
|
|
3374
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
3115
3375
|
path: [ "start", "filter" ],
|
|
3116
3376
|
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"
|
|
3117
|
-
})
|
|
3377
|
+
});
|
|
3118
3378
|
}
|
|
3119
3379
|
|
|
3120
3380
|
function checkStartRequirementReads(def, issues) {
|
|
@@ -3410,15 +3670,13 @@ function seedEarlierSiblingTarget(args) {
|
|
|
3410
3670
|
}
|
|
3411
3671
|
|
|
3412
3672
|
function opSites(def) {
|
|
3413
|
-
|
|
3414
|
-
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({
|
|
3673
|
+
return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
|
|
3415
3674
|
ops: action.ops,
|
|
3416
|
-
path: [
|
|
3675
|
+
path: [ ...path, "ops" ],
|
|
3417
3676
|
label: `action "${action.name}"`,
|
|
3418
3677
|
stage: stage,
|
|
3419
3678
|
activity: activity
|
|
3420
|
-
});
|
|
3421
|
-
return sites;
|
|
3679
|
+
}));
|
|
3422
3680
|
}
|
|
3423
3681
|
|
|
3424
3682
|
function checkFieldReadOpValues(def, issues) {
|
|
@@ -3435,7 +3693,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
3435
3693
|
}
|
|
3436
3694
|
|
|
3437
3695
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
3438
|
-
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({
|
|
3696
|
+
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({
|
|
3439
3697
|
...ctx,
|
|
3440
3698
|
read: read,
|
|
3441
3699
|
path: readPath,
|
|
@@ -3486,35 +3744,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
|
3486
3744
|
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)"}`;
|
|
3487
3745
|
}
|
|
3488
3746
|
|
|
3489
|
-
function
|
|
3747
|
+
function checkFieldTargetOps(def, issues) {
|
|
3490
3748
|
const workflow = def.fields ?? [];
|
|
3491
3749
|
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
workflow: workflow,
|
|
3495
|
-
stage: site.stage.fields ?? [],
|
|
3496
|
-
activity: site.activity.fields ?? []
|
|
3497
|
-
};
|
|
3498
|
-
checkUpdateWhereTargetKind({
|
|
3750
|
+
const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
|
|
3751
|
+
rule !== void 0 && "target" in op && checkTargetKind({
|
|
3499
3752
|
op: op,
|
|
3500
|
-
path: [ ...
|
|
3753
|
+
path: [ ...path, "target" ],
|
|
3501
3754
|
label: site.label,
|
|
3502
|
-
scopes:
|
|
3755
|
+
scopes: opTargetScopes(workflow, site),
|
|
3756
|
+
rule: rule,
|
|
3503
3757
|
issues: issues
|
|
3504
|
-
}), checkUpdateWhereMergeKeys({
|
|
3758
|
+
}), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
|
|
3505
3759
|
op: op,
|
|
3506
|
-
path:
|
|
3760
|
+
path: path,
|
|
3507
3761
|
label: site.label,
|
|
3508
3762
|
issues: issues
|
|
3509
3763
|
});
|
|
3510
3764
|
}
|
|
3511
3765
|
}
|
|
3512
3766
|
|
|
3513
|
-
|
|
3767
|
+
const arithmeticTargetRule = {
|
|
3768
|
+
accepts: target => target.type === "number",
|
|
3769
|
+
issue: () => "arithmetic ops target `number` entries only"
|
|
3770
|
+
}, TARGET_KIND_RULES = {
|
|
3771
|
+
"field.inc": arithmeticTargetRule,
|
|
3772
|
+
"field.dec": arithmeticTargetRule,
|
|
3773
|
+
"field.setIfMissing": {
|
|
3774
|
+
accepts: target => !isAlwaysArrayFieldKind(target.type),
|
|
3775
|
+
issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
|
|
3776
|
+
},
|
|
3777
|
+
"field.updateWhere": {
|
|
3778
|
+
accepts: target => target.type === "array",
|
|
3779
|
+
issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
|
|
3783
|
+
function opTargetScopes(workflow, site) {
|
|
3784
|
+
return {
|
|
3785
|
+
workflow: workflow,
|
|
3786
|
+
stage: site.stage.fields ?? [],
|
|
3787
|
+
activity: site.activity.fields ?? []
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
|
|
3514
3792
|
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3515
|
-
target === void 0 || target
|
|
3516
|
-
path:
|
|
3517
|
-
message: `${label}
|
|
3793
|
+
target === void 0 || rule.accepts(target) || issues.push({
|
|
3794
|
+
path: path,
|
|
3795
|
+
message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
|
|
3518
3796
|
});
|
|
3519
3797
|
}
|
|
3520
3798
|
|
|
@@ -3746,7 +4024,7 @@ function checkWorkflowInvariants(def) {
|
|
|
3746
4024
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
3747
4025
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3748
4026
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3749
|
-
checkFieldReadOpValues(def, issues),
|
|
4027
|
+
checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3750
4028
|
checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
|
|
3751
4029
|
checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
|
|
3752
4030
|
checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
|
|
@@ -3787,6 +4065,8 @@ exports.CONDITION_VARS = CONDITION_VARS;
|
|
|
3787
4065
|
|
|
3788
4066
|
exports.ContractViolationError = ContractViolationError;
|
|
3789
4067
|
|
|
4068
|
+
exports.DECISION_SEMANTICS = DECISION_SEMANTICS;
|
|
4069
|
+
|
|
3790
4070
|
exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
|
|
3791
4071
|
|
|
3792
4072
|
exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
|
|
@@ -3837,6 +4117,8 @@ exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
|
|
|
3837
4117
|
|
|
3838
4118
|
exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
|
|
3839
4119
|
|
|
4120
|
+
exports.SIGNAL_SEMANTICS = SIGNAL_SEMANTICS;
|
|
4121
|
+
|
|
3840
4122
|
exports.START_FILTER_VARS = START_FILTER_VARS;
|
|
3841
4123
|
|
|
3842
4124
|
exports.START_REQUIREMENT_VARS = START_REQUIREMENT_VARS;
|
|
@@ -3859,6 +4141,8 @@ exports.actorFulfillsRole = actorFulfillsRole;
|
|
|
3859
4141
|
|
|
3860
4142
|
exports.andConditions = andConditions;
|
|
3861
4143
|
|
|
4144
|
+
exports.assignmentKindAcceptsRoles = assignmentKindAcceptsRoles;
|
|
4145
|
+
|
|
3862
4146
|
exports.checkWorkflowInvariants = checkWorkflowInvariants;
|
|
3863
4147
|
|
|
3864
4148
|
exports.choiceValueIssues = choiceValueIssues;
|
|
@@ -3921,6 +4205,10 @@ exports.groq = groq;
|
|
|
3921
4205
|
|
|
3922
4206
|
exports.groupMembershipNames = groupMembershipNames;
|
|
3923
4207
|
|
|
4208
|
+
exports.isAlwaysArrayFieldKind = isAlwaysArrayFieldKind;
|
|
4209
|
+
|
|
4210
|
+
exports.isAssignmentFieldEntry = isAssignmentFieldEntry;
|
|
4211
|
+
|
|
3924
4212
|
exports.isBareSeedId = isBareSeedId;
|
|
3925
4213
|
|
|
3926
4214
|
exports.isCascadeFired = isCascadeFired;
|
|
@@ -3937,6 +4225,8 @@ exports.isNotesEntry = isNotesEntry;
|
|
|
3937
4225
|
|
|
3938
4226
|
exports.isParseableInstant = isParseableInstant;
|
|
3939
4227
|
|
|
4228
|
+
exports.isRecord = isRecord;
|
|
4229
|
+
|
|
3940
4230
|
exports.isSingleDocRefEntry = isSingleDocRefEntry;
|
|
3941
4231
|
|
|
3942
4232
|
exports.isSingleDocRefKind = isSingleDocRefKind;
|