@hyperscale0/udl 4.2.1 → 4.3.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 +8 -0
- package/dist/object-contract.d.ts +11 -7
- package/dist/object-contract.d.ts.map +1 -1
- package/dist/object-contract.js +95 -5
- package/dist/object-contract.js.map +1 -1
- package/dist/schema.d.ts +183 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +27 -12
- package/dist/schema.js.map +1 -1
- package/docs/README.md +8 -15
- package/package.json +2 -2
- package/spec/README.md +8 -15
- package/spec/udl.schema.json +96 -50
- package/src/object-contract.ts +135 -18
- package/src/schema.ts +34 -13
package/src/object-contract.ts
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
type UdlField,
|
|
7
7
|
type UdlObjectField,
|
|
8
8
|
type UdlObjectKind,
|
|
9
|
+
type UdlSubjectRequirement,
|
|
10
|
+
type UdlValue,
|
|
11
|
+
type UdlInstrument,
|
|
9
12
|
} from "./schema.js";
|
|
10
13
|
|
|
11
14
|
export const requestAttributionSchema = z.union([
|
|
@@ -58,6 +61,10 @@ export interface ObjectActionDiscovery extends ObjectActionContext {
|
|
|
58
61
|
instrument: string;
|
|
59
62
|
action: string;
|
|
60
63
|
requirements: readonly UdlObjectField[];
|
|
64
|
+
requirementConditions?: Record<
|
|
65
|
+
string,
|
|
66
|
+
NonNullable<UdlSubjectRequirement["when"]>
|
|
67
|
+
>;
|
|
61
68
|
creationRequirements?: readonly UdlObjectField[];
|
|
62
69
|
creationOnlyNames?: readonly string[];
|
|
63
70
|
inputSchema: JsonSchemaDocument;
|
|
@@ -275,33 +282,36 @@ export function projectObjectDiscovery(
|
|
|
275
282
|
const isCreationAction =
|
|
276
283
|
name === "create" || (createHidden && canStartFromInitial);
|
|
277
284
|
|
|
278
|
-
const rawCreateReqs:
|
|
279
|
-
field: UdlField;
|
|
280
|
-
objectField?: string | undefined;
|
|
281
|
-
}[] = isCreationAction
|
|
285
|
+
const rawCreateReqs: UdlSubjectRequirement[] = isCreationAction
|
|
282
286
|
? [
|
|
283
287
|
...(instrument.actions.create?.subject?.requirements ?? []),
|
|
284
|
-
...attachmentRefListFields.map((field) => ({
|
|
288
|
+
...attachmentRefListFields.map((field) => ({
|
|
289
|
+
field: field as UdlObjectField,
|
|
290
|
+
})),
|
|
285
291
|
]
|
|
286
292
|
: [];
|
|
287
293
|
const actionReqs = action.subject?.requirements ?? [];
|
|
288
294
|
const actionReqNames = new Set(
|
|
289
295
|
actionReqs.map((r) => r.objectField ?? r.field.name),
|
|
290
296
|
);
|
|
291
|
-
const mergedReqs:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}[] = [...actionReqs];
|
|
297
|
+
const mergedReqs: UdlSubjectRequirement[] = actionReqs.map(
|
|
298
|
+
(requirement) => ({ ...requirement }),
|
|
299
|
+
);
|
|
295
300
|
for (const req of rawCreateReqs) {
|
|
296
301
|
const reqName = req.objectField ?? req.field.name;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
const existing = mergedReqs.find(
|
|
303
|
+
(r) => (r.objectField ?? r.field.name) === reqName,
|
|
304
|
+
);
|
|
305
|
+
if (!existing) mergedReqs.push({ ...req });
|
|
306
|
+
else if (!req.when) delete existing.when;
|
|
307
|
+
else if (existing.when)
|
|
308
|
+
existing.when = [...existing.when, ...req.when];
|
|
304
309
|
}
|
|
310
|
+
const requirementConditions = Object.fromEntries(
|
|
311
|
+
mergedReqs.flatMap((req) =>
|
|
312
|
+
req.when ? [[req.objectField ?? req.field.name, req.when]] : [],
|
|
313
|
+
),
|
|
314
|
+
);
|
|
305
315
|
const unbound = [
|
|
306
316
|
...(action.subject?.adapters ?? []),
|
|
307
317
|
...(isCreationAction
|
|
@@ -346,6 +356,9 @@ export function projectObjectDiscovery(
|
|
|
346
356
|
name: requirement.objectField ?? requirement.field.name,
|
|
347
357
|
}) as UdlObjectField,
|
|
348
358
|
),
|
|
359
|
+
...(Object.keys(requirementConditions).length
|
|
360
|
+
? { requirementConditions }
|
|
361
|
+
: {}),
|
|
349
362
|
...(creationRequirements.length > 0
|
|
350
363
|
? { creationRequirements }
|
|
351
364
|
: {}),
|
|
@@ -391,11 +404,104 @@ export function projectObjectDiscovery(
|
|
|
391
404
|
};
|
|
392
405
|
}
|
|
393
406
|
|
|
407
|
+
/** Values overwritten before invocation cannot decide a guard during discovery. */
|
|
408
|
+
export function objectActionSelf(
|
|
409
|
+
instrument: UdlInstrument,
|
|
410
|
+
actionName: string,
|
|
411
|
+
fields: Readonly<Record<string, unknown>> = {},
|
|
412
|
+
): Record<string, unknown> {
|
|
413
|
+
const action = instrument.actions[actionName]!;
|
|
414
|
+
const known = {
|
|
415
|
+
...Object.fromEntries(
|
|
416
|
+
instrument.fields.flatMap((field) =>
|
|
417
|
+
"value" in field ? [[field.name, field.value]] : [],
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
...fields,
|
|
421
|
+
};
|
|
422
|
+
for (const calculation of [
|
|
423
|
+
...instrument.calculate,
|
|
424
|
+
...(action.calculate ?? []),
|
|
425
|
+
])
|
|
426
|
+
delete known[calculation.target];
|
|
427
|
+
for (const name of Object.keys(action.set ?? {})) delete known[name];
|
|
428
|
+
return known;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface ObjectActionStateOptions {
|
|
432
|
+
attached?: boolean;
|
|
433
|
+
self?: Readonly<Record<string, unknown>>;
|
|
434
|
+
input?: Readonly<Record<string, unknown>>;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Unknown invocation state remains possible; a known false guard suppresses a prompt. */
|
|
438
|
+
function requirementPossible(
|
|
439
|
+
paths: NonNullable<UdlSubjectRequirement["when"]>,
|
|
440
|
+
action: ObjectActionDiscovery,
|
|
441
|
+
fields: Readonly<Record<string, unknown>>,
|
|
442
|
+
options: ObjectActionStateOptions,
|
|
443
|
+
): boolean {
|
|
444
|
+
return paths.some((path) =>
|
|
445
|
+
path.every((condition) => {
|
|
446
|
+
const read = (value: UdlValue): unknown => {
|
|
447
|
+
if ("literal" in value) return value.literal;
|
|
448
|
+
const [root, name, ...tail] = value.field.split(".");
|
|
449
|
+
if (!name || tail.length) return undefined;
|
|
450
|
+
if (root === "subject") return fields[name];
|
|
451
|
+
if (
|
|
452
|
+
condition.instrument !== action.instrument ||
|
|
453
|
+
condition.action !== action.action
|
|
454
|
+
)
|
|
455
|
+
return undefined;
|
|
456
|
+
if (root === "self") return options.self?.[name];
|
|
457
|
+
if (root === "input") return options.input?.[name];
|
|
458
|
+
return undefined;
|
|
459
|
+
};
|
|
460
|
+
let left = read(condition.guard.left);
|
|
461
|
+
let right = read(condition.guard.right);
|
|
462
|
+
if (left === undefined || right === undefined) return true;
|
|
463
|
+
try {
|
|
464
|
+
if (condition.valueType === "money") {
|
|
465
|
+
left = BigInt(String(left));
|
|
466
|
+
right = BigInt(String(right));
|
|
467
|
+
} else if (condition.valueType === "date") {
|
|
468
|
+
left = Date.parse(String(left));
|
|
469
|
+
right = Date.parse(String(right));
|
|
470
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return true;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
typeof left !== typeof right ||
|
|
477
|
+
!["string", "number", "boolean", "bigint"].includes(typeof left)
|
|
478
|
+
)
|
|
479
|
+
return true;
|
|
480
|
+
const a = left as string | number | bigint;
|
|
481
|
+
const b = right as string | number | bigint;
|
|
482
|
+
switch (condition.guard.operator) {
|
|
483
|
+
case "==":
|
|
484
|
+
return left === right;
|
|
485
|
+
case "!=":
|
|
486
|
+
return left !== right;
|
|
487
|
+
case "<":
|
|
488
|
+
return a < b;
|
|
489
|
+
case "<=":
|
|
490
|
+
return a <= b;
|
|
491
|
+
case ">":
|
|
492
|
+
return a > b;
|
|
493
|
+
case ">=":
|
|
494
|
+
return a >= b;
|
|
495
|
+
}
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
394
500
|
/** Object-specific values determine missing fields; global discovery cannot. */
|
|
395
501
|
export function objectActionState(
|
|
396
502
|
action: ObjectActionDiscovery,
|
|
397
503
|
fields: Readonly<Record<string, unknown>>,
|
|
398
|
-
options?:
|
|
504
|
+
options?: ObjectActionStateOptions | boolean,
|
|
399
505
|
): ObjectActionState {
|
|
400
506
|
const attached =
|
|
401
507
|
typeof options === "boolean" ? options : (options?.attached ?? false);
|
|
@@ -404,6 +510,17 @@ export function objectActionState(
|
|
|
404
510
|
...action,
|
|
405
511
|
requiredNow: action.requirements
|
|
406
512
|
.filter((field) => {
|
|
513
|
+
const conditions = action.requirementConditions?.[field.name];
|
|
514
|
+
if (
|
|
515
|
+
conditions &&
|
|
516
|
+
!requirementPossible(
|
|
517
|
+
conditions,
|
|
518
|
+
action,
|
|
519
|
+
fields,
|
|
520
|
+
typeof options === "object" ? options : {},
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
return false;
|
|
407
524
|
const value = fields[field.name];
|
|
408
525
|
const optional =
|
|
409
526
|
field.optional || (attached && creationFilter.has(field.name));
|
|
@@ -431,7 +548,7 @@ export function validateObjectActionSubject(
|
|
|
431
548
|
action: ObjectActionDiscovery,
|
|
432
549
|
storedFields: Readonly<Record<string, unknown>>,
|
|
433
550
|
submittedFields: Readonly<Record<string, unknown>>,
|
|
434
|
-
options?:
|
|
551
|
+
options?: ObjectActionStateOptions | boolean,
|
|
435
552
|
): readonly SubjectRequirementIssue[] {
|
|
436
553
|
const origin = `${action.instrument}.${action.action}.subject`;
|
|
437
554
|
const issues: SubjectRequirementIssue[] = [];
|
package/src/schema.ts
CHANGED
|
@@ -154,10 +154,43 @@ export const udlObjectFieldSchema = z.discriminatedUnion("type", [
|
|
|
154
154
|
boundedListField,
|
|
155
155
|
]);
|
|
156
156
|
|
|
157
|
+
/** Values are constants or resolved typed paths, never executable strings. */
|
|
158
|
+
export const udlValueSchema = z.union([
|
|
159
|
+
z.strictObject({ literal: scalar }),
|
|
160
|
+
z.strictObject({ field: path }),
|
|
161
|
+
]);
|
|
162
|
+
const value = udlValueSchema;
|
|
163
|
+
|
|
164
|
+
const comparisonRequirement = z.strictObject({
|
|
165
|
+
kind: z.literal("compare"),
|
|
166
|
+
left: value,
|
|
167
|
+
operator: z.enum(["==", "!=", "<", "<=", ">", ">="]),
|
|
168
|
+
right: value,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Alternatives are OR; each invocation path is a conjunction of scoped guards.
|
|
172
|
+
export const udlSubjectConditionsSchema = z
|
|
173
|
+
.array(
|
|
174
|
+
z
|
|
175
|
+
.array(
|
|
176
|
+
z.strictObject({
|
|
177
|
+
instrument: instrumentId,
|
|
178
|
+
action: name,
|
|
179
|
+
guard: comparisonRequirement,
|
|
180
|
+
valueType: z.enum(["money", "date"]).optional(),
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
183
|
+
.min(1)
|
|
184
|
+
.max(32),
|
|
185
|
+
)
|
|
186
|
+
.min(1)
|
|
187
|
+
.max(128);
|
|
188
|
+
|
|
157
189
|
export const udlSubjectRequirementSchema = z.strictObject({
|
|
158
190
|
field: udlObjectFieldSchema,
|
|
159
191
|
// Present only for an authored rename.
|
|
160
192
|
objectField: name.optional(),
|
|
193
|
+
when: udlSubjectConditionsSchema.optional(),
|
|
161
194
|
});
|
|
162
195
|
|
|
163
196
|
export const adapterSubjectSnapshotSchema = z.strictObject({
|
|
@@ -192,6 +225,7 @@ export type AttachmentPartyBinding = z.infer<
|
|
|
192
225
|
>;
|
|
193
226
|
export const udlObjectAttachmentSchema = z.strictObject({
|
|
194
227
|
name,
|
|
228
|
+
parent: name.optional(),
|
|
195
229
|
instrument: instrumentId,
|
|
196
230
|
parties: z.record(name, attachmentPartyBindingSchema),
|
|
197
231
|
});
|
|
@@ -207,12 +241,6 @@ export const udlObjectKindSchema = z.strictObject({
|
|
|
207
241
|
columns: z.array(name).max(8),
|
|
208
242
|
});
|
|
209
243
|
|
|
210
|
-
/** Values are constants or resolved typed paths, never executable strings. */
|
|
211
|
-
export const udlValueSchema = z.union([
|
|
212
|
-
z.strictObject({ literal: scalar }),
|
|
213
|
-
z.strictObject({ field: path }),
|
|
214
|
-
]);
|
|
215
|
-
const value = udlValueSchema;
|
|
216
244
|
const values = z.array(value).min(1).max(256);
|
|
217
245
|
|
|
218
246
|
const selection = z.strictObject({
|
|
@@ -295,13 +323,6 @@ export const udlCalculationSchema = z.discriminatedUnion("op", [
|
|
|
295
323
|
}),
|
|
296
324
|
]);
|
|
297
325
|
|
|
298
|
-
const comparisonRequirement = z.strictObject({
|
|
299
|
-
kind: z.literal("compare"),
|
|
300
|
-
left: value,
|
|
301
|
-
operator: z.enum(["==", "!=", "<", "<=", ">", ">="]),
|
|
302
|
-
right: value,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
326
|
export const udlRequirementSchema = z.discriminatedUnion("kind", [
|
|
306
327
|
z.strictObject({
|
|
307
328
|
kind: z.literal("hours"),
|