@sanity/workflow-engine 0.17.0 → 0.19.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 +53 -0
- package/DATAMODEL.md +193 -13
- package/README.md +23 -0
- package/dist/_chunks-cjs/invariants.cjs +696 -122
- package/dist/_chunks-es/invariants.js +643 -123
- package/dist/define.cjs +6 -1
- package/dist/define.d.cts +397 -40
- package/dist/define.d.ts +397 -40
- package/dist/define.js +7 -2
- package/dist/index.cjs +3089 -2159
- package/dist/index.d.cts +1199 -205
- package/dist/index.d.ts +1199 -205
- package/dist/index.js +3012 -2132
- package/package.json +1 -1
|
@@ -4,6 +4,317 @@ import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as eva
|
|
|
4
4
|
|
|
5
5
|
import { parse } from "groq-js";
|
|
6
6
|
|
|
7
|
+
class WorkflowError extends Error {
|
|
8
|
+
kind;
|
|
9
|
+
constructor(kind, message, options) {
|
|
10
|
+
super(message, options), this.kind = kind;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ContractViolationError extends WorkflowError {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super("contract-violation", message), this.name = "ContractViolationError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class InstanceNotFoundError extends WorkflowError {
|
|
21
|
+
instanceId;
|
|
22
|
+
constructor(args) {
|
|
23
|
+
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
24
|
+
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class DefinitionNotFoundError extends WorkflowError {
|
|
29
|
+
definition;
|
|
30
|
+
version;
|
|
31
|
+
constructor(args) {
|
|
32
|
+
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`),
|
|
33
|
+
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SpawnContractsInvalidError extends WorkflowError {
|
|
38
|
+
issues;
|
|
39
|
+
constructor(args) {
|
|
40
|
+
super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
|
|
41
|
+
this.issues = args.issues;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class DefinitionInUseError extends WorkflowError {
|
|
46
|
+
definition;
|
|
47
|
+
blockedBy;
|
|
48
|
+
constructor(args) {
|
|
49
|
+
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
50
|
+
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function definitionInUseMessage(definition, blockedBy) {
|
|
55
|
+
if (blockedBy.reason === "non-terminal-instances") {
|
|
56
|
+
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
57
|
+
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.`;
|
|
58
|
+
}
|
|
59
|
+
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
60
|
+
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class EffectNotFoundError extends WorkflowError {
|
|
64
|
+
instanceId;
|
|
65
|
+
effectKey;
|
|
66
|
+
settled;
|
|
67
|
+
constructor(args) {
|
|
68
|
+
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
69
|
+
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function effectNotFoundMessage(args) {
|
|
74
|
+
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
75
|
+
if (args.settled === void 0) return base;
|
|
76
|
+
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
77
|
+
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}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function errorMessage(err) {
|
|
81
|
+
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rethrowWithContext(err, context) {
|
|
85
|
+
throw new Error(`${context}: ${errorMessage(err)}`, {
|
|
86
|
+
cause: err
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
|
|
91
|
+
|
|
92
|
+
function terminalState(instance) {
|
|
93
|
+
return instance.abortedAt !== void 0 ? "aborted" : instance.completedAt !== void 0 ? "completed" : "in-flight";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isUnprimed(instance) {
|
|
97
|
+
return instance.stages.length === 0 && terminalState(instance) === "in-flight";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseDefinitionSnapshotValue(instance) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(instance.definitionSnapshot);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseDefinitionSnapshot(instance) {
|
|
109
|
+
return parseDefinitionSnapshotValue(instance);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parentRef(instance) {
|
|
113
|
+
return instance.ancestors.at(-1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
|
|
117
|
+
|
|
118
|
+
class ReaderModelAcknowledgementError extends WorkflowError {
|
|
119
|
+
code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
|
|
120
|
+
expectedMinReaderModel;
|
|
121
|
+
engineMinReaderModel=DATA_MODEL_MIN_READER;
|
|
122
|
+
engineModelVersion=DATA_MODEL_VERSION;
|
|
123
|
+
documentationUrl=READER_MODEL_ROLLOUT_URL;
|
|
124
|
+
constructor(expectedMinReaderModel, context = "Deployment") {
|
|
125
|
+
const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
|
|
126
|
+
super("reader-model-acknowledgement", `${context} expected reader floor ${expected}; the installed engine requires acknowledgement ${DATA_MODEL_MIN_READER}. Dependency upgrades can change writer compatibility. Upgrade all readers and Functions before accepting a higher floor; after verifying the rollout, change the literal in deployment configuration. ${READER_MODEL_ROLLOUT_URL}`),
|
|
127
|
+
this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertReaderModelAcknowledgement(expectedMinReaderModel, context) {
|
|
132
|
+
if (typeof expectedMinReaderModel != "number" || !Number.isFinite(expectedMinReaderModel) || !Number.isInteger(expectedMinReaderModel) || expectedMinReaderModel < 0 || expectedMinReaderModel !== DATA_MODEL_MIN_READER) throw new ReaderModelAcknowledgementError(expectedMinReaderModel, context);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
|
|
136
|
+
id: "governed-model-stamps",
|
|
137
|
+
introducedInModel: 1,
|
|
138
|
+
minReaderModel: 0,
|
|
139
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
140
|
+
compatibility: "additive",
|
|
141
|
+
applicability: "unconditional",
|
|
142
|
+
summary: "Definition and instance documents carry model provenance and reader-floor stamps."
|
|
143
|
+
}), Object.freeze({
|
|
144
|
+
id: "subject-field-kind",
|
|
145
|
+
introducedInModel: 2,
|
|
146
|
+
minReaderModel: 0,
|
|
147
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
148
|
+
compatibility: "additive",
|
|
149
|
+
applicability: "detectable",
|
|
150
|
+
summary: "A workflow-level subject field identifies the document a workflow is about."
|
|
151
|
+
}), Object.freeze({
|
|
152
|
+
id: "typed-scalar-choice-lists",
|
|
153
|
+
introducedInModel: 2,
|
|
154
|
+
minReaderModel: 2,
|
|
155
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
156
|
+
compatibility: "reader-floor",
|
|
157
|
+
applicability: "detectable",
|
|
158
|
+
summary: "Scalar fields may constrain writes to a persisted typed choice list."
|
|
159
|
+
}), Object.freeze({
|
|
160
|
+
id: "action-semantics",
|
|
161
|
+
introducedInModel: 2,
|
|
162
|
+
minReaderModel: 0,
|
|
163
|
+
documentTypes: Object.freeze([ "definition" ]),
|
|
164
|
+
compatibility: "additive",
|
|
165
|
+
applicability: "detectable",
|
|
166
|
+
summary: "Ordinary actions may carry a closed bag of advisory workflow semantics."
|
|
167
|
+
}), Object.freeze({
|
|
168
|
+
id: "inclusive-scalar-bounds",
|
|
169
|
+
introducedInModel: 2,
|
|
170
|
+
minReaderModel: 2,
|
|
171
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
172
|
+
compatibility: "reader-floor",
|
|
173
|
+
applicability: "detectable",
|
|
174
|
+
summary: "String, text, and number values may carry persisted inclusive bounds."
|
|
175
|
+
}), Object.freeze({
|
|
176
|
+
id: "progress-field-kind",
|
|
177
|
+
introducedInModel: 3,
|
|
178
|
+
minReaderModel: 0,
|
|
179
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
180
|
+
compatibility: "additive",
|
|
181
|
+
applicability: "detectable",
|
|
182
|
+
summary: "A progress field kind carries application-defined 0–100 completion."
|
|
183
|
+
}), Object.freeze({
|
|
184
|
+
id: "effect-claim-tokens",
|
|
185
|
+
introducedInModel: 3,
|
|
186
|
+
minReaderModel: 0,
|
|
187
|
+
documentTypes: Object.freeze([ "instance" ]),
|
|
188
|
+
compatibility: "additive",
|
|
189
|
+
applicability: "detectable",
|
|
190
|
+
summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
|
|
191
|
+
}) ]);
|
|
192
|
+
|
|
193
|
+
function recordOf(value) {
|
|
194
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) ? value : void 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recordsAt(record, key) {
|
|
198
|
+
const value = record[key];
|
|
199
|
+
return Array.isArray(value) ? value.map(recordOf).filter(item => item !== void 0) : [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function nestedFieldEntries(entries) {
|
|
203
|
+
return entries.flatMap(entry => [ entry, ...nestedFieldEntries(recordsAt(entry, "fields")), ...nestedFieldEntries(recordsAt(entry, "of")) ]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parsedDefinitionSnapshot(root) {
|
|
207
|
+
if (typeof root.definitionSnapshot == "string") return recordOf(parseDefinitionSnapshotValue({
|
|
208
|
+
_id: typeof root._id == "string" ? root._id : "<unknown instance>",
|
|
209
|
+
definitionSnapshot: root.definitionSnapshot
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function persistedFieldEntries(document) {
|
|
214
|
+
const root = recordOf(document);
|
|
215
|
+
if (root === void 0) return [];
|
|
216
|
+
const snapshot = parsedDefinitionSnapshot(root), roots = snapshot === void 0 ? [ root ] : [ root, snapshot ], stages = roots.flatMap(candidate => recordsAt(candidate, "stages")), activities = stages.flatMap(stage => recordsAt(stage, "activities")), actions = activities.flatMap(activity => recordsAt(activity, "actions")), effects = actions.flatMap(action => recordsAt(action, "effects"));
|
|
217
|
+
return nestedFieldEntries([ ...roots.flatMap(candidate => recordsAt(candidate, "fields")), ...stages.flatMap(stage => recordsAt(stage, "fields")), ...activities.flatMap(activity => recordsAt(activity, "fields")), ...actions.flatMap(action => recordsAt(action, "params")), ...effects.flatMap(effect => recordsAt(effect, "outputs")) ]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasChoiceList(document) {
|
|
221
|
+
return persistedFieldEntries(document).some(entry => {
|
|
222
|
+
const options = recordOf(entry.options);
|
|
223
|
+
return options !== void 0 && Array.isArray(options.list);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hasFieldKind(document, kind) {
|
|
228
|
+
return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function hasActionSemantics(document) {
|
|
232
|
+
const root = recordOf(document);
|
|
233
|
+
return root === void 0 ? !1 : recordsAt(root, "stages").flatMap(stage => recordsAt(stage, "activities")).flatMap(activity => recordsAt(activity, "actions")).some(action => Array.isArray(action.semantics));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasScalarValidation(document) {
|
|
237
|
+
return persistedFieldEntries(document).some(entry => {
|
|
238
|
+
const validation = recordOf(entry.validation);
|
|
239
|
+
return typeof validation?.min == "number" || typeof validation?.max == "number";
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasClaimTokens(document) {
|
|
244
|
+
const root = recordOf(document);
|
|
245
|
+
return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
|
|
246
|
+
const claim = recordOf(entry.claim);
|
|
247
|
+
return claim !== void 0 && typeof claim.claimToken == "string";
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const featureDetectors = {
|
|
252
|
+
"governed-model-stamps": () => !0,
|
|
253
|
+
"subject-field-kind": document => hasFieldKind(document, "subject"),
|
|
254
|
+
"typed-scalar-choice-lists": hasChoiceList,
|
|
255
|
+
"action-semantics": hasActionSemantics,
|
|
256
|
+
"inclusive-scalar-bounds": hasScalarValidation,
|
|
257
|
+
"progress-field-kind": document => hasFieldKind(document, "progress"),
|
|
258
|
+
"effect-claim-tokens": hasClaimTokens
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
function requiredModelFeatures(documentType, document) {
|
|
262
|
+
return DATA_MODEL_CHANGES.filter(change => change.documentTypes.some(candidate => candidate === documentType) && featureDetectors[change.id](document));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function requiredReaderModel(documentType, document) {
|
|
266
|
+
return Math.max(0, ...requiredModelFeatures(documentType, document).map(change => change.minReaderModel));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function modelStampFor(args) {
|
|
270
|
+
return {
|
|
271
|
+
modelVersion: DATA_MODEL_VERSION,
|
|
272
|
+
minReaderModel: Math.max(args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function fieldTreeShape(value) {
|
|
277
|
+
if (Array.isArray(value)) return value.map(fieldTreeShape);
|
|
278
|
+
if (value === null) return "null";
|
|
279
|
+
if (typeof value == "object") {
|
|
280
|
+
const record = value;
|
|
281
|
+
return Object.fromEntries(Object.keys(record).toSorted().map(key => [ key, fieldTreeShape(record[key]) ]));
|
|
282
|
+
}
|
|
283
|
+
return typeof value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function modelVersionOf(doc) {
|
|
287
|
+
const stamp = doc.modelVersion;
|
|
288
|
+
return typeof stamp == "number" ? stamp : 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function minReaderModelOf(doc) {
|
|
292
|
+
const floor = doc.minReaderModel;
|
|
293
|
+
return typeof floor == "number" ? floor : modelVersionOf(doc);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
class ModelVersionAheadError extends WorkflowError {
|
|
297
|
+
documentId;
|
|
298
|
+
documentModelVersion;
|
|
299
|
+
requiredReaderModel;
|
|
300
|
+
engineModelVersion;
|
|
301
|
+
constructor(args) {
|
|
302
|
+
super("model-version-ahead", `Document "${args.documentId}" was written by engine data model ${args.documentModelVersion} and requires a reader at model ${args.requiredReaderModel} or newer; this engine reads up to model ${DATA_MODEL_VERSION}. Upgrade @sanity/workflow-engine to a version that understands it.`),
|
|
303
|
+
this.name = "ModelVersionAheadError", this.documentId = args.documentId, this.documentModelVersion = args.documentModelVersion,
|
|
304
|
+
this.requiredReaderModel = args.requiredReaderModel, this.engineModelVersion = DATA_MODEL_VERSION;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function assertReadableModel(doc) {
|
|
309
|
+
const documentReaderModel = minReaderModelOf(doc);
|
|
310
|
+
if (documentReaderModel > DATA_MODEL_VERSION) throw new ModelVersionAheadError({
|
|
311
|
+
documentId: doc._id,
|
|
312
|
+
documentModelVersion: modelVersionOf(doc),
|
|
313
|
+
requiredReaderModel: documentReaderModel
|
|
314
|
+
});
|
|
315
|
+
return doc;
|
|
316
|
+
}
|
|
317
|
+
|
|
7
318
|
function isCascadeFired(action) {
|
|
8
319
|
return action.when !== void 0;
|
|
9
320
|
}
|
|
@@ -24,16 +335,6 @@ function driverKind(actor) {
|
|
|
24
335
|
return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
|
|
25
336
|
}
|
|
26
337
|
|
|
27
|
-
function errorMessage(err) {
|
|
28
|
-
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function rethrowWithContext(err, context) {
|
|
32
|
-
throw new Error(`${context}: ${errorMessage(err)}`, {
|
|
33
|
-
cause: err
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
338
|
function andConditions(parts) {
|
|
38
339
|
const present = parts.filter(p => p !== void 0);
|
|
39
340
|
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
@@ -234,71 +535,6 @@ function isGdr(value) {
|
|
|
234
535
|
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
235
536
|
}
|
|
236
537
|
|
|
237
|
-
class WorkflowError extends Error {
|
|
238
|
-
kind;
|
|
239
|
-
constructor(kind, message, options) {
|
|
240
|
-
super(message, options), this.kind = kind;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
class ContractViolationError extends WorkflowError {
|
|
245
|
-
constructor(message) {
|
|
246
|
-
super("contract-violation", message), this.name = "ContractViolationError";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
class InstanceNotFoundError extends WorkflowError {
|
|
251
|
-
instanceId;
|
|
252
|
-
constructor(args) {
|
|
253
|
-
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
254
|
-
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
class DefinitionNotFoundError extends WorkflowError {
|
|
259
|
-
definition;
|
|
260
|
-
version;
|
|
261
|
-
constructor(args) {
|
|
262
|
-
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`),
|
|
263
|
-
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
class DefinitionInUseError extends WorkflowError {
|
|
268
|
-
definition;
|
|
269
|
-
blockedBy;
|
|
270
|
-
constructor(args) {
|
|
271
|
-
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
272
|
-
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function definitionInUseMessage(definition, blockedBy) {
|
|
277
|
-
if (blockedBy.reason === "non-terminal-instances") {
|
|
278
|
-
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
279
|
-
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.`;
|
|
280
|
-
}
|
|
281
|
-
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
282
|
-
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
class EffectNotFoundError extends WorkflowError {
|
|
286
|
-
instanceId;
|
|
287
|
-
effectKey;
|
|
288
|
-
settled;
|
|
289
|
-
constructor(args) {
|
|
290
|
-
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
291
|
-
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function effectNotFoundMessage(args) {
|
|
296
|
-
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
297
|
-
if (args.settled === void 0) return base;
|
|
298
|
-
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
299
|
-
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}`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
538
|
const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
|
|
303
539
|
|
|
304
540
|
function validateTag(tag) {
|
|
@@ -321,7 +557,13 @@ function asPredicate(validate) {
|
|
|
321
557
|
};
|
|
322
558
|
}
|
|
323
559
|
|
|
324
|
-
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts)
|
|
560
|
+
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
|
|
561
|
+
|
|
562
|
+
function lakeSegment(label) {
|
|
563
|
+
return v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const WorkflowResourceSchema = v.variant("type", [ v.object({
|
|
325
567
|
type: v.literal("dataset"),
|
|
326
568
|
id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
327
569
|
}), v.object({
|
|
@@ -337,13 +579,46 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
|
|
|
337
579
|
name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
338
580
|
resource: WorkflowResourceSchema
|
|
339
581
|
}), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
|
|
340
|
-
name:
|
|
341
|
-
|
|
582
|
+
name: lakeSegment("name"),
|
|
583
|
+
expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
|
|
584
|
+
tag: lakeSegment("tag"),
|
|
342
585
|
workflowResource: WorkflowResourceSchema,
|
|
343
|
-
resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings =>
|
|
586
|
+
resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
|
|
344
587
|
definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition"))
|
|
345
|
-
})
|
|
346
|
-
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
function firstDuplicatePair(items, keyOf) {
|
|
591
|
+
const seen = /* @__PURE__ */ new Map;
|
|
592
|
+
for (const item of items) {
|
|
593
|
+
const key = keyOf(item), earlier = seen.get(key);
|
|
594
|
+
if (earlier !== void 0) return [ earlier, item ];
|
|
595
|
+
seen.set(key, item);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function duplicateHandleMessage(bindings) {
|
|
600
|
+
const pair = firstDuplicatePair(bindings, binding => binding.name);
|
|
601
|
+
if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function duplicateNameMessage(deployments) {
|
|
605
|
+
const pair = firstDuplicatePair(deployments, deployment => deployment.name);
|
|
606
|
+
if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function partitionKey(deployment) {
|
|
610
|
+
return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function partitionCollisionMessage(deployments) {
|
|
614
|
+
const pair = firstDuplicatePair(deployments, partitionKey);
|
|
615
|
+
if (pair === void 0) return;
|
|
616
|
+
const [first, second] = pair;
|
|
617
|
+
return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
|
|
621
|
+
deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
|
|
347
622
|
telemetry: v.optional(TelemetryLoggerSchema)
|
|
348
623
|
});
|
|
349
624
|
|
|
@@ -580,6 +855,8 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
|
|
|
580
855
|
required: entry.required,
|
|
581
856
|
initialValue: entry.initialValue,
|
|
582
857
|
editable: editable,
|
|
858
|
+
options: entry.options,
|
|
859
|
+
validation: entry.validation,
|
|
583
860
|
types: entry.types,
|
|
584
861
|
fields: entry.fields,
|
|
585
862
|
of: entry.of
|
|
@@ -722,7 +999,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
722
999
|
};
|
|
723
1000
|
}
|
|
724
1001
|
|
|
725
|
-
const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
|
|
1002
|
+
const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
|
|
726
1003
|
|
|
727
1004
|
function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
|
|
728
1005
|
if (target === void 0 || target.type === "url") return target;
|
|
@@ -743,18 +1020,15 @@ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
|
|
|
743
1020
|
};
|
|
744
1021
|
}
|
|
745
1022
|
|
|
746
|
-
function
|
|
747
|
-
|
|
748
|
-
action: action,
|
|
749
|
-
path: path,
|
|
750
|
-
env: env,
|
|
751
|
-
ctx: ctx
|
|
752
|
-
});
|
|
753
|
-
action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
|
|
1023
|
+
function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
|
|
1024
|
+
action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
|
|
754
1025
|
path: [ ...path, "roles" ],
|
|
755
1026
|
message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
|
|
756
1027
|
});
|
|
757
|
-
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function desugarActionOps(args) {
|
|
1031
|
+
const {action: action, path: path, env: env, activityName: activityName, ctx: ctx} = args, ops = desugarOps({
|
|
758
1032
|
ops: action.ops,
|
|
759
1033
|
path: [ ...path, "ops" ],
|
|
760
1034
|
env: env,
|
|
@@ -765,9 +1039,32 @@ function desugarAction({action: action, path: path, env: env, activityName: acti
|
|
|
765
1039
|
type: "status.set",
|
|
766
1040
|
activity: activityName,
|
|
767
1041
|
status: action.status
|
|
768
|
-
}),
|
|
1042
|
+
}), ops;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
|
|
1046
|
+
if ("type" in action) return desugarClaimAction({
|
|
1047
|
+
action: action,
|
|
1048
|
+
path: path,
|
|
1049
|
+
env: env,
|
|
1050
|
+
ctx: ctx
|
|
1051
|
+
});
|
|
1052
|
+
reportEmptyActionRoles({
|
|
1053
|
+
action: action,
|
|
1054
|
+
path: path,
|
|
1055
|
+
ctx: ctx
|
|
1056
|
+
});
|
|
1057
|
+
const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarActionOps({
|
|
1058
|
+
action: action,
|
|
1059
|
+
path: path,
|
|
1060
|
+
env: env,
|
|
1061
|
+
activityName: activityName,
|
|
1062
|
+
ctx: ctx
|
|
1063
|
+
});
|
|
1064
|
+
return {
|
|
769
1065
|
...stripUndefined({
|
|
770
1066
|
name: action.name,
|
|
1067
|
+
semantics: action.semantics,
|
|
771
1068
|
title: action.title,
|
|
772
1069
|
description: action.description,
|
|
773
1070
|
group: normalizeGroup(action.group),
|
|
@@ -1154,7 +1451,7 @@ function isTerminalActivityStatus(status) {
|
|
|
1154
1451
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1155
1452
|
}
|
|
1156
1453
|
|
|
1157
|
-
const 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" ], CONDITION_VARS = [ {
|
|
1454
|
+
const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], 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" ], CONDITION_VARS = [ {
|
|
1158
1455
|
name: "self",
|
|
1159
1456
|
binding: "always",
|
|
1160
1457
|
label: "this workflow instance",
|
|
@@ -1163,7 +1460,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
|
|
|
1163
1460
|
name: "fields",
|
|
1164
1461
|
binding: "always",
|
|
1165
1462
|
label: "the workflow's fields",
|
|
1166
|
-
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
|
|
1463
|
+
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` (or `subject`) DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
|
|
1167
1464
|
}, {
|
|
1168
1465
|
name: "parent",
|
|
1169
1466
|
binding: "always",
|
|
@@ -1244,18 +1541,26 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
|
|
|
1244
1541
|
binding: "always",
|
|
1245
1542
|
label: "the spawned subworkflows",
|
|
1246
1543
|
description: "Every row of the instance's subworkflow registry, faceted by `activity`/`action`/`definition`/`rowKey`/`status` (`'active'|'done'|'aborted'`) with `current` marking the open stage entry's cohort and `stage` the child's current stage. Usable anywhere — transition `when`s, requirements, any stage's gates; the settled gate is `count($subworkflows[activity == <name> && current && status == 'active']) == 0`."
|
|
1247
|
-
} ], RESERVED_CONDITION_VARS = CONDITION_VARS.map(v2 => v2.name), FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
|
|
1544
|
+
} ], SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR = "subjectHasInFlightInstance", RESERVED_CONDITION_VARS = [ ...CONDITION_VARS.map(v2 => v2.name), SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR ], FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
|
|
1248
1545
|
name: "tag",
|
|
1546
|
+
label: "this engine tag",
|
|
1249
1547
|
description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
|
|
1250
1548
|
}, {
|
|
1251
1549
|
name: "definition",
|
|
1550
|
+
label: "this workflow definition",
|
|
1252
1551
|
description: "The `name` of the definition under evaluation (its own start block binds it)."
|
|
1253
1552
|
}, {
|
|
1254
1553
|
name: "now",
|
|
1554
|
+
label: "the current time",
|
|
1255
1555
|
description: "The ISO clock reading of the evaluating engine."
|
|
1556
|
+
}, {
|
|
1557
|
+
name: SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR,
|
|
1558
|
+
label: "the subject already has an in-flight workflow",
|
|
1559
|
+
description: "Whether any instance in this engine tag, across all definitions, has the same resource-qualified subject and no `completedAt`. Advisory under concurrent starts."
|
|
1256
1560
|
} ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
|
|
1257
1561
|
name: "fields",
|
|
1258
|
-
|
|
1562
|
+
label: "the start's input fields",
|
|
1563
|
+
description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` or `subject` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
|
|
1259
1564
|
} ], GUARD_PREDICATE_VARS = [ {
|
|
1260
1565
|
name: "guard",
|
|
1261
1566
|
description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
|
|
@@ -1405,6 +1710,18 @@ function releaseRef({res: res, releaseName: releaseName}) {
|
|
|
1405
1710
|
};
|
|
1406
1711
|
}
|
|
1407
1712
|
|
|
1713
|
+
function isSingleDocRefKind(kind) {
|
|
1714
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function refKindAcceptsTypes(kind) {
|
|
1718
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function isSingleDocRefEntry(entry) {
|
|
1722
|
+
return isSingleDocRefKind(entry._type);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1408
1725
|
function isTodoListItem(row) {
|
|
1409
1726
|
if (typeof row != "object" || row === null) return !1;
|
|
1410
1727
|
const candidate = row, status = candidate.status;
|
|
@@ -1455,13 +1772,39 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
|
|
|
1455
1772
|
}), tolerantObject()({
|
|
1456
1773
|
type: v.literal("role"),
|
|
1457
1774
|
role: NonEmptyString
|
|
1458
|
-
}) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString,
|
|
1775
|
+
}) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableProgress = v.union([ v.null(), v.pipe(v.number(), v.finite("progress must be a finite number"), v.minValue(0, "progress must be at least 0"), v.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
|
|
1776
|
+
|
|
1777
|
+
function normalizedChoiceKind(kind) {
|
|
1778
|
+
return kind === "dateTime" ? "datetime" : kind;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function checkChoiceList(args) {
|
|
1782
|
+
const {entryType: entryType, options: options, validation: validation} = args;
|
|
1783
|
+
if (options === void 0) return;
|
|
1784
|
+
if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
|
|
1785
|
+
const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => checkValueAgainst({
|
|
1786
|
+
entryType: kind,
|
|
1787
|
+
value: option.value,
|
|
1788
|
+
validation: validation
|
|
1789
|
+
}, valueSchemas)?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
|
|
1790
|
+
for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
|
|
1791
|
+
seen.add(option.value);
|
|
1792
|
+
return issues.length === 0 ? void 0 : issues;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function choiceValueIssues(options, value) {
|
|
1796
|
+
if (!(options === void 0 || value === null || value === void 0)) return options.list.some(option => Object.is(option.value, value)) ? void 0 : [ `value ${JSON.stringify(value)} is not declared in \`options.list\`; expected one of ${options.list.map(option => JSON.stringify(option.value)).join(", ")}` ];
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const fieldValueSchemas = {
|
|
1459
1800
|
"doc.ref": v.union([ v.null(), GdrShape ]),
|
|
1460
1801
|
"doc.refs": v.array(GdrShape),
|
|
1802
|
+
subject: v.union([ v.null(), GdrShape ]),
|
|
1461
1803
|
"release.ref": v.union([ v.null(), ReleaseRefShape ]),
|
|
1462
1804
|
string: NullableString,
|
|
1463
1805
|
text: NullableString,
|
|
1464
1806
|
number: NullableNumber,
|
|
1807
|
+
progress: NullableProgress,
|
|
1465
1808
|
boolean: NullableBoolean,
|
|
1466
1809
|
date: NullableDate,
|
|
1467
1810
|
datetime: NullableDateTime,
|
|
@@ -1475,7 +1818,56 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
|
|
|
1475
1818
|
};
|
|
1476
1819
|
|
|
1477
1820
|
function shapeValueSchema(shape, leaf) {
|
|
1478
|
-
|
|
1821
|
+
if (shape.type === "object") return objectSchema(shape.fields ?? [], leaf);
|
|
1822
|
+
if (shape.type === "array") return v.array(objectSchema(shape.of ?? [], leaf));
|
|
1823
|
+
const schema = leaf[shape.type] ?? v.any();
|
|
1824
|
+
return constrainedScalarSchema({
|
|
1825
|
+
schema: schema,
|
|
1826
|
+
entryType: shape.type,
|
|
1827
|
+
...shape
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function scalarMeasurement(entryType, value) {
|
|
1832
|
+
if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
|
|
1833
|
+
if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function scalarBoundIssue(args) {
|
|
1837
|
+
const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
|
|
1838
|
+
return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function scalarValidationIssues(args) {
|
|
1842
|
+
const {entryType: entryType, validation: validation, value: value} = args;
|
|
1843
|
+
if (validation === void 0 || value === null || value === void 0) return;
|
|
1844
|
+
const measured = scalarMeasurement(entryType, value);
|
|
1845
|
+
if (measured === void 0) return;
|
|
1846
|
+
const issues = [ scalarBoundIssue({
|
|
1847
|
+
entryType: entryType,
|
|
1848
|
+
measured: measured,
|
|
1849
|
+
bound: validation.min,
|
|
1850
|
+
limit: "min"
|
|
1851
|
+
}), scalarBoundIssue({
|
|
1852
|
+
entryType: entryType,
|
|
1853
|
+
measured: measured,
|
|
1854
|
+
bound: validation.max,
|
|
1855
|
+
limit: "max"
|
|
1856
|
+
}) ].filter(issue => issue !== void 0);
|
|
1857
|
+
return issues.length === 0 ? void 0 : issues;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function constrainedScalarSchema(args) {
|
|
1861
|
+
const {schema: schema, entryType: entryType, options: options, validation: validation} = args;
|
|
1862
|
+
return options === void 0 && validation === void 0 ? schema : v.pipe(schema, v.check(value => choiceValueIssues(options, value) === void 0 && scalarValidationIssues({
|
|
1863
|
+
entryType: entryType,
|
|
1864
|
+
validation: validation,
|
|
1865
|
+
value: value
|
|
1866
|
+
}) === void 0, issue => [ ...choiceValueIssues(options, issue.input) ?? [], ...scalarValidationIssues({
|
|
1867
|
+
entryType: entryType,
|
|
1868
|
+
validation: validation,
|
|
1869
|
+
value: issue.input
|
|
1870
|
+
}) ?? [] ].join("; ")));
|
|
1479
1871
|
}
|
|
1480
1872
|
|
|
1481
1873
|
function objectSchema(fields, leaf) {
|
|
@@ -1498,7 +1890,7 @@ function appendItemSchema(entryType, shape) {
|
|
|
1498
1890
|
function rejectedRefTypes(args) {
|
|
1499
1891
|
const {entryType: entryType, types: types, value: value} = args;
|
|
1500
1892
|
if (types === void 0 || value === null || value === void 0) return [];
|
|
1501
|
-
if (entryType
|
|
1893
|
+
if (!refKindAcceptsTypes(entryType)) return [];
|
|
1502
1894
|
let items = [ value ];
|
|
1503
1895
|
return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
|
|
1504
1896
|
[ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
|
|
@@ -1533,6 +1925,10 @@ function checkValueAgainst(args, leaf) {
|
|
|
1533
1925
|
entryType: args.entryType,
|
|
1534
1926
|
types: args.types,
|
|
1535
1927
|
value: args.value
|
|
1928
|
+
}) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
|
|
1929
|
+
entryType: args.entryType,
|
|
1930
|
+
validation: args.validation,
|
|
1931
|
+
value: args.value
|
|
1536
1932
|
}) : formatIssues(result.issues);
|
|
1537
1933
|
}
|
|
1538
1934
|
|
|
@@ -1563,6 +1959,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
|
|
|
1563
1959
|
...valueSchemas,
|
|
1564
1960
|
"doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
|
|
1565
1961
|
"doc.refs": v.array(AuthoringGdrShape),
|
|
1962
|
+
subject: v.union([ v.null(), AuthoringGdrShape ]),
|
|
1566
1963
|
"release.ref": v.union([ v.null(), v.looseObject({
|
|
1567
1964
|
id: AuthoringRefId,
|
|
1568
1965
|
type: v.literal("system.release"),
|
|
@@ -1602,10 +1999,10 @@ function validateFieldAppendItem(args) {
|
|
|
1602
1999
|
});
|
|
1603
2000
|
}
|
|
1604
2001
|
|
|
1605
|
-
function formatIssues(issues) {
|
|
2002
|
+
function formatIssues(issues, formatMessage = issue => issue.message) {
|
|
1606
2003
|
return issues.map(i => {
|
|
1607
2004
|
const keys = i.path?.map(p => p.key) ?? [];
|
|
1608
|
-
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i
|
|
2005
|
+
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
|
|
1609
2006
|
});
|
|
1610
2007
|
}
|
|
1611
2008
|
|
|
@@ -1731,7 +2128,15 @@ function groupMembershipNames(group) {
|
|
|
1731
2128
|
return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
|
|
1732
2129
|
}
|
|
1733
2130
|
|
|
1734
|
-
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`")
|
|
2131
|
+
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
|
|
2132
|
+
min: v.optional(FiniteNumber),
|
|
2133
|
+
max: v.optional(FiniteNumber)
|
|
2134
|
+
}), v.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v.strictObject({
|
|
2135
|
+
list: v.pipe(v.array(v.strictObject({
|
|
2136
|
+
title: NonEmpty,
|
|
2137
|
+
value: v.union([ v.string(), v.number() ])
|
|
2138
|
+
})), v.minLength(1, "declare at least one choice, or omit `options`"))
|
|
2139
|
+
});
|
|
1735
2140
|
|
|
1736
2141
|
function asShape(input) {
|
|
1737
2142
|
return typeof input == "object" && input !== null ? input : {};
|
|
@@ -1765,14 +2170,16 @@ function compositeChecked(entries) {
|
|
|
1765
2170
|
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\``));
|
|
1766
2171
|
}
|
|
1767
2172
|
|
|
1768
|
-
const FieldShapeSchema = v.lazy(() => compositeChecked({
|
|
2173
|
+
const FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
|
|
1769
2174
|
type: FieldValueKindSchema,
|
|
1770
2175
|
name: FieldEntryName,
|
|
1771
2176
|
title: v.optional(v.string()),
|
|
1772
2177
|
description: v.optional(v.string()),
|
|
2178
|
+
options: v.optional(ChoiceOptionsSchema),
|
|
2179
|
+
validation: v.optional(ScalarValidationSchema),
|
|
1773
2180
|
fields: v.optional(v.array(FieldShapeSchema)),
|
|
1774
2181
|
of: v.optional(v.array(FieldShapeSchema))
|
|
1775
|
-
})), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
|
|
2182
|
+
}), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
|
|
1776
2183
|
|
|
1777
2184
|
function fieldBase(editable, group) {
|
|
1778
2185
|
return {
|
|
@@ -1790,6 +2197,8 @@ function fieldEntryFields(editable, group) {
|
|
|
1790
2197
|
return {
|
|
1791
2198
|
type: FieldKindSchema,
|
|
1792
2199
|
...fieldBase(editable, group),
|
|
2200
|
+
options: v.optional(ChoiceOptionsSchema),
|
|
2201
|
+
validation: v.optional(ScalarValidationSchema),
|
|
1793
2202
|
types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
|
|
1794
2203
|
fields: v.optional(v.array(FieldShapeSchema)),
|
|
1795
2204
|
of: v.optional(v.array(FieldShapeSchema))
|
|
@@ -1802,7 +2211,9 @@ function literalSeedIssues(entry) {
|
|
|
1802
2211
|
value: entry.initialValue.value,
|
|
1803
2212
|
types: entry.types,
|
|
1804
2213
|
fields: entry.fields,
|
|
1805
|
-
of: entry.of
|
|
2214
|
+
of: entry.of,
|
|
2215
|
+
options: entry.options,
|
|
2216
|
+
validation: entry.validation
|
|
1806
2217
|
});
|
|
1807
2218
|
}
|
|
1808
2219
|
|
|
@@ -1811,10 +2222,41 @@ function literalSeedCheck() {
|
|
|
1811
2222
|
}
|
|
1812
2223
|
|
|
1813
2224
|
function refTypesCheck() {
|
|
1814
|
-
return v.check(entry => entry.types === void 0 || entry.type
|
|
2225
|
+
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}"`);
|
|
1815
2226
|
}
|
|
1816
2227
|
|
|
1817
|
-
|
|
2228
|
+
function choiceOptionsCheck() {
|
|
2229
|
+
return v.check(entry => checkChoiceList({
|
|
2230
|
+
entryType: entry.type,
|
|
2231
|
+
options: entry.options,
|
|
2232
|
+
validation: entry.validation
|
|
2233
|
+
}) === void 0, issue => (checkChoiceList({
|
|
2234
|
+
entryType: issue.input.type,
|
|
2235
|
+
options: issue.input.options,
|
|
2236
|
+
validation: issue.input.validation
|
|
2237
|
+
}) ?? []).join("; "));
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
|
|
2241
|
+
|
|
2242
|
+
function scalarValidationCheck() {
|
|
2243
|
+
return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function scalarValidationDeclarationIssues(entry) {
|
|
2247
|
+
const {type: type, validation: validation} = entry;
|
|
2248
|
+
if (validation === void 0) return;
|
|
2249
|
+
if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
|
|
2250
|
+
if (type === "progress") {
|
|
2251
|
+
const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
|
|
2252
|
+
return issues2.length === 0 ? void 0 : issues2;
|
|
2253
|
+
}
|
|
2254
|
+
if (type === "number") return;
|
|
2255
|
+
const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
|
|
2256
|
+
return issues.length === 0 ? void 0 : issues;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
|
|
1818
2260
|
type: v.literal("claim"),
|
|
1819
2261
|
name: FieldEntryName,
|
|
1820
2262
|
title: v.optional(v.string()),
|
|
@@ -1845,17 +2287,25 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
|
|
|
1845
2287
|
with: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1846
2288
|
context: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1847
2289
|
onExit: v.optional(picklist([ "detach", "abort" ]))
|
|
1848
|
-
}), ActionParamSchema = v.strictObject({
|
|
2290
|
+
}), ActionParamSchema = v.pipe(v.strictObject({
|
|
1849
2291
|
type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
|
|
1850
2292
|
name: NonEmpty,
|
|
1851
2293
|
title: v.optional(v.string()),
|
|
1852
2294
|
description: v.optional(v.string()),
|
|
1853
|
-
required: v.optional(v.boolean())
|
|
1854
|
-
|
|
2295
|
+
required: v.optional(v.boolean()),
|
|
2296
|
+
options: v.optional(ChoiceOptionsSchema),
|
|
2297
|
+
validation: v.optional(ScalarValidationSchema)
|
|
2298
|
+
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2299
|
+
|
|
2300
|
+
function hasUniqueSemanticNamespaces(semantics) {
|
|
2301
|
+
const namespaces = semantics.map(semantic => semantic.split(".", 1)[0]);
|
|
2302
|
+
return new Set(namespaces).size === namespaces.length;
|
|
2303
|
+
}
|
|
1855
2304
|
|
|
1856
2305
|
function actionFields(op, group) {
|
|
1857
2306
|
return {
|
|
1858
2307
|
name: NonEmpty,
|
|
2308
|
+
semantics: v.optional(v.pipe(v.array(picklist(ACTION_SEMANTICS)), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace"))),
|
|
1859
2309
|
title: v.optional(v.string()),
|
|
1860
2310
|
description: v.optional(v.string()),
|
|
1861
2311
|
group: v.optional(group),
|
|
@@ -2044,7 +2494,7 @@ function startKindOf(definition) {
|
|
|
2044
2494
|
}
|
|
2045
2495
|
|
|
2046
2496
|
function isSubjectEntry(entry) {
|
|
2047
|
-
return entry.
|
|
2497
|
+
return entry.type === "subject";
|
|
2048
2498
|
}
|
|
2049
2499
|
|
|
2050
2500
|
function isInputSourced(entry) {
|
|
@@ -2546,6 +2996,52 @@ function checkAssigneesEntries(def, issues) {
|
|
|
2546
2996
|
});
|
|
2547
2997
|
}
|
|
2548
2998
|
|
|
2999
|
+
function nestedSubjectPaths(shapes, base) {
|
|
3000
|
+
return (shapes ?? []).flatMap((shape, i) => isSubjectEntry(shape) ? [ [ ...base, i, "type" ] ] : [ ...nestedSubjectPaths(shape.fields, [ ...base, i, "fields" ]), ...nestedSubjectPaths(shape.of, [ ...base, i, "of" ]) ]);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function checkSubjectEffectOutputs(def, issues) {
|
|
3004
|
+
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) pushSubjectOutputIssues({
|
|
3005
|
+
action: action,
|
|
3006
|
+
path: [ "stages", i, "activities", j, "actions", a ],
|
|
3007
|
+
issues: issues
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function pushSubjectOutputIssues(args) {
|
|
3012
|
+
const {action: action, path: path, issues: issues} = args;
|
|
3013
|
+
for (const [e, effect] of (action.effects ?? []).entries()) {
|
|
3014
|
+
const base = [ ...path, "effects", e, "outputs" ];
|
|
3015
|
+
for (const nestedPath of nestedSubjectPaths(effect.outputs, base)) issues.push({
|
|
3016
|
+
path: nestedPath,
|
|
3017
|
+
message: `effect "${effect.name}" declares a \`subject\` output shape — no subject reader ever looks at effect outputs. Declare \`doc.ref\` for a document-valued output`
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
function checkSubjectEntries(def, issues) {
|
|
3023
|
+
for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) {
|
|
3024
|
+
const subjects = (entries ?? []).flatMap((entry, n) => isSubjectEntry(entry) ? [ {
|
|
3025
|
+
entry: entry,
|
|
3026
|
+
n: n
|
|
3027
|
+
} ] : []);
|
|
3028
|
+
if (scope !== "workflow") for (const {entry: entry, n: n} of subjects) issues.push({
|
|
3029
|
+
path: [ ...path, n, "type" ],
|
|
3030
|
+
message: `${label} entry "${entry.name}" is a \`subject\` — the subject names the document the WORKFLOW is about, so it is valid only on a workflow-scope entry. Declare \`doc.ref\` for a plain reference here`
|
|
3031
|
+
}); else for (const {n: n} of subjects.slice(1)) issues.push({
|
|
3032
|
+
path: [ ...path, n ],
|
|
3033
|
+
message: "at most one subject-kind field entry per workflow — the runtime identifies THE subject by kind, and a second one makes it ambiguous"
|
|
3034
|
+
});
|
|
3035
|
+
for (const [n, entry] of (entries ?? []).entries()) {
|
|
3036
|
+
const nested = [ ...nestedSubjectPaths(entry.fields, [ ...path, n, "fields" ]), ...nestedSubjectPaths(entry.of, [ ...path, n, "of" ]) ];
|
|
3037
|
+
for (const nestedPath of nested) issues.push({
|
|
3038
|
+
path: nestedPath,
|
|
3039
|
+
message: `${label} entry "${entry.name}" declares a \`subject\` sub-field — a nested subject can never be read as the workflow's subject. Declare \`doc.ref\` for a nested reference`
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2549
3045
|
function checkActivityTerminalPaths(def, issues) {
|
|
2550
3046
|
for (const [i, stage] of def.stages.entries()) {
|
|
2551
3047
|
const resolvable = terminallyResolvableActivities(stage);
|
|
@@ -2596,12 +3092,30 @@ function checkStart(def, issues) {
|
|
|
2596
3092
|
if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
|
|
2597
3093
|
path: [ "start" ],
|
|
2598
3094
|
message: "a spawn-only (lifecycle 'child') definition declares `start` — children are instantiated by a parent's `spawn`, never started standalone, so the block would never apply. Remove `start`, or drop `lifecycle: 'child'`"
|
|
2599
|
-
}), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def
|
|
3095
|
+
}), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), checkStartSubjectVariable(def, issues),
|
|
3096
|
+
def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
|
|
2600
3097
|
path: [ "fields", n, "required" ],
|
|
2601
|
-
message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not
|
|
3098
|
+
message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not the workflow's subject. Make it optional, seed it another way (query/literal), or declare it the \`subject\` entry (the document the run is about)`
|
|
2602
3099
|
});
|
|
2603
3100
|
}
|
|
2604
3101
|
|
|
3102
|
+
function checkStartSubjectVariable(def, issues) {
|
|
3103
|
+
const subject = (def.fields ?? []).find(isSubjectEntry);
|
|
3104
|
+
for (const [key, condition] of [ [ "filter", def.start?.filter ], [ "allowed", def.start?.allowed ] ]) if (condition !== void 0 && conditionParameterNames(condition).has(SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR)) {
|
|
3105
|
+
if (subject === void 0) {
|
|
3106
|
+
issues.push({
|
|
3107
|
+
path: [ "start", key ],
|
|
3108
|
+
message: `start.${key} reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition declares no \`subject\` entry — the engine has no prospective subject identity to match. Declare a \`subject\` entry (the document the workflow is about), or remove the variable`
|
|
3109
|
+
});
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
key === "allowed" && !isInputSourced(subject) && issues.push({
|
|
3113
|
+
path: [ "start", key ],
|
|
3114
|
+
message: `start.allowed reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition's subject entry "${subject.name}" is not \`input\`-sourced — the start gate cannot bind its prospective subject. Make the subject input-sourced, or remove the variable`
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
2605
3119
|
function checkStartFilterReads(def, issues) {
|
|
2606
3120
|
const filter = def.start?.filter;
|
|
2607
3121
|
filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
|
|
@@ -2609,7 +3123,7 @@ function checkStartFilterReads(def, issues) {
|
|
|
2609
3123
|
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 start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
|
|
2610
3124
|
}), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
2611
3125
|
path: [ "start", "filter" ],
|
|
2612
|
-
message: "start.filter reads the candidate document (its root), but the definition declares no
|
|
3126
|
+
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"
|
|
2613
3127
|
}));
|
|
2614
3128
|
}
|
|
2615
3129
|
|
|
@@ -2926,7 +3440,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
2926
3440
|
ops: site.ops,
|
|
2927
3441
|
path: site.path,
|
|
2928
3442
|
label: site.label,
|
|
2929
|
-
activityEntries:
|
|
3443
|
+
activityEntries: site.activity.fields ?? []
|
|
2930
3444
|
});
|
|
2931
3445
|
}
|
|
2932
3446
|
|
|
@@ -2948,6 +3462,9 @@ function fieldReadsIn(value, path) {
|
|
|
2948
3462
|
|
|
2949
3463
|
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
|
|
2950
3464
|
const hosts = [ {
|
|
3465
|
+
scope: "activity",
|
|
3466
|
+
entries: activityEntries
|
|
3467
|
+
}, {
|
|
2951
3468
|
scope: "stage",
|
|
2952
3469
|
entries: stageEntries
|
|
2953
3470
|
}, {
|
|
@@ -2960,8 +3477,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2960
3477
|
message: opFieldReadMissMessage({
|
|
2961
3478
|
read: read,
|
|
2962
3479
|
where: where,
|
|
2963
|
-
hosts: hosts
|
|
2964
|
-
activityEntries: activityEntries
|
|
3480
|
+
hosts: hosts
|
|
2965
3481
|
})
|
|
2966
3482
|
});
|
|
2967
3483
|
return;
|
|
@@ -2975,9 +3491,9 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2975
3491
|
});
|
|
2976
3492
|
}
|
|
2977
3493
|
|
|
2978
|
-
function opFieldReadMissMessage({read: read, where: where, hosts: hosts
|
|
2979
|
-
const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`,
|
|
2980
|
-
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
|
|
3494
|
+
function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
3495
|
+
const searched = read.scope === void 0 ? "activity, stage, or workflow scope" : `${read.scope} scope`, known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
|
|
3496
|
+
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)"}`;
|
|
2981
3497
|
}
|
|
2982
3498
|
|
|
2983
3499
|
function checkUpdateWhereOps(def, issues) {
|
|
@@ -3140,10 +3656,12 @@ const SCALAR = {
|
|
|
3140
3656
|
kind: "list",
|
|
3141
3657
|
item: GDR_VALUE
|
|
3142
3658
|
}),
|
|
3659
|
+
subject: () => DOC_CONTENT,
|
|
3143
3660
|
"release.ref": () => RELEASE_VALUE,
|
|
3144
3661
|
string: () => SCALAR,
|
|
3145
3662
|
text: () => SCALAR,
|
|
3146
3663
|
number: () => SCALAR,
|
|
3664
|
+
progress: () => SCALAR,
|
|
3147
3665
|
boolean: () => SCALAR,
|
|
3148
3666
|
date: () => SCALAR,
|
|
3149
3667
|
datetime: () => SCALAR,
|
|
@@ -3164,7 +3682,8 @@ const SCALAR = {
|
|
|
3164
3682
|
})
|
|
3165
3683
|
}, START_ALLOWED_VALUE_NODES = {
|
|
3166
3684
|
...VALUE_NODES,
|
|
3167
|
-
"doc.ref": () => GDR_VALUE
|
|
3685
|
+
"doc.ref": () => GDR_VALUE,
|
|
3686
|
+
subject: () => GDR_VALUE
|
|
3168
3687
|
};
|
|
3169
3688
|
|
|
3170
3689
|
function valueNodeFor(shape, nodes) {
|
|
@@ -3236,9 +3755,10 @@ function checkWorkflowInvariants(def) {
|
|
|
3236
3755
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3237
3756
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3238
3757
|
checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3239
|
-
checkAssigneesEntries(def, issues),
|
|
3758
|
+
checkAssigneesEntries(def, issues), checkSubjectEntries(def, issues), checkSubjectEffectOutputs(def, issues),
|
|
3759
|
+
checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
|
|
3240
3760
|
checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
|
|
3241
3761
|
checkGroups(def, issues), issues;
|
|
3242
3762
|
}
|
|
3243
3763
|
|
|
3244
|
-
export { ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, 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_ALLOWED_VARS, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|
|
3764
|
+
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DATA_MODEL_CHANGES, DATA_MODEL_MIN_READER, DATA_MODEL_VERSION, 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, ModelVersionAheadError, NonEmptyString, PersistedDocShapeError, READER_MODEL_ROLLOUT_URL, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, ReaderModelAcknowledgementError, START_ALLOWED_VARS, START_FILTER_VARS, SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR, SpawnContractsInvalidError, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assertReadableModel, assertReaderModelAcknowledgement, checkFieldValue, checkWorkflowInvariants, choiceValueIssues, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldTreeShape, fieldValueSchemas, 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, isUnprimed, labelFor, minReaderModelOf, modelStampFor, modelVersionOf, parentRef, parseDefinitionSnapshot, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, requiredModelFeatures, requiredReaderModel, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, terminalState, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|