@metaobjectsdev/metadata 0.5.0 → 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.
Files changed (168) hide show
  1. package/dist/attr-schema-validate.d.ts.map +1 -1
  2. package/dist/attr-schema-validate.js +66 -11
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/documentation/doc-constants.d.ts +11 -0
  5. package/dist/core/documentation/doc-constants.d.ts.map +1 -0
  6. package/dist/core/documentation/doc-constants.js +20 -0
  7. package/dist/core/documentation/doc-constants.js.map +1 -0
  8. package/dist/core/documentation/doc-provider.d.ts +3 -0
  9. package/dist/core/documentation/doc-provider.d.ts.map +1 -0
  10. package/dist/core/documentation/doc-provider.js +10 -0
  11. package/dist/core/documentation/doc-provider.js.map +1 -0
  12. package/dist/core/documentation/doc-schema.d.ts +8 -0
  13. package/dist/core/documentation/doc-schema.d.ts.map +1 -0
  14. package/dist/core/documentation/doc-schema.js +53 -0
  15. package/dist/core/documentation/doc-schema.js.map +1 -0
  16. package/dist/core/field/field-constants.d.ts +25 -1
  17. package/dist/core/field/field-constants.d.ts.map +1 -1
  18. package/dist/core/field/field-constants.js +32 -1
  19. package/dist/core/field/field-constants.js.map +1 -1
  20. package/dist/core/field/field-schema.d.ts +2 -0
  21. package/dist/core/field/field-schema.d.ts.map +1 -1
  22. package/dist/core/field/field-schema.js +20 -2
  23. package/dist/core/field/field-schema.js.map +1 -1
  24. package/dist/core/field/meta-field.d.ts +2 -1
  25. package/dist/core/field/meta-field.d.ts.map +1 -1
  26. package/dist/core/field/meta-field.js +6 -4
  27. package/dist/core/field/meta-field.js.map +1 -1
  28. package/dist/core/object/meta-object.d.ts.map +1 -1
  29. package/dist/core/object/meta-object.js +6 -5
  30. package/dist/core/object/meta-object.js.map +1 -1
  31. package/dist/core/parser-yaml.d.ts.map +1 -1
  32. package/dist/core/parser-yaml.js +9 -2
  33. package/dist/core/parser-yaml.js.map +1 -1
  34. package/dist/core/relationship/meta-relationship.d.ts +4 -0
  35. package/dist/core/relationship/meta-relationship.d.ts.map +1 -1
  36. package/dist/core/relationship/meta-relationship.js +11 -1
  37. package/dist/core/relationship/meta-relationship.js.map +1 -1
  38. package/dist/core/relationship/relationship-constants.d.ts +9 -0
  39. package/dist/core/relationship/relationship-constants.d.ts.map +1 -1
  40. package/dist/core/relationship/relationship-constants.js +15 -0
  41. package/dist/core/relationship/relationship-constants.js.map +1 -1
  42. package/dist/core/relationship/relationship-schema.d.ts.map +1 -1
  43. package/dist/core/relationship/relationship-schema.js +15 -1
  44. package/dist/core/relationship/relationship-schema.js.map +1 -1
  45. package/dist/core/yaml-desugar.d.ts +8 -1
  46. package/dist/core/yaml-desugar.d.ts.map +1 -1
  47. package/dist/core/yaml-desugar.js +166 -15
  48. package/dist/core/yaml-desugar.js.map +1 -1
  49. package/dist/core-types.d.ts.map +1 -1
  50. package/dist/core-types.js +28 -11
  51. package/dist/core-types.js.map +1 -1
  52. package/dist/errors.d.ts +1 -1
  53. package/dist/errors.d.ts.map +1 -1
  54. package/dist/errors.js +21 -0
  55. package/dist/errors.js.map +1 -1
  56. package/dist/index.d.ts +6 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +7 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  61. package/dist/loader/meta-data-loader.js +12 -1
  62. package/dist/loader/meta-data-loader.js.map +1 -1
  63. package/dist/loader/validation-passes.d.ts +2 -0
  64. package/dist/loader/validation-passes.d.ts.map +1 -1
  65. package/dist/loader/validation-passes.js +69 -3
  66. package/dist/loader/validation-passes.js.map +1 -1
  67. package/dist/naming.d.ts +3 -2
  68. package/dist/naming.d.ts.map +1 -1
  69. package/dist/naming.js +14 -12
  70. package/dist/naming.js.map +1 -1
  71. package/dist/parser-core.d.ts.map +1 -1
  72. package/dist/parser-core.js +91 -48
  73. package/dist/parser-core.js.map +1 -1
  74. package/dist/persistence/db/db-constants.d.ts +2 -2
  75. package/dist/persistence/db/db-constants.d.ts.map +1 -1
  76. package/dist/persistence/db/db-constants.js +2 -2
  77. package/dist/persistence/db/db-constants.js.map +1 -1
  78. package/dist/persistence/db/db-provider.d.ts.map +1 -1
  79. package/dist/persistence/db/db-provider.js +10 -13
  80. package/dist/persistence/db/db-provider.js.map +1 -1
  81. package/dist/persistence/db/db-schema.d.ts +2 -4
  82. package/dist/persistence/db/db-schema.d.ts.map +1 -1
  83. package/dist/persistence/db/db-schema.js +5 -13
  84. package/dist/persistence/db/db-schema.js.map +1 -1
  85. package/dist/persistence/origin/meta-origin.d.ts +10 -0
  86. package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
  87. package/dist/persistence/origin/meta-origin.js +14 -1
  88. package/dist/persistence/origin/meta-origin.js.map +1 -1
  89. package/dist/persistence/origin/origin-constants.d.ts +3 -1
  90. package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
  91. package/dist/persistence/origin/origin-constants.js +6 -0
  92. package/dist/persistence/origin/origin-constants.js.map +1 -1
  93. package/dist/persistence/origin/origin-schema.d.ts +1 -1
  94. package/dist/persistence/origin/origin-schema.d.ts.map +1 -1
  95. package/dist/persistence/origin/origin-schema.js +12 -2
  96. package/dist/persistence/origin/origin-schema.js.map +1 -1
  97. package/dist/persistence/source/meta-source.d.ts +11 -9
  98. package/dist/persistence/source/meta-source.d.ts.map +1 -1
  99. package/dist/persistence/source/meta-source.js +23 -15
  100. package/dist/persistence/source/meta-source.js.map +1 -1
  101. package/dist/persistence/source/source-constants.d.ts +32 -11
  102. package/dist/persistence/source/source-constants.d.ts.map +1 -1
  103. package/dist/persistence/source/source-constants.js +55 -16
  104. package/dist/persistence/source/source-constants.js.map +1 -1
  105. package/dist/persistence/source/source-schema.d.ts +4 -0
  106. package/dist/persistence/source/source-schema.d.ts.map +1 -0
  107. package/dist/persistence/source/source-schema.js +37 -0
  108. package/dist/persistence/source/source-schema.js.map +1 -0
  109. package/dist/persistence/source/validate-source-roles.d.ts +12 -0
  110. package/dist/persistence/source/validate-source-roles.d.ts.map +1 -0
  111. package/dist/persistence/source/validate-source-roles.js +38 -0
  112. package/dist/persistence/source/validate-source-roles.js.map +1 -0
  113. package/dist/registry.d.ts +13 -0
  114. package/dist/registry.d.ts.map +1 -1
  115. package/dist/registry.js +26 -0
  116. package/dist/registry.js.map +1 -1
  117. package/dist/shared/base-types.d.ts +2 -1
  118. package/dist/shared/base-types.d.ts.map +1 -1
  119. package/dist/shared/base-types.js +5 -2
  120. package/dist/shared/base-types.js.map +1 -1
  121. package/dist/template/meta-template.d.ts +13 -0
  122. package/dist/template/meta-template.d.ts.map +1 -0
  123. package/dist/template/meta-template.js +13 -0
  124. package/dist/template/meta-template.js.map +1 -0
  125. package/dist/template/template-constants.d.ts +17 -0
  126. package/dist/template/template-constants.d.ts.map +1 -0
  127. package/dist/template/template-constants.js +46 -0
  128. package/dist/template/template-constants.js.map +1 -0
  129. package/dist/template/template-schema.d.ts +3 -0
  130. package/dist/template/template-schema.d.ts.map +1 -0
  131. package/dist/template/template-schema.js +84 -0
  132. package/dist/template/template-schema.js.map +1 -0
  133. package/package.json +1 -1
  134. package/src/attr-schema-validate.ts +89 -9
  135. package/src/core/documentation/doc-constants.ts +22 -0
  136. package/src/core/documentation/doc-provider.ts +12 -0
  137. package/src/core/documentation/doc-schema.ts +64 -0
  138. package/src/core/field/field-constants.ts +41 -1
  139. package/src/core/field/field-schema.ts +25 -0
  140. package/src/core/field/meta-field.ts +6 -3
  141. package/src/core/object/meta-object.ts +6 -6
  142. package/src/core/parser-yaml.ts +11 -3
  143. package/src/core/relationship/meta-relationship.ts +14 -0
  144. package/src/core/relationship/relationship-constants.ts +20 -0
  145. package/src/core/relationship/relationship-schema.ts +18 -0
  146. package/src/core/yaml-desugar.ts +206 -24
  147. package/src/core-types.ts +31 -9
  148. package/src/errors.ts +21 -0
  149. package/src/index.ts +7 -0
  150. package/src/loader/meta-data-loader.ts +15 -1
  151. package/src/loader/validation-passes.ts +98 -1
  152. package/src/naming.ts +15 -13
  153. package/src/parser-core.ts +119 -73
  154. package/src/persistence/db/db-constants.ts +2 -2
  155. package/src/persistence/db/db-provider.ts +10 -16
  156. package/src/persistence/db/db-schema.ts +5 -15
  157. package/src/persistence/origin/meta-origin.ts +15 -0
  158. package/src/persistence/origin/origin-constants.ts +7 -0
  159. package/src/persistence/origin/origin-schema.ts +15 -1
  160. package/src/persistence/source/meta-source.ts +30 -17
  161. package/src/persistence/source/source-constants.ts +66 -17
  162. package/src/persistence/source/source-schema.ts +54 -0
  163. package/src/persistence/source/validate-source-roles.ts +53 -0
  164. package/src/registry.ts +31 -0
  165. package/src/shared/base-types.ts +5 -2
  166. package/src/template/meta-template.ts +12 -0
  167. package/src/template/template-constants.ts +53 -0
  168. 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 (called by MetaDataLoader.load() in order).
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, TYPE_SOURCE } from "./shared/base-types.js";
2
+ import { TYPE_FIELD } from "./shared/base-types.js";
3
3
  import { PACKAGE_SEPARATOR } from "./shared/structural.js";
4
- import { FIELD_ATTR_DB_COLUMN } from "./persistence/db/db-constants.js";
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) => c.type === TYPE_SOURCE && c.subType === SOURCE_SUBTYPE_DB_TABLE,
40
+ (c): c is MetaSource =>
41
+ c instanceof MetaSource && c.isWritable() && c.role === SOURCE_ROLE_PRIMARY,
41
42
  );
42
- const name = source?.ownAttr(SOURCE_DB_TABLE_ATTR_NAME);
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 attr = field.ownAttr(FIELD_ATTR_DB_COLUMN);
49
- if (typeof attr === "string") return attr;
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 source[dbTable] or source[dbView] child,
55
- * or undefined if no @schema attr is set or no source child exists. Callers decide what
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) => c.type === TYPE_SOURCE
62
- && (c.subType === SOURCE_SUBTYPE_DB_TABLE || c.subType === SOURCE_SUBTYPE_DB_VIEW),
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);
@@ -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
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
210
- throw new ParseError("Top-level metadata must be an object", { ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" });
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
- const topLevel = parsed as Record<string, unknown>;
223
+ const topLevel = parsed as Record<string, unknown>;
214
224
 
215
- // --- Find the wrapper key (skip $schema) ---
216
- const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
225
+ // --- Find the wrapper key (skip $schema) ---
226
+ const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
217
227
 
218
- if (wrapperKeys.length === 0) {
219
- throw new ParseError("Top-level metadata object has no type wrapper key", { ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" });
220
- }
221
- if (wrapperKeys.length > 1) {
222
- throw new ParseError(
223
- `Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`,
224
- { ...errOpts(source), code: "ERR_TOP_LEVEL_NOT_OBJECT" },
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
- const rootKey = wrapperKeys[0]!;
229
- const rootData = topLevel[rootKey];
238
+ const rootKey = wrapperKeys[0]!;
239
+ const rootData = topLevel[rootKey];
230
240
 
231
- if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
232
- throw new ParseError(
233
- `Top-level wrapper "${rootKey}" must contain an object`,
234
- { ...errOpts(source, rootKey), code: "ERR_TOP_LEVEL_NOT_OBJECT" },
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
- const rootDataObj = rootData as Record<string, unknown>;
239
- const { type: rootType, subType: rootSubType } = splitTypeKey(rootKey, opts.registry);
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
- if (opts.intoRoot !== undefined) {
253
- // --- Merge mode: parse root's attrs/children into the existing root ---
254
- // The JSON root's own package/super/reserved-keys are not re-applied to the
255
- // existing root. BUT: children from the NEW JSON should inherit from the
256
- // NEW root's package, not the existing root's.
257
- const newRootPkg = rootDataObj[RESERVED_KEY_PACKAGE];
258
- const contextPkg = (typeof newRootPkg === "string" ? newRootPkg : opts.intoRoot.package) ?? "";
259
- parseNodeInto(
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
- opts.intoRoot,
262
- opts.intoRoot, // accumulating root for super resolution
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: opts.intoRoot, warnings, errors };
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
- const attrSpec = registry.attrsOf(owner.type, owner.subType).find((s) => s.name === attrName);
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
- /** Custom DB column name override on a field (maps to @dbColumn in metadata). */
4
- export const FIELD_ATTR_DB_COLUMN = "dbColumn";
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 (@dbColumn, @db.indexed on fields; @name on source.dbTable /
3
- // source.dbView) by extending the core-registered types. Mirrors Java's
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
- SOURCE_SUBTYPE_DB_TABLE,
12
- SOURCE_SUBTYPE_DB_VIEW,
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 — @dbColumn / @db.indexed on fields, @name on source.dbTable / source.dbView.",
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: [dbColumnSchema, dbIndexedSchema],
22
+ attributes: [columnSchema, dbIndexedSchema],
25
23
  });
26
24
  }
27
- // Two explicit calls (not a loop) dbTable and dbView are the only DB
28
- // source subtypes, and there is no DB_SOURCE_SUBTYPES constant to loop.
29
- registry.extend(TYPE_SOURCE, SOURCE_SUBTYPE_DB_TABLE, {
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
- FIELD_ATTR_DB_COLUMN,
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
- /** `@dbColumn` — column-name override; on every field subtype. */
16
- export const dbColumnSchema: AttrSchema = {
17
- name: FIELD_ATTR_DB_COLUMN,
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
- "Override the generated SQL column name for this field. Defaults to the field name run through the project's columnNamingStrategy.",
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 per origin subtype. base has none; passthrough and aggregate carry their respective 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
  ]);