@metaobjectsdev/metadata 0.5.0-rc.3 → 0.6.0-rc.1
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/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +66 -11
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/documentation/doc-constants.d.ts +11 -0
- package/dist/core/documentation/doc-constants.d.ts.map +1 -0
- package/dist/core/documentation/doc-constants.js +20 -0
- package/dist/core/documentation/doc-constants.js.map +1 -0
- package/dist/core/documentation/doc-provider.d.ts +3 -0
- package/dist/core/documentation/doc-provider.d.ts.map +1 -0
- package/dist/core/documentation/doc-provider.js +10 -0
- package/dist/core/documentation/doc-provider.js.map +1 -0
- package/dist/core/documentation/doc-schema.d.ts +8 -0
- package/dist/core/documentation/doc-schema.d.ts.map +1 -0
- package/dist/core/documentation/doc-schema.js +53 -0
- package/dist/core/documentation/doc-schema.js.map +1 -0
- package/dist/core/field/field-constants.d.ts +25 -1
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +32 -1
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-schema.d.ts +2 -0
- package/dist/core/field/field-schema.d.ts.map +1 -1
- package/dist/core/field/field-schema.js +20 -2
- package/dist/core/field/field-schema.js.map +1 -1
- package/dist/core/field/meta-field.d.ts +2 -1
- package/dist/core/field/meta-field.d.ts.map +1 -1
- package/dist/core/field/meta-field.js +6 -4
- package/dist/core/field/meta-field.js.map +1 -1
- package/dist/core/object/meta-object.d.ts.map +1 -1
- package/dist/core/object/meta-object.js +6 -5
- package/dist/core/object/meta-object.js.map +1 -1
- package/dist/core/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +9 -2
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/core/relationship/meta-relationship.d.ts +4 -0
- package/dist/core/relationship/meta-relationship.d.ts.map +1 -1
- package/dist/core/relationship/meta-relationship.js +11 -1
- package/dist/core/relationship/meta-relationship.js.map +1 -1
- package/dist/core/relationship/relationship-constants.d.ts +9 -0
- package/dist/core/relationship/relationship-constants.d.ts.map +1 -1
- package/dist/core/relationship/relationship-constants.js +15 -0
- package/dist/core/relationship/relationship-constants.js.map +1 -1
- package/dist/core/relationship/relationship-schema.d.ts.map +1 -1
- package/dist/core/relationship/relationship-schema.js +15 -1
- package/dist/core/relationship/relationship-schema.js.map +1 -1
- package/dist/core/yaml-desugar.d.ts +8 -1
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +166 -15
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +28 -11
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +12 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +2 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +69 -3
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts +3 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +14 -12
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +91 -48
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/db/db-constants.d.ts +2 -2
- package/dist/persistence/db/db-constants.d.ts.map +1 -1
- package/dist/persistence/db/db-constants.js +2 -2
- package/dist/persistence/db/db-constants.js.map +1 -1
- package/dist/persistence/db/db-provider.d.ts.map +1 -1
- package/dist/persistence/db/db-provider.js +10 -13
- package/dist/persistence/db/db-provider.js.map +1 -1
- package/dist/persistence/db/db-schema.d.ts +2 -4
- package/dist/persistence/db/db-schema.d.ts.map +1 -1
- package/dist/persistence/db/db-schema.js +5 -13
- package/dist/persistence/db/db-schema.js.map +1 -1
- package/dist/persistence/origin/meta-origin.d.ts +10 -0
- package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
- package/dist/persistence/origin/meta-origin.js +14 -1
- package/dist/persistence/origin/meta-origin.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts +3 -1
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +6 -0
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-schema.d.ts +1 -1
- package/dist/persistence/origin/origin-schema.d.ts.map +1 -1
- package/dist/persistence/origin/origin-schema.js +12 -2
- package/dist/persistence/origin/origin-schema.js.map +1 -1
- package/dist/persistence/source/meta-source.d.ts +11 -9
- package/dist/persistence/source/meta-source.d.ts.map +1 -1
- package/dist/persistence/source/meta-source.js +23 -15
- package/dist/persistence/source/meta-source.js.map +1 -1
- package/dist/persistence/source/source-constants.d.ts +32 -11
- package/dist/persistence/source/source-constants.d.ts.map +1 -1
- package/dist/persistence/source/source-constants.js +55 -16
- package/dist/persistence/source/source-constants.js.map +1 -1
- package/dist/persistence/source/source-schema.d.ts +4 -0
- package/dist/persistence/source/source-schema.d.ts.map +1 -0
- package/dist/persistence/source/source-schema.js +37 -0
- package/dist/persistence/source/source-schema.js.map +1 -0
- package/dist/persistence/source/validate-source-roles.d.ts +12 -0
- package/dist/persistence/source/validate-source-roles.d.ts.map +1 -0
- package/dist/persistence/source/validate-source-roles.js +38 -0
- package/dist/persistence/source/validate-source-roles.js.map +1 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +26 -0
- package/dist/registry.js.map +1 -1
- package/dist/shared/base-types.d.ts +2 -1
- package/dist/shared/base-types.d.ts.map +1 -1
- package/dist/shared/base-types.js +5 -2
- package/dist/shared/base-types.js.map +1 -1
- package/dist/template/meta-template.d.ts +13 -0
- package/dist/template/meta-template.d.ts.map +1 -0
- package/dist/template/meta-template.js +13 -0
- package/dist/template/meta-template.js.map +1 -0
- package/dist/template/template-constants.d.ts +17 -0
- package/dist/template/template-constants.d.ts.map +1 -0
- package/dist/template/template-constants.js +46 -0
- package/dist/template/template-constants.js.map +1 -0
- package/dist/template/template-schema.d.ts +3 -0
- package/dist/template/template-schema.d.ts.map +1 -0
- package/dist/template/template-schema.js +84 -0
- package/dist/template/template-schema.js.map +1 -0
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +89 -9
- package/src/core/documentation/doc-constants.ts +22 -0
- package/src/core/documentation/doc-provider.ts +12 -0
- package/src/core/documentation/doc-schema.ts +64 -0
- package/src/core/field/field-constants.ts +41 -1
- package/src/core/field/field-schema.ts +25 -0
- package/src/core/field/meta-field.ts +6 -3
- package/src/core/object/meta-object.ts +6 -6
- package/src/core/parser-yaml.ts +11 -3
- package/src/core/relationship/meta-relationship.ts +14 -0
- package/src/core/relationship/relationship-constants.ts +20 -0
- package/src/core/relationship/relationship-schema.ts +18 -0
- package/src/core/yaml-desugar.ts +206 -24
- package/src/core-types.ts +31 -9
- package/src/errors.ts +21 -0
- package/src/index.ts +7 -0
- package/src/loader/meta-data-loader.ts +15 -1
- package/src/loader/validation-passes.ts +98 -1
- package/src/naming.ts +15 -13
- package/src/parser-core.ts +119 -73
- package/src/persistence/db/db-constants.ts +2 -2
- package/src/persistence/db/db-provider.ts +10 -16
- package/src/persistence/db/db-schema.ts +5 -15
- package/src/persistence/origin/meta-origin.ts +15 -0
- package/src/persistence/origin/origin-constants.ts +7 -0
- package/src/persistence/origin/origin-schema.ts +15 -1
- package/src/persistence/source/meta-source.ts +30 -17
- package/src/persistence/source/source-constants.ts +66 -17
- package/src/persistence/source/source-schema.ts +54 -0
- package/src/persistence/source/validate-source-roles.ts +53 -0
- package/src/registry.ts +31 -0
- package/src/shared/base-types.ts +5 -2
- package/src/template/meta-template.ts +12 -0
- package/src/template/template-constants.ts +53 -0
- package/src/template/template-schema.ts +106 -0
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// warnings. No loader state is read or written — these are pure functions.
|
|
5
5
|
//
|
|
6
6
|
// Exported: validateDataGridSortFields, validateFilterableHasIndex,
|
|
7
|
-
// validateOriginPaths
|
|
7
|
+
// validateOriginPaths, validateDataGridFilterValues,
|
|
8
|
+
// validateFieldObjectStorage (called by MetaDataLoader.load() in order).
|
|
8
9
|
// Private: _findObject, _findField, _findRelationship,
|
|
9
10
|
// _validateFromPath, _validateViaPath (helpers, not exported).
|
|
10
11
|
|
|
@@ -17,7 +18,12 @@ import {
|
|
|
17
18
|
TYPE_IDENTITY,
|
|
18
19
|
TYPE_ORIGIN,
|
|
19
20
|
TYPE_RELATIONSHIP,
|
|
21
|
+
TYPE_TEMPLATE,
|
|
20
22
|
} from "../shared/base-types.js";
|
|
23
|
+
import {
|
|
24
|
+
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
25
|
+
TEMPLATE_ATTR_REQUIRED_SLOTS,
|
|
26
|
+
} from "../template/template-constants.js";
|
|
21
27
|
import {
|
|
22
28
|
LAYOUT_SUBTYPE_DATA_GRID,
|
|
23
29
|
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
|
|
@@ -25,6 +31,9 @@ import {
|
|
|
25
31
|
} from "../presentation/layout/layout-constants.js";
|
|
26
32
|
import {
|
|
27
33
|
FIELD_ATTR_FILTERABLE,
|
|
34
|
+
FIELD_ATTR_OBJECT_REF,
|
|
35
|
+
FIELD_ATTR_STORAGE,
|
|
36
|
+
STORAGE_FLATTENED,
|
|
28
37
|
} from "../core/field/field-constants.js";
|
|
29
38
|
import { FIELD_ATTR_DB_INDEXED } from "../persistence/db/db-constants.js";
|
|
30
39
|
import { IDENTITY_ATTR_FIELDS } from "../core/identity/identity-constants.js";
|
|
@@ -72,6 +81,52 @@ export function validateDataGridSortFields(root: MetaData): ParseError[] {
|
|
|
72
81
|
return errors;
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// template.* @payloadRef / @requiredSlots validation (FR-004 Plan #3, T2)
|
|
86
|
+
//
|
|
87
|
+
// Metadata-internal half of `verify` — runs at load time (no provider needed):
|
|
88
|
+
// - @payloadRef resolves to a known object (the payload view-object)
|
|
89
|
+
// - every @requiredSlots entry is a real field on that payload
|
|
90
|
+
// The template-text half (every {{var}} resolves to a payload field) needs the
|
|
91
|
+
// external template text via a provider, so it lives in the build-time
|
|
92
|
+
// `meta verify` step, not here.
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
96
|
+
const errors: ParseError[] = [];
|
|
97
|
+
for (const tmpl of root.ownChildren().filter((c) => c.type === TYPE_TEMPLATE)) {
|
|
98
|
+
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
99
|
+
if (typeof payloadRef !== "string") continue; // absence handled by the required-attr schema check
|
|
100
|
+
const payload = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === payloadRef);
|
|
101
|
+
if (!payload) {
|
|
102
|
+
errors.push(
|
|
103
|
+
new ParseError(
|
|
104
|
+
`template "${tmpl.name}" @payloadRef "${payloadRef}" does not resolve to a known object in this model`,
|
|
105
|
+
{ code: "ERR_INVALID_TEMPLATE" },
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const fieldNames = new Set(
|
|
111
|
+
payload.children().filter((c) => c.type === TYPE_FIELD).map((f) => f.name),
|
|
112
|
+
);
|
|
113
|
+
const slots = tmpl.ownAttr(TEMPLATE_ATTR_REQUIRED_SLOTS);
|
|
114
|
+
const slotList = Array.isArray(slots) ? slots : typeof slots === "string" ? [slots] : [];
|
|
115
|
+
for (const slot of slotList) {
|
|
116
|
+
if (typeof slot === "string" && !fieldNames.has(slot)) {
|
|
117
|
+
errors.push(
|
|
118
|
+
new ParseError(
|
|
119
|
+
`template "${tmpl.name}" @requiredSlots "${slot}" is not a field on payload "${payloadRef}". ` +
|
|
120
|
+
`Available fields: ${[...fieldNames].join(", ")}`,
|
|
121
|
+
{ code: "ERR_INVALID_TEMPLATE" },
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
|
|
75
130
|
// ---------------------------------------------------------------------------
|
|
76
131
|
// @filterable without index validation
|
|
77
132
|
// ---------------------------------------------------------------------------
|
|
@@ -291,6 +346,48 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
291
346
|
return errors;
|
|
292
347
|
}
|
|
293
348
|
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// @storage cross-attribute validation
|
|
351
|
+
//
|
|
352
|
+
// Rules:
|
|
353
|
+
// 1. @storage requires @objectRef to be present (storage is meaningless
|
|
354
|
+
// without a referenced object type).
|
|
355
|
+
// 2. @storage "flattened" requires isArray to be absent or false (cannot
|
|
356
|
+
// flatten a variable-length array into a fixed column set).
|
|
357
|
+
//
|
|
358
|
+
// Only field.object nodes carry @storage in practice, but the check is applied
|
|
359
|
+
// to every field node that has @storage set — matching the permissive "check
|
|
360
|
+
// what's there" model used by the other validation passes.
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
export function validateFieldObjectStorage(root: MetaData): ParseError[] {
|
|
364
|
+
const errors: ParseError[] = [];
|
|
365
|
+
for (const obj of root.ownChildren().filter((c) => c.type === TYPE_OBJECT)) {
|
|
366
|
+
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
367
|
+
const storage = field.ownAttr(FIELD_ATTR_STORAGE);
|
|
368
|
+
if (storage === undefined || storage === null) continue;
|
|
369
|
+
const objectRef = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
370
|
+
if (typeof objectRef !== "string" || objectRef.length === 0) {
|
|
371
|
+
errors.push(
|
|
372
|
+
new ParseError(
|
|
373
|
+
`field "${obj.name}.${field.name}" sets @storage but has no @objectRef`,
|
|
374
|
+
{ code: "ERR_STORAGE_WITHOUT_OBJECT_REF" },
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (storage === STORAGE_FLATTENED && field.isArray === true) {
|
|
379
|
+
errors.push(
|
|
380
|
+
new ParseError(
|
|
381
|
+
`field "${obj.name}.${field.name}" sets @storage "flattened" with isArray=true; flattened storage requires a single nested value`,
|
|
382
|
+
{ code: "ERR_STORAGE_FLATTENED_ARRAY" },
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return errors;
|
|
389
|
+
}
|
|
390
|
+
|
|
294
391
|
// ---------------------------------------------------------------------------
|
|
295
392
|
// Layout dataGrid @filter value validation
|
|
296
393
|
//
|
package/src/naming.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { MetaData } from "./shared/meta-data.js";
|
|
2
|
-
import { TYPE_FIELD
|
|
2
|
+
import { TYPE_FIELD } from "./shared/base-types.js";
|
|
3
3
|
import { PACKAGE_SEPARATOR } from "./shared/structural.js";
|
|
4
|
-
import {
|
|
4
|
+
import { FIELD_ATTR_COLUMN } from "./persistence/db/db-constants.js";
|
|
5
5
|
import {
|
|
6
|
-
SOURCE_SUBTYPE_DB_TABLE,
|
|
7
|
-
SOURCE_SUBTYPE_DB_VIEW,
|
|
8
|
-
SOURCE_DB_TABLE_ATTR_NAME,
|
|
9
6
|
SOURCE_ATTR_SCHEMA,
|
|
7
|
+
SOURCE_ROLE_PRIMARY,
|
|
10
8
|
} from "./persistence/source/source-constants.js";
|
|
9
|
+
import { MetaSource } from "./persistence/source/meta-source.js";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Strip the package prefix from a metadata-qualified name
|
|
@@ -36,30 +35,33 @@ export function pluralize(s: string): string {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export function resolveTableName(entity: MetaData): string {
|
|
38
|
+
// Primary writable source carries the physical table name (@table).
|
|
39
39
|
const source = entity.ownChildren().find(
|
|
40
|
-
(c)
|
|
40
|
+
(c): c is MetaSource =>
|
|
41
|
+
c instanceof MetaSource && c.isWritable() && c.role === SOURCE_ROLE_PRIMARY,
|
|
41
42
|
);
|
|
42
|
-
const name = source?.
|
|
43
|
+
const name = source?.tableName;
|
|
43
44
|
if (typeof name === "string" && name !== "") return name;
|
|
44
45
|
return pluralize(toSnakeCase(entity.name));
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export function resolveColumnName(field: MetaData): string {
|
|
48
|
-
const
|
|
49
|
-
if (typeof
|
|
49
|
+
const col = field.ownAttr(FIELD_ATTR_COLUMN);
|
|
50
|
+
if (typeof col === "string" && col) return col;
|
|
50
51
|
return toSnakeCase(field.name);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
* Returns the DB schema declared on an entity's
|
|
55
|
-
*
|
|
55
|
+
* Returns the DB schema declared on an entity's primary source child, or undefined
|
|
56
|
+
* when no @schema attr is set or no source child exists. @schema is paradigm-agnostic
|
|
57
|
+
* (works for writable tables and read-only views/projections alike). Callers decide what
|
|
56
58
|
* "undefined" means for their dialect — Postgres treats it as the default public schema,
|
|
57
59
|
* SQLite treats it as the only allowed value (no schema concept).
|
|
58
60
|
*/
|
|
59
61
|
export function resolveTableSchema(entity: MetaData): string | undefined {
|
|
60
62
|
const source = entity.ownChildren().find(
|
|
61
|
-
(c)
|
|
62
|
-
|
|
63
|
+
(c): c is MetaSource =>
|
|
64
|
+
c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
|
|
63
65
|
);
|
|
64
66
|
if (!source) return undefined;
|
|
65
67
|
const schema = source.ownAttr(SOURCE_ATTR_SCHEMA);
|
package/src/parser-core.ts
CHANGED
|
@@ -188,6 +188,14 @@ function expandPackageForPath(basePkg: string, pkgPath: string): string {
|
|
|
188
188
|
// a single parse call. Set at buildTree entry, read deep in the call tree.
|
|
189
189
|
let _deferSuperResolution = false;
|
|
190
190
|
|
|
191
|
+
// Module-level hard-error sink for inline @-attr checks (used by
|
|
192
|
+
// applyInlineAttrsAndUnknownKeys). Set at buildTree entry alongside the
|
|
193
|
+
// errors[] aggregator so a reserved-word-as-attr violation lands in the
|
|
194
|
+
// loader's errors[] without changing the helper's signature.
|
|
195
|
+
// Safe because buildTree is synchronous — same reentrancy argument as
|
|
196
|
+
// _deferSuperResolution.
|
|
197
|
+
let _currentErrors: ParseError[] | undefined;
|
|
198
|
+
|
|
191
199
|
/**
|
|
192
200
|
* buildTree — the shared registry-driven tree-builder.
|
|
193
201
|
*
|
|
@@ -205,94 +213,100 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
205
213
|
const strict = opts.strict ?? false;
|
|
206
214
|
const source = opts.sourceName;
|
|
207
215
|
_deferSuperResolution = opts.deferSuperResolution === true;
|
|
216
|
+
_currentErrors = errors;
|
|
208
217
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
try {
|
|
219
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
220
|
+
throw new ParseError("Top-level metadata must be an object", { ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" });
|
|
221
|
+
}
|
|
212
222
|
|
|
213
|
-
|
|
223
|
+
const topLevel = parsed as Record<string, unknown>;
|
|
214
224
|
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
// --- Find the wrapper key (skip $schema) ---
|
|
226
|
+
const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
|
|
217
227
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
if (wrapperKeys.length === 0) {
|
|
229
|
+
throw new ParseError("Top-level metadata object has no type wrapper key", { ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" });
|
|
230
|
+
}
|
|
231
|
+
if (wrapperKeys.length > 1) {
|
|
232
|
+
throw new ParseError(
|
|
233
|
+
`Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`,
|
|
234
|
+
{ ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
const rootKey = wrapperKeys[0]!;
|
|
239
|
+
const rootData = topLevel[rootKey];
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
242
|
+
throw new ParseError(
|
|
243
|
+
`Top-level wrapper "${rootKey}" must contain an object`,
|
|
244
|
+
{ ...errOpts(source, rootKey), code: "ERR_TOP_LEVEL_NOT_OBJECT" },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
237
247
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Check root type is registered (always throw — can't skip the root)
|
|
242
|
-
if (!opts.registry.has(rootType, rootSubType)) {
|
|
243
|
-
const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
|
|
244
|
-
? "ERR_UNKNOWN_SUBTYPE" as const
|
|
245
|
-
: "ERR_UNKNOWN_TYPE" as const;
|
|
246
|
-
throw new ParseError(
|
|
247
|
-
`Unknown root type "${rootType}.${rootSubType}" — not registered`,
|
|
248
|
-
{ ...errOpts(source, rootKey), code: rootTypeCode },
|
|
249
|
-
);
|
|
250
|
-
}
|
|
248
|
+
const rootDataObj = rootData as Record<string, unknown>;
|
|
249
|
+
const { type: rootType, subType: rootSubType } = splitTypeKey(rootKey, opts.registry);
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
// Check root type is registered (always throw — can't skip the root)
|
|
252
|
+
if (!opts.registry.has(rootType, rootSubType)) {
|
|
253
|
+
const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
|
|
254
|
+
? "ERR_UNKNOWN_SUBTYPE" as const
|
|
255
|
+
: "ERR_UNKNOWN_TYPE" as const;
|
|
256
|
+
throw new ParseError(
|
|
257
|
+
`Unknown root type "${rootType}.${rootSubType}" — not registered`,
|
|
258
|
+
{ ...errOpts(source, rootKey), code: rootTypeCode },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (opts.intoRoot !== undefined) {
|
|
263
|
+
// --- Merge mode: parse root's attrs/children into the existing root ---
|
|
264
|
+
// The JSON root's own package/super/reserved-keys are not re-applied to the
|
|
265
|
+
// existing root. BUT: children from the NEW JSON should inherit from the
|
|
266
|
+
// NEW root's package, not the existing root's.
|
|
267
|
+
const newRootPkg = rootDataObj[RESERVED_KEY_PACKAGE];
|
|
268
|
+
const contextPkg = (typeof newRootPkg === "string" ? newRootPkg : opts.intoRoot.package) ?? "";
|
|
269
|
+
parseNodeInto(
|
|
270
|
+
rootDataObj,
|
|
271
|
+
opts.intoRoot,
|
|
272
|
+
opts.intoRoot, // accumulating root for super resolution
|
|
273
|
+
contextPkg,
|
|
274
|
+
opts.registry,
|
|
275
|
+
warnings,
|
|
276
|
+
errors,
|
|
277
|
+
strict,
|
|
278
|
+
source,
|
|
279
|
+
rootKey,
|
|
280
|
+
);
|
|
281
|
+
return { root: opts.intoRoot, warnings, errors };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- Fresh root mode: create a new root from the JSON ---
|
|
285
|
+
// The cast is safe within the core provider: `metadata.root` is the only
|
|
286
|
+
// registered `metadata` subtype, and its factory unconditionally produces a
|
|
287
|
+
// MetaRoot (see core-types.ts). A registry that registered a second
|
|
288
|
+
// `metadata.*` subtype backed by a non-MetaRoot factory would break this
|
|
289
|
+
// cast — a known limitation of the `TypeDefinition.factory: => MetaData`
|
|
290
|
+
// signature. `parseNodeFresh` is a general node parser, so MetaData is its
|
|
291
|
+
// correct return type; this top-level callsite is where the doc-root invariant holds.
|
|
292
|
+
const root = parseNodeFresh(
|
|
293
|
+
rootType,
|
|
294
|
+
rootSubType,
|
|
260
295
|
rootDataObj,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
contextPkg,
|
|
296
|
+
undefined, // no accumulating root yet — built as we go
|
|
297
|
+
"", // no inherited context pkg yet for the root itself
|
|
264
298
|
opts.registry,
|
|
265
299
|
warnings,
|
|
266
300
|
errors,
|
|
267
301
|
strict,
|
|
268
302
|
source,
|
|
269
303
|
rootKey,
|
|
270
|
-
);
|
|
271
|
-
return { root
|
|
304
|
+
) as MetaRoot;
|
|
305
|
+
return { root, warnings, errors };
|
|
306
|
+
} finally {
|
|
307
|
+
_deferSuperResolution = false;
|
|
308
|
+
_currentErrors = undefined;
|
|
272
309
|
}
|
|
273
|
-
|
|
274
|
-
// --- Fresh root mode: create a new root from the JSON ---
|
|
275
|
-
// The cast is safe within the core provider: `metadata.root` is the only
|
|
276
|
-
// registered `metadata` subtype, and its factory unconditionally produces a
|
|
277
|
-
// MetaRoot (see core-types.ts). A registry that registered a second
|
|
278
|
-
// `metadata.*` subtype backed by a non-MetaRoot factory would break this
|
|
279
|
-
// cast — a known limitation of the `TypeDefinition.factory: => MetaData`
|
|
280
|
-
// signature. `parseNodeFresh` is a general node parser, so MetaData is its
|
|
281
|
-
// correct return type; this top-level callsite is where the doc-root invariant holds.
|
|
282
|
-
const root = parseNodeFresh(
|
|
283
|
-
rootType,
|
|
284
|
-
rootSubType,
|
|
285
|
-
rootDataObj,
|
|
286
|
-
undefined, // no accumulating root yet — built as we go
|
|
287
|
-
"", // no inherited context pkg yet for the root itself
|
|
288
|
-
opts.registry,
|
|
289
|
-
warnings,
|
|
290
|
-
errors,
|
|
291
|
-
strict,
|
|
292
|
-
source,
|
|
293
|
-
rootKey,
|
|
294
|
-
) as MetaRoot;
|
|
295
|
-
return { root, warnings, errors };
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
// ---------------------------------------------------------------------------
|
|
@@ -580,6 +594,34 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
580
594
|
|
|
581
595
|
// Inline attribute (@-prefixed) — materialize into a MetaAttr instance.
|
|
582
596
|
const attrName = key.slice(ATTR_PREFIX.length);
|
|
597
|
+
|
|
598
|
+
// Reserved structural keys (name/package/extends/abstract/overlay/isArray
|
|
599
|
+
// /children/value) must NOT be @-prefixed — they're written bare. An
|
|
600
|
+
// @-prefixed reserved word is always a metadata-author error (e.g.
|
|
601
|
+
// "@isArray" instead of bare "isArray") and is reported as a hard error
|
|
602
|
+
// regardless of strict mode so downstream code never sees a bogus
|
|
603
|
+
// MetaAttr named after a reserved word.
|
|
604
|
+
if (RESERVED_KEYS.has(attrName)) {
|
|
605
|
+
const displayName =
|
|
606
|
+
model.name !== "" ? `${model.type}.${model.subType} '${model.name}'` : `${model.type}.${model.subType}`;
|
|
607
|
+
const msg =
|
|
608
|
+
`Reserved structural key '${attrName}' must not be ${ATTR_PREFIX}-prefixed ` +
|
|
609
|
+
`on ${displayName} at ${path} (write it bare)`;
|
|
610
|
+
if (strict) {
|
|
611
|
+
throw new ParseError(msg, { ...errOpts(source, path), code: "ERR_RESERVED_ATTR" });
|
|
612
|
+
}
|
|
613
|
+
// Lax mode: route through the module-level errors sink so the loader
|
|
614
|
+
// sees this as a hard error (parity with attr-schema-validate's
|
|
615
|
+
// ERR_BAD_ATTR_VALUE direct pushes). Falls back to warnings only if
|
|
616
|
+
// _currentErrors isn't bound (unreachable when called from buildTree).
|
|
617
|
+
if (_currentErrors !== undefined) {
|
|
618
|
+
_currentErrors.push(new ParseError(msg, { ...errOpts(source, path), code: "ERR_RESERVED_ATTR" }));
|
|
619
|
+
} else {
|
|
620
|
+
warnings.push(msg);
|
|
621
|
+
}
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
583
625
|
const rawVal = nodeData[key];
|
|
584
626
|
|
|
585
627
|
try {
|
|
@@ -606,7 +648,11 @@ function materializeAttr(
|
|
|
606
648
|
rawVal: unknown,
|
|
607
649
|
registry: TypeRegistry,
|
|
608
650
|
): MetaAttr {
|
|
609
|
-
|
|
651
|
+
// Resolve attr spec: per-type attrs take precedence over common attrs.
|
|
652
|
+
// Common attrs are consulted as a fallback so they get the correct MetaAttr
|
|
653
|
+
// subclass (and thus the right coerce/validateValue) at parse time.
|
|
654
|
+
const perTypeSpec = registry.attrsOf(owner.type, owner.subType).find((s) => s.name === attrName);
|
|
655
|
+
const attrSpec = perTypeSpec ?? registry.getCommonAttrs().find((s) => s.name === attrName);
|
|
610
656
|
let subType: string;
|
|
611
657
|
if (attrSpec !== undefined && attrSpec.valueType !== undefined) {
|
|
612
658
|
subType = attrSpec.valueType;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// DB concern constants — physical DB column attr keys.
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
export const
|
|
3
|
+
/** Column name override on a field (maps to @column in metadata). */
|
|
4
|
+
export const FIELD_ATTR_COLUMN = "column";
|
|
5
5
|
/** When true, suppress the @filterable-without-index Loader warning (Project D drift check). */
|
|
6
6
|
export const FIELD_ATTR_DB_INDEXED = "db.indexed";
|
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
// dbProvider — the DB-domain MetaDataTypeProvider. Registers the DB-domain
|
|
2
|
-
// attributes (@
|
|
3
|
-
// source.
|
|
2
|
+
// attributes (@column / @db.indexed on fields; @table/@kind/@role/@schema on
|
|
3
|
+
// source.rdb) by extending the core-registered types. Mirrors Java's
|
|
4
4
|
// CoreDBMetaDataProvider (com.metaobjects.database).
|
|
5
5
|
|
|
6
6
|
import type { MetaDataTypeProvider } from "../../provider.js";
|
|
7
7
|
import type { TypeRegistry } from "../../registry.js";
|
|
8
8
|
import { TYPE_FIELD, TYPE_SOURCE } from "../../shared/base-types.js";
|
|
9
9
|
import { FIELD_SUBTYPES } from "../../core/field/field-constants.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "../source/source-constants.js";
|
|
14
|
-
import { dbColumnSchema, dbIndexedSchema, sourceNameSchema } from "./db-schema.js";
|
|
10
|
+
import { SOURCE_SUBTYPE_RDB } from "../source/source-constants.js";
|
|
11
|
+
import { columnSchema, dbIndexedSchema } from "./db-schema.js";
|
|
12
|
+
import { sourceRdbAttrs } from "../source/source-schema.js";
|
|
15
13
|
|
|
16
14
|
export const dbProvider: MetaDataTypeProvider = {
|
|
17
15
|
id: "metaobjects-db",
|
|
18
16
|
dependencies: ["metaobjects-core-types"],
|
|
19
17
|
description:
|
|
20
|
-
"DB-domain attributes — @
|
|
18
|
+
"DB-domain attributes — @column / @db.indexed on fields, @table/@kind/@role/@schema on source.rdb.",
|
|
21
19
|
registerTypes(registry: TypeRegistry): void {
|
|
22
20
|
for (const subType of FIELD_SUBTYPES) {
|
|
23
21
|
registry.extend(TYPE_FIELD, subType, {
|
|
24
|
-
attributes: [
|
|
22
|
+
attributes: [columnSchema, dbIndexedSchema],
|
|
25
23
|
});
|
|
26
24
|
}
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
attributes: [sourceNameSchema],
|
|
31
|
-
});
|
|
32
|
-
registry.extend(TYPE_SOURCE, SOURCE_SUBTYPE_DB_VIEW, {
|
|
33
|
-
attributes: [sourceNameSchema],
|
|
25
|
+
// source.rdb — @table/@kind/@role/@schema attrs.
|
|
26
|
+
registry.extend(TYPE_SOURCE, SOURCE_SUBTYPE_RDB, {
|
|
27
|
+
attributes: [...sourceRdbAttrs],
|
|
34
28
|
});
|
|
35
29
|
},
|
|
36
30
|
};
|
|
@@ -7,18 +7,17 @@ import {
|
|
|
7
7
|
ATTR_SUBTYPE_BOOLEAN,
|
|
8
8
|
} from "../../core/attr/attr-constants.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
FIELD_ATTR_COLUMN,
|
|
11
11
|
FIELD_ATTR_DB_INDEXED,
|
|
12
12
|
} from "./db-constants.js";
|
|
13
|
-
import { SOURCE_ATTR_NAME } from "../source/source-constants.js";
|
|
14
13
|
|
|
15
|
-
/** `@
|
|
16
|
-
export const
|
|
17
|
-
name:
|
|
14
|
+
/** `@column` — column-name override on every field subtype (source.rdb). */
|
|
15
|
+
export const columnSchema: AttrSchema = {
|
|
16
|
+
name: FIELD_ATTR_COLUMN,
|
|
18
17
|
valueType: ATTR_SUBTYPE_STRING,
|
|
19
18
|
required: false,
|
|
20
19
|
description:
|
|
21
|
-
"
|
|
20
|
+
"Physical column name for this field on an rdb source. Defaults to the field name via columnNamingStrategy.",
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
/** `@db.indexed` — suppress the @filterable-without-index warning; on every field subtype. */
|
|
@@ -29,12 +28,3 @@ export const dbIndexedSchema: AttrSchema = {
|
|
|
29
28
|
description:
|
|
30
29
|
"When true, suppress the @filterable-without-index Loader warning (the field is indexed by other means).",
|
|
31
30
|
};
|
|
32
|
-
|
|
33
|
-
/** `@name` — the SQL table/view identifier; on source.dbTable and source.dbView. */
|
|
34
|
-
export const sourceNameSchema: AttrSchema = {
|
|
35
|
-
name: SOURCE_ATTR_NAME,
|
|
36
|
-
valueType: ATTR_SUBTYPE_STRING,
|
|
37
|
-
required: false,
|
|
38
|
-
description:
|
|
39
|
-
"The SQL table or view name for this source. Defaults to the object name run through the columnNamingStrategy when omitted.",
|
|
40
|
-
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ORIGIN_AGGREGATE_ATTR_AGG,
|
|
15
15
|
ORIGIN_AGGREGATE_ATTR_OF,
|
|
16
16
|
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
17
|
+
ORIGIN_COLLECTION_ATTR_VIA,
|
|
17
18
|
type AggregateFunction,
|
|
18
19
|
} from "./origin-constants.js";
|
|
19
20
|
|
|
@@ -65,3 +66,17 @@ export class MetaAggregateOrigin extends MetaOrigin {
|
|
|
65
66
|
return typeof v === "string" ? v : undefined;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Collection origin — the (array) field's value is a relationship-derived
|
|
72
|
+
* array of nested view-objects (FR-004 R4). Carries `@via` (required): the
|
|
73
|
+
* dotted relationship path the collection walks (e.g. "Author.posts"), or a
|
|
74
|
+
* wildcard-prefixed selector for a package-spanning collection (e.g. "*.User").
|
|
75
|
+
*/
|
|
76
|
+
export class MetaCollectionOrigin extends MetaOrigin {
|
|
77
|
+
/** The dotted relationship path (or wildcard selector) this collection is sourced from. */
|
|
78
|
+
get via(): string | undefined {
|
|
79
|
+
const v = this.ownAttr(ORIGIN_COLLECTION_ATTR_VIA);
|
|
80
|
+
return typeof v === "string" ? v : undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -13,11 +13,13 @@ import { SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
|
13
13
|
|
|
14
14
|
export const ORIGIN_SUBTYPE_PASSTHROUGH = "passthrough";
|
|
15
15
|
export const ORIGIN_SUBTYPE_AGGREGATE = "aggregate";
|
|
16
|
+
export const ORIGIN_SUBTYPE_COLLECTION = "collection";
|
|
16
17
|
|
|
17
18
|
export const ORIGIN_SUBTYPES = [
|
|
18
19
|
SUBTYPE_BASE,
|
|
19
20
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
20
21
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
22
|
+
ORIGIN_SUBTYPE_COLLECTION,
|
|
21
23
|
] as const;
|
|
22
24
|
export type OriginSubType = (typeof ORIGIN_SUBTYPES)[number];
|
|
23
25
|
|
|
@@ -25,6 +27,11 @@ export type OriginSubType = (typeof ORIGIN_SUBTYPES)[number];
|
|
|
25
27
|
export const ORIGIN_PASSTHROUGH_ATTR_FROM = "from";
|
|
26
28
|
export const ORIGIN_PASSTHROUGH_ATTR_VIA = "via";
|
|
27
29
|
|
|
30
|
+
// collection attrs — a relationship-derived array of nested view-objects
|
|
31
|
+
// (FR-004 R4). @via is the dotted relationship path (optionally wildcard-
|
|
32
|
+
// prefixed, e.g. "*.User", for a package-spanning collection).
|
|
33
|
+
export const ORIGIN_COLLECTION_ATTR_VIA = "via";
|
|
34
|
+
|
|
28
35
|
// aggregate attrs
|
|
29
36
|
export const ORIGIN_AGGREGATE_ATTR_AGG = "agg";
|
|
30
37
|
export const ORIGIN_AGGREGATE_ATTR_OF = "of";
|
|
@@ -7,11 +7,13 @@ import { SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
|
7
7
|
import {
|
|
8
8
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
9
9
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
10
|
+
ORIGIN_SUBTYPE_COLLECTION,
|
|
10
11
|
ORIGIN_PASSTHROUGH_ATTR_FROM,
|
|
11
12
|
ORIGIN_PASSTHROUGH_ATTR_VIA,
|
|
12
13
|
ORIGIN_AGGREGATE_ATTR_AGG,
|
|
13
14
|
ORIGIN_AGGREGATE_ATTR_OF,
|
|
14
15
|
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
16
|
+
ORIGIN_COLLECTION_ATTR_VIA,
|
|
15
17
|
AGGREGATE_FUNCTIONS,
|
|
16
18
|
} from "./origin-constants.js";
|
|
17
19
|
|
|
@@ -58,9 +60,21 @@ const aggregateOriginAttrs: AttrSchema[] = [
|
|
|
58
60
|
},
|
|
59
61
|
];
|
|
60
62
|
|
|
61
|
-
/** Attrs
|
|
63
|
+
/** Attrs on origin.collection — @via (the relationship path) is required. */
|
|
64
|
+
const collectionOriginAttrs: AttrSchema[] = [
|
|
65
|
+
{
|
|
66
|
+
name: ORIGIN_COLLECTION_ATTR_VIA,
|
|
67
|
+
valueType: ATTR_SUBTYPE_STRING,
|
|
68
|
+
required: true,
|
|
69
|
+
description:
|
|
70
|
+
"Dotted relationship path the collection walks to produce an array of nested view-objects (e.g. 'Author.posts'), or a wildcard selector for a package-spanning collection (e.g. '*.User').",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** Attrs per origin subtype. base has none; the others carry their respective attrs. */
|
|
62
75
|
export const ORIGIN_ATTRS_MAP = new Map<string, AttrSchema[]>([
|
|
63
76
|
[SUBTYPE_BASE, []],
|
|
64
77
|
[ORIGIN_SUBTYPE_PASSTHROUGH, [...passthroughOriginAttrs]],
|
|
65
78
|
[ORIGIN_SUBTYPE_AGGREGATE, [...aggregateOriginAttrs]],
|
|
79
|
+
[ORIGIN_SUBTYPE_COLLECTION, [...collectionOriginAttrs]],
|
|
66
80
|
]);
|