@hyperscale0/udl 3.2.0 → 4.1.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/README.md +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +11 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +5 -0
- package/dist/evolution.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts.map +1 -1
- package/dist/instrument-references.js +21 -1
- package/dist/instrument-references.js.map +1 -1
- package/dist/object-contract.d.ts +144 -0
- package/dist/object-contract.d.ts.map +1 -0
- package/dist/object-contract.js +279 -0
- package/dist/object-contract.js.map +1 -0
- package/dist/reporting.d.ts +0 -1
- package/dist/reporting.d.ts.map +1 -1
- package/dist/reporting.js +0 -1
- package/dist/reporting.js.map +1 -1
- package/dist/schema.d.ts +1815 -126
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +100 -33
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +5 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +403 -146
- package/dist/validation.js.map +1 -1
- package/docs/README.md +66 -17
- package/package.json +4 -4
- package/spec/README.md +66 -17
- package/spec/{darb.udl.json → cars.udl.json} +7 -7
- package/spec/udl.schema.json +2876 -6516
- package/src/diagnostics.ts +27 -1
- package/src/evolution.ts +8 -0
- package/src/index.ts +3 -0
- package/src/instrument-references.ts +25 -1
- package/src/object-contract.ts +464 -0
- package/src/reporting.ts +0 -1
- package/src/schema.ts +155 -33
- package/src/validation.ts +635 -174
package/src/diagnostics.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export type UdlIssueCode =
|
|
2
|
+
| "subject_field_conflict"
|
|
3
|
+
| "subject_field_unknown"
|
|
4
|
+
| "subject_requirement_missing"
|
|
5
|
+
| "subject_adapter_unbound"
|
|
6
|
+
| "subject_party_unbound"
|
|
7
|
+
| "party_name_reserved"
|
|
8
|
+
| "party_kind_mismatch"
|
|
9
|
+
| "staff_role_unknown"
|
|
10
|
+
| "product_party_unbound"
|
|
11
|
+
| "product_party_invalid"
|
|
2
12
|
| "UDL1001"
|
|
3
13
|
| "UDL1002"
|
|
4
14
|
| "UDL1003"
|
|
@@ -19,9 +29,25 @@ export interface UdlIssue {
|
|
|
19
29
|
category: string;
|
|
20
30
|
}
|
|
21
31
|
const fixes: Record<UdlIssueCode, string> = {
|
|
32
|
+
subject_field_conflict:
|
|
33
|
+
"Rename the field or use matching types and constraints.",
|
|
34
|
+
subject_field_unknown: "Name a declared subject requirement or object field.",
|
|
35
|
+
subject_requirement_missing: "Supply the required action field.",
|
|
36
|
+
subject_adapter_unbound: "Bind the required adapter declaration.",
|
|
37
|
+
subject_party_unbound:
|
|
38
|
+
"Bind the parameter to a subject role or declared party.",
|
|
39
|
+
party_name_reserved:
|
|
40
|
+
"Choose a party name other than owner, actor or operator.",
|
|
41
|
+
party_kind_mismatch: "Use a business or subject role for money.",
|
|
42
|
+
staff_role_unknown: "Use a registered staff permission role.",
|
|
43
|
+
product_party_unbound:
|
|
44
|
+
"Supply the consumed business in the Build party bindings.",
|
|
45
|
+
product_party_invalid:
|
|
46
|
+
"Bind a business participant in the same tenant and environment.",
|
|
47
|
+
|
|
22
48
|
UDL1001: "Encode the source as UTF-8.",
|
|
23
49
|
UDL1002: "Repair JSON syntax.",
|
|
24
|
-
UDL1003: "Use the UDL
|
|
50
|
+
UDL1003: "Use the UDL 4 typed grammar.",
|
|
25
51
|
UDL1004: "Reduce the declared structure or expansion.",
|
|
26
52
|
UDL2001: "Give each declaration a distinct name.",
|
|
27
53
|
UDL2002: "Repair the named type or declaration.",
|
package/src/evolution.ts
CHANGED
|
@@ -28,6 +28,14 @@ export function diffValidatedUdlEvolution(
|
|
|
28
28
|
for (const [name, party] of Object.entries(live.parties))
|
|
29
29
|
if (stable(party) !== stable(next.parties[name]))
|
|
30
30
|
refuse(`$.parties.${name}`, "a live party cannot change or disappear");
|
|
31
|
+
for (const object of live.objects) {
|
|
32
|
+
const updated = next.objects.find((o) => o.id === object.id);
|
|
33
|
+
if (!updated || stable(object) !== stable(updated))
|
|
34
|
+
refuse(
|
|
35
|
+
`$.objects.${object.id}`,
|
|
36
|
+
"a live object kind cannot change or disappear; recreate development estates",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
31
39
|
for (const instrument of live.instruments) {
|
|
32
40
|
const updated = next.instruments.find((i) => i.id === instrument.id);
|
|
33
41
|
if (!updated || stable(instrument) !== stable(updated))
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,10 @@ export { parseUdl, canonicalizeUdl } from "./parser.js";
|
|
|
3
3
|
export { serializeUdl, canonicalDigest } from "./canonical.js";
|
|
4
4
|
export {
|
|
5
5
|
validateUdl,
|
|
6
|
+
RESERVED_OBJECT_NAMES,
|
|
6
7
|
assertValidUdl,
|
|
7
8
|
resolveField,
|
|
9
|
+
resolveSubjectRequirement,
|
|
8
10
|
UdlError,
|
|
9
11
|
type UdlValidationResult,
|
|
10
12
|
} from "./validation.js";
|
|
@@ -28,3 +30,4 @@ export {
|
|
|
28
30
|
validateReportDefinition,
|
|
29
31
|
reportExpressionTypes,
|
|
30
32
|
} from "./reporting-validation.js";
|
|
33
|
+
export * from "./object-contract.js";
|
|
@@ -52,6 +52,29 @@ export function mapUdlInstrumentReferences(
|
|
|
52
52
|
);
|
|
53
53
|
} else if (object(value)) {
|
|
54
54
|
const properties = object(schema.properties) ? schema.properties : {};
|
|
55
|
+
const discriminator = object(properties.type)
|
|
56
|
+
? properties.type.const
|
|
57
|
+
: undefined;
|
|
58
|
+
if (
|
|
59
|
+
(discriminator === "ref" || discriminator === "list") &&
|
|
60
|
+
value.type === discriminator &&
|
|
61
|
+
value.targetKind === "instrument"
|
|
62
|
+
) {
|
|
63
|
+
if (typeof value.target === "string")
|
|
64
|
+
locations.set(JSON.stringify([...path, "target"]), {
|
|
65
|
+
parent: value,
|
|
66
|
+
key: "target",
|
|
67
|
+
kind: "instrument",
|
|
68
|
+
});
|
|
69
|
+
else if (Array.isArray(value.target))
|
|
70
|
+
value.target.forEach((_, index) =>
|
|
71
|
+
locations.set(JSON.stringify([...path, "target", index]), {
|
|
72
|
+
parent: value.target as unknown[],
|
|
73
|
+
key: index,
|
|
74
|
+
kind: "instrument",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
55
78
|
for (const [name, entry] of Object.entries(value))
|
|
56
79
|
visit(
|
|
57
80
|
properties[name] ?? schema.additionalProperties,
|
|
@@ -81,12 +104,13 @@ export function referencedUdlInstrumentIds(
|
|
|
81
104
|
const found = new Set<string>();
|
|
82
105
|
mapUdlInstrumentReferences(
|
|
83
106
|
{
|
|
84
|
-
udl:
|
|
107
|
+
udl: 4,
|
|
85
108
|
version: 1,
|
|
86
109
|
product: "references",
|
|
87
110
|
title: "References",
|
|
88
111
|
currency: "SAR",
|
|
89
112
|
parties: {},
|
|
113
|
+
objects: [],
|
|
90
114
|
instruments: [instrument],
|
|
91
115
|
},
|
|
92
116
|
(id) => {
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import {
|
|
3
|
+
udlExternalIdSchema,
|
|
4
|
+
udlObjectIdSchema,
|
|
5
|
+
type UdlDocument,
|
|
6
|
+
type UdlField,
|
|
7
|
+
type UdlObjectField,
|
|
8
|
+
type UdlObjectKind,
|
|
9
|
+
} from "./schema.js";
|
|
10
|
+
|
|
11
|
+
export const requestAttributionSchema = z.union([
|
|
12
|
+
z.strictObject({
|
|
13
|
+
kind: z.literal("human"),
|
|
14
|
+
principalId: z.string().min(1),
|
|
15
|
+
humanId: z.string().min(1),
|
|
16
|
+
source: z.enum(["staff_session", "customer_session"]),
|
|
17
|
+
}),
|
|
18
|
+
z.strictObject({
|
|
19
|
+
kind: z.literal("machine"),
|
|
20
|
+
principalId: z.string().min(1),
|
|
21
|
+
source: z.enum(["api_key", "system"]),
|
|
22
|
+
}),
|
|
23
|
+
z.strictObject({
|
|
24
|
+
kind: z.literal("machine"),
|
|
25
|
+
principalId: z.string().min(1),
|
|
26
|
+
humanId: z.string().min(1),
|
|
27
|
+
source: z.literal("api_key_delegate"),
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
export type RequestAttribution = z.infer<typeof requestAttributionSchema>;
|
|
31
|
+
|
|
32
|
+
export type JsonSchemaDocument = z.core.JSONSchema.BaseSchema;
|
|
33
|
+
|
|
34
|
+
export interface ObjectKindDiscovery {
|
|
35
|
+
creation: true;
|
|
36
|
+
kind: string;
|
|
37
|
+
title: string;
|
|
38
|
+
fields: readonly UdlObjectField[];
|
|
39
|
+
authoredFields: readonly string[];
|
|
40
|
+
columns: readonly string[];
|
|
41
|
+
createSchema: JsonSchemaDocument;
|
|
42
|
+
actions: readonly ObjectActionDiscovery[];
|
|
43
|
+
}
|
|
44
|
+
export type ObjectActionTarget =
|
|
45
|
+
| { kind: "attachment"; attachment: string }
|
|
46
|
+
| { kind: "instance"; attachment: string; instanceId: string };
|
|
47
|
+
export interface ObjectActionContext {
|
|
48
|
+
productBuildId: string;
|
|
49
|
+
digest: string;
|
|
50
|
+
target: ObjectActionTarget;
|
|
51
|
+
}
|
|
52
|
+
export interface ObjectActionDiscovery extends ObjectActionContext {
|
|
53
|
+
name: string;
|
|
54
|
+
title: string;
|
|
55
|
+
instrument: string;
|
|
56
|
+
action: string;
|
|
57
|
+
requirements: readonly UdlObjectField[];
|
|
58
|
+
creationRequirements?: readonly UdlObjectField[];
|
|
59
|
+
creationOnlyNames?: readonly string[];
|
|
60
|
+
inputSchema: JsonSchemaDocument;
|
|
61
|
+
fieldsSchema: JsonSchemaDocument;
|
|
62
|
+
availability:
|
|
63
|
+
| { status: "available" }
|
|
64
|
+
| { status: "unavailable"; code: string; message: string };
|
|
65
|
+
}
|
|
66
|
+
export type EvidenceState =
|
|
67
|
+
| "missing"
|
|
68
|
+
| "pending"
|
|
69
|
+
| "satisfied"
|
|
70
|
+
| "refused"
|
|
71
|
+
| "stale";
|
|
72
|
+
|
|
73
|
+
export interface ObjectEvidenceSummary {
|
|
74
|
+
requirementId: string;
|
|
75
|
+
subjectId: string;
|
|
76
|
+
protectedInputDigest: string;
|
|
77
|
+
adapter: string;
|
|
78
|
+
declarationDigest: string;
|
|
79
|
+
productBuildId: string;
|
|
80
|
+
state: EvidenceState;
|
|
81
|
+
reference?: string;
|
|
82
|
+
observedAt?: string;
|
|
83
|
+
reasonCode?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ObjectActionState extends ObjectActionDiscovery {
|
|
87
|
+
requiredNow: readonly string[];
|
|
88
|
+
evidence?: readonly ObjectEvidenceSummary[];
|
|
89
|
+
}
|
|
90
|
+
/** GET /v1/products/{productId}/objects. Core supplies the frozen Build identity. */
|
|
91
|
+
export type RetainedObjectKind = Pick<
|
|
92
|
+
ObjectKindDiscovery,
|
|
93
|
+
"kind" | "title" | "fields" | "authoredFields" | "columns"
|
|
94
|
+
> & {
|
|
95
|
+
productBuildId: string;
|
|
96
|
+
digest: string;
|
|
97
|
+
creation: false;
|
|
98
|
+
};
|
|
99
|
+
export interface ObjectDiscovery {
|
|
100
|
+
retainedKinds: readonly RetainedObjectKind[];
|
|
101
|
+
productBuildId: string;
|
|
102
|
+
digest: string;
|
|
103
|
+
kinds: readonly ObjectKindDiscovery[];
|
|
104
|
+
}
|
|
105
|
+
export interface ObjectActionsResponse {
|
|
106
|
+
objectId: string;
|
|
107
|
+
revision: number;
|
|
108
|
+
actions: readonly ObjectActionState[];
|
|
109
|
+
evidence: readonly ObjectEvidenceSummary[];
|
|
110
|
+
}
|
|
111
|
+
export interface ObjectInstance {
|
|
112
|
+
objectId: string;
|
|
113
|
+
kind: string;
|
|
114
|
+
revision: number;
|
|
115
|
+
externalId?: string;
|
|
116
|
+
fields: Record<string, unknown>;
|
|
117
|
+
productBuildId: string;
|
|
118
|
+
}
|
|
119
|
+
export interface ObjectListResponse {
|
|
120
|
+
items: ObjectInstance[];
|
|
121
|
+
nextCursor?: string;
|
|
122
|
+
}
|
|
123
|
+
export interface ObjectActionResponse {
|
|
124
|
+
object: ObjectInstance;
|
|
125
|
+
outcome: { instrument: string; instanceId: string; state: string } | null;
|
|
126
|
+
evidence: readonly ObjectEvidenceSummary[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const amount = z.string().regex(/^(0|[1-9][0-9]{0,17})$/);
|
|
130
|
+
const text = z.string().min(1).max(2048);
|
|
131
|
+
|
|
132
|
+
/** Values, rather than declarations. The same validator feeds forms and admission. */
|
|
133
|
+
export function udlFieldValueSchema(field: UdlField): z.ZodType {
|
|
134
|
+
let schema: z.ZodType;
|
|
135
|
+
switch (field.type) {
|
|
136
|
+
case "money":
|
|
137
|
+
schema = amount
|
|
138
|
+
.refine(
|
|
139
|
+
(value) =>
|
|
140
|
+
/^(0|[1-9][0-9]{0,17})$/.test(value) &&
|
|
141
|
+
(field.minimum === undefined ||
|
|
142
|
+
BigInt(value) >= BigInt(field.minimum)) &&
|
|
143
|
+
(field.maximum === undefined ||
|
|
144
|
+
BigInt(value) <= BigInt(field.maximum)),
|
|
145
|
+
"Money is outside its declared bounds",
|
|
146
|
+
)
|
|
147
|
+
.meta({
|
|
148
|
+
...(field.minimum !== undefined
|
|
149
|
+
? { "x-udl-minimum": field.minimum }
|
|
150
|
+
: {}),
|
|
151
|
+
...(field.maximum !== undefined
|
|
152
|
+
? { "x-udl-maximum": field.maximum }
|
|
153
|
+
: {}),
|
|
154
|
+
});
|
|
155
|
+
break;
|
|
156
|
+
case "account":
|
|
157
|
+
schema = text;
|
|
158
|
+
break;
|
|
159
|
+
case "ref":
|
|
160
|
+
schema = field.targetKind === "object" ? udlObjectIdSchema : text;
|
|
161
|
+
break;
|
|
162
|
+
case "date":
|
|
163
|
+
schema = z.iso.datetime({ offset: true });
|
|
164
|
+
break;
|
|
165
|
+
case "duration":
|
|
166
|
+
schema = z.number().int().safe().positive();
|
|
167
|
+
break;
|
|
168
|
+
case "text": {
|
|
169
|
+
let value = z
|
|
170
|
+
.string()
|
|
171
|
+
.min(field.minLength ?? 1)
|
|
172
|
+
.max(field.maxLength ?? 2048);
|
|
173
|
+
if (field.pattern) value = value.regex(new RegExp(field.pattern));
|
|
174
|
+
schema = value;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "integer": {
|
|
178
|
+
let value = z.number().int().safe();
|
|
179
|
+
if (field.minimum !== undefined) value = value.min(field.minimum);
|
|
180
|
+
if (field.maximum !== undefined) value = value.max(field.maximum);
|
|
181
|
+
schema = value;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "percent":
|
|
185
|
+
schema = z.number().int().min(0).max(10000);
|
|
186
|
+
break;
|
|
187
|
+
case "boolean":
|
|
188
|
+
schema = z.boolean();
|
|
189
|
+
break;
|
|
190
|
+
case "enum":
|
|
191
|
+
schema = z.enum(field.values);
|
|
192
|
+
break;
|
|
193
|
+
case "list": {
|
|
194
|
+
const item =
|
|
195
|
+
field.item === "money"
|
|
196
|
+
? amount
|
|
197
|
+
: field.item === "date"
|
|
198
|
+
? z.iso.datetime({ offset: true })
|
|
199
|
+
: field.item === "integer"
|
|
200
|
+
? z.number().int().safe()
|
|
201
|
+
: field.item === "ref" && field.targetKind === "object"
|
|
202
|
+
? udlObjectIdSchema
|
|
203
|
+
: text;
|
|
204
|
+
schema = z.array(item).max(field.maxItems);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if ("value" in field && field.value !== undefined)
|
|
209
|
+
schema = z.literal(field.value);
|
|
210
|
+
if (field.description) schema = schema.describe(field.description);
|
|
211
|
+
return schema;
|
|
212
|
+
}
|
|
213
|
+
/** Creation accepts the full metadata union, with every value optional. */
|
|
214
|
+
export function objectCreateSchema(kind: UdlObjectKind): z.ZodObject {
|
|
215
|
+
return z.strictObject({
|
|
216
|
+
externalId: udlExternalIdSchema.optional(),
|
|
217
|
+
fields: z
|
|
218
|
+
.strictObject(
|
|
219
|
+
Object.fromEntries(
|
|
220
|
+
kind.fields.map((field) => [
|
|
221
|
+
field.name,
|
|
222
|
+
udlFieldValueSchema(field).optional(),
|
|
223
|
+
]),
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
.optional(),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Canonical UDL supplies meaning; core adds authority, lifecycle and readiness. */
|
|
231
|
+
export function projectObjectDiscovery(
|
|
232
|
+
document: UdlDocument,
|
|
233
|
+
build: Pick<ObjectDiscovery, "productBuildId" | "digest">,
|
|
234
|
+
): ObjectDiscovery {
|
|
235
|
+
return {
|
|
236
|
+
...build,
|
|
237
|
+
retainedKinds: [],
|
|
238
|
+
kinds: document.objects.map((kind) => ({
|
|
239
|
+
creation: true,
|
|
240
|
+
kind: kind.id,
|
|
241
|
+
title: kind.title,
|
|
242
|
+
fields: kind.fields,
|
|
243
|
+
authoredFields: kind.authoredFields,
|
|
244
|
+
columns: kind.columns,
|
|
245
|
+
createSchema: z.toJSONSchema(objectCreateSchema(kind), { io: "input" }),
|
|
246
|
+
actions: document.instruments
|
|
247
|
+
.filter((instrument) => instrument.subject === kind.id)
|
|
248
|
+
.flatMap((instrument) => {
|
|
249
|
+
const createHidden =
|
|
250
|
+
!!instrument.actions.create &&
|
|
251
|
+
!instrument.actions.create.publicAction;
|
|
252
|
+
const attachmentRefListFields = instrument.fields.filter(
|
|
253
|
+
(field) =>
|
|
254
|
+
field.type === "list" &&
|
|
255
|
+
field.item === "ref" &&
|
|
256
|
+
field.targetKind !== "object",
|
|
257
|
+
);
|
|
258
|
+
return instrument.actionOrder.flatMap((name) => {
|
|
259
|
+
const action = instrument.actions[name]!;
|
|
260
|
+
if (!action.publicAction) return [];
|
|
261
|
+
const transition = instrument.lifecycle.transitions[name];
|
|
262
|
+
const canStartFromInitial = Boolean(
|
|
263
|
+
transition?.from.includes(instrument.lifecycle.initial),
|
|
264
|
+
);
|
|
265
|
+
const isCreationAction =
|
|
266
|
+
name === "create" || (createHidden && canStartFromInitial);
|
|
267
|
+
|
|
268
|
+
const rawCreateReqs: {
|
|
269
|
+
field: UdlField;
|
|
270
|
+
objectField?: string | undefined;
|
|
271
|
+
}[] = isCreationAction
|
|
272
|
+
? [
|
|
273
|
+
...(instrument.actions.create?.subject?.requirements ?? []),
|
|
274
|
+
...attachmentRefListFields.map((field) => ({ field })),
|
|
275
|
+
]
|
|
276
|
+
: [];
|
|
277
|
+
const actionReqs = action.subject?.requirements ?? [];
|
|
278
|
+
const actionReqNames = new Set(
|
|
279
|
+
actionReqs.map((r) => r.objectField ?? r.field.name),
|
|
280
|
+
);
|
|
281
|
+
const mergedReqs: {
|
|
282
|
+
field: UdlField;
|
|
283
|
+
objectField?: string | undefined;
|
|
284
|
+
}[] = [...actionReqs];
|
|
285
|
+
for (const req of rawCreateReqs) {
|
|
286
|
+
const reqName = req.objectField ?? req.field.name;
|
|
287
|
+
if (
|
|
288
|
+
!mergedReqs.some(
|
|
289
|
+
(r) => (r.objectField ?? r.field.name) === reqName,
|
|
290
|
+
)
|
|
291
|
+
) {
|
|
292
|
+
mergedReqs.push(req);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const unbound = [
|
|
296
|
+
...(action.subject?.adapters ?? []),
|
|
297
|
+
...(isCreationAction
|
|
298
|
+
? (instrument.actions.create?.subject?.adapters ?? [])
|
|
299
|
+
: []),
|
|
300
|
+
].find((adapter) => adapter.snapshot === null);
|
|
301
|
+
const seenCreation = new Set<string>();
|
|
302
|
+
const creationRequirements: UdlObjectField[] = [];
|
|
303
|
+
for (const requirement of rawCreateReqs) {
|
|
304
|
+
const name = requirement.objectField ?? requirement.field.name;
|
|
305
|
+
if (!seenCreation.has(name)) {
|
|
306
|
+
seenCreation.add(name);
|
|
307
|
+
creationRequirements.push({
|
|
308
|
+
...requirement.field,
|
|
309
|
+
name,
|
|
310
|
+
} as UdlObjectField);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const creationOnlyNames = creationRequirements
|
|
314
|
+
.map((field) => field.name)
|
|
315
|
+
.filter((name) => !actionReqNames.has(name));
|
|
316
|
+
return [
|
|
317
|
+
{
|
|
318
|
+
...build,
|
|
319
|
+
target: {
|
|
320
|
+
kind: "attachment" as const,
|
|
321
|
+
attachment: kind.attachments.find(
|
|
322
|
+
(attachment) => attachment.instrument === instrument.id,
|
|
323
|
+
)!.name,
|
|
324
|
+
},
|
|
325
|
+
name: action.publicAction,
|
|
326
|
+
title: action.summary,
|
|
327
|
+
instrument: instrument.id,
|
|
328
|
+
action: name,
|
|
329
|
+
requirements: mergedReqs.map(
|
|
330
|
+
(requirement) =>
|
|
331
|
+
({
|
|
332
|
+
...requirement.field,
|
|
333
|
+
name: requirement.objectField ?? requirement.field.name,
|
|
334
|
+
}) as UdlObjectField,
|
|
335
|
+
),
|
|
336
|
+
...(creationRequirements.length > 0
|
|
337
|
+
? { creationRequirements }
|
|
338
|
+
: {}),
|
|
339
|
+
...(creationRequirements.length > 0
|
|
340
|
+
? { creationOnlyNames }
|
|
341
|
+
: {}),
|
|
342
|
+
fieldsSchema: z.toJSONSchema(
|
|
343
|
+
z.strictObject(
|
|
344
|
+
Object.fromEntries(
|
|
345
|
+
mergedReqs.map((requirement) => [
|
|
346
|
+
requirement.objectField ?? requirement.field.name,
|
|
347
|
+
udlFieldValueSchema(requirement.field).optional(),
|
|
348
|
+
]),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
{ io: "input" },
|
|
352
|
+
),
|
|
353
|
+
inputSchema: z.toJSONSchema(
|
|
354
|
+
z.strictObject(
|
|
355
|
+
Object.fromEntries(
|
|
356
|
+
action.input.map((field) => [
|
|
357
|
+
field.name,
|
|
358
|
+
field.optional
|
|
359
|
+
? udlFieldValueSchema(field).optional()
|
|
360
|
+
: udlFieldValueSchema(field),
|
|
361
|
+
]),
|
|
362
|
+
),
|
|
363
|
+
),
|
|
364
|
+
{ io: "input" },
|
|
365
|
+
),
|
|
366
|
+
availability: unbound
|
|
367
|
+
? {
|
|
368
|
+
status: "unavailable" as const,
|
|
369
|
+
code: "subject_adapter_unbound",
|
|
370
|
+
message: `Adapter declaration ${unbound.binding} is unavailable`,
|
|
371
|
+
}
|
|
372
|
+
: { status: "available" as const },
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
});
|
|
376
|
+
}),
|
|
377
|
+
})),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Object-specific values determine missing fields; global discovery cannot. */
|
|
382
|
+
export function objectActionState(
|
|
383
|
+
action: ObjectActionDiscovery,
|
|
384
|
+
fields: Readonly<Record<string, unknown>>,
|
|
385
|
+
options?: { attached?: boolean } | boolean,
|
|
386
|
+
): ObjectActionState {
|
|
387
|
+
const attached =
|
|
388
|
+
typeof options === "boolean" ? options : (options?.attached ?? false);
|
|
389
|
+
const creationFilter = new Set(action.creationOnlyNames ?? []);
|
|
390
|
+
return {
|
|
391
|
+
...action,
|
|
392
|
+
requiredNow: action.requirements
|
|
393
|
+
.filter((field) => {
|
|
394
|
+
const value = fields[field.name];
|
|
395
|
+
const optional =
|
|
396
|
+
field.optional || (attached && creationFilter.has(field.name));
|
|
397
|
+
return (
|
|
398
|
+
!(optional && value === undefined) &&
|
|
399
|
+
!udlFieldValueSchema(field).safeParse(value).success
|
|
400
|
+
);
|
|
401
|
+
})
|
|
402
|
+
.map((field) => field.name),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
export interface SubjectRequirementIssue {
|
|
406
|
+
code:
|
|
407
|
+
| "subject_requirement_missing"
|
|
408
|
+
| "subject_field_unknown"
|
|
409
|
+
| "subject_adapter_unbound";
|
|
410
|
+
action: string;
|
|
411
|
+
field?: string;
|
|
412
|
+
origin: string;
|
|
413
|
+
message: string;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Check merged metadata before dispatch. Runtime guards add their active child requirements. */
|
|
417
|
+
export function validateObjectActionSubject(
|
|
418
|
+
action: ObjectActionDiscovery,
|
|
419
|
+
storedFields: Readonly<Record<string, unknown>>,
|
|
420
|
+
submittedFields: Readonly<Record<string, unknown>>,
|
|
421
|
+
options?: { attached?: boolean } | boolean,
|
|
422
|
+
): readonly SubjectRequirementIssue[] {
|
|
423
|
+
const origin = `${action.instrument}.${action.action}.subject`;
|
|
424
|
+
const issues: SubjectRequirementIssue[] = [];
|
|
425
|
+
if (
|
|
426
|
+
action.availability.status === "unavailable" &&
|
|
427
|
+
action.availability.code === "subject_adapter_unbound"
|
|
428
|
+
) {
|
|
429
|
+
issues.push({
|
|
430
|
+
code: "subject_adapter_unbound",
|
|
431
|
+
action: action.name,
|
|
432
|
+
origin,
|
|
433
|
+
message: action.availability.message,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const admitted = new Set(action.requirements.map((field) => field.name));
|
|
437
|
+
for (const field of Object.keys(submittedFields)) {
|
|
438
|
+
if (!admitted.has(field))
|
|
439
|
+
issues.push({
|
|
440
|
+
code: "subject_field_unknown",
|
|
441
|
+
action: action.name,
|
|
442
|
+
field,
|
|
443
|
+
origin,
|
|
444
|
+
message: `${field} is not admitted by ${action.name}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
for (const field of objectActionState(
|
|
448
|
+
action,
|
|
449
|
+
{
|
|
450
|
+
...storedFields,
|
|
451
|
+
...submittedFields,
|
|
452
|
+
},
|
|
453
|
+
options,
|
|
454
|
+
).requiredNow) {
|
|
455
|
+
issues.push({
|
|
456
|
+
code: "subject_requirement_missing",
|
|
457
|
+
action: action.name,
|
|
458
|
+
field,
|
|
459
|
+
origin: `${origin}.${field}`,
|
|
460
|
+
message: `${action.name} requires a valid ${field}`,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return issues;
|
|
464
|
+
}
|
package/src/reporting.ts
CHANGED
|
@@ -171,7 +171,6 @@ export const reportDefinitionSchema = z.strictObject({
|
|
|
171
171
|
request: z.array(name).min(1).max(64),
|
|
172
172
|
read: z.array(name).min(1).max(64),
|
|
173
173
|
release: z.array(name).max(64),
|
|
174
|
-
independentApproval: z.literal(true),
|
|
175
174
|
retention: z.strictObject({ policy: name, years: integer.min(1).max(100) }),
|
|
176
175
|
}),
|
|
177
176
|
});
|