@metaobjectsdev/codegen-ts 0.14.1 → 0.15.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 (154) hide show
  1. package/dist/column-mapper.d.ts.map +1 -1
  2. package/dist/column-mapper.js +36 -16
  3. package/dist/column-mapper.js.map +1 -1
  4. package/dist/generators/api-model.js +21 -13
  5. package/dist/generators/api-model.js.map +1 -1
  6. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  7. package/dist/generators/docs-data-builder.js +27 -22
  8. package/dist/generators/docs-data-builder.js.map +1 -1
  9. package/dist/generators/docs-file.d.ts.map +1 -1
  10. package/dist/generators/docs-file.js +2 -1
  11. package/dist/generators/docs-file.js.map +1 -1
  12. package/dist/generators/extractor-file.d.ts.map +1 -1
  13. package/dist/generators/extractor-file.js +4 -2
  14. package/dist/generators/extractor-file.js.map +1 -1
  15. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  16. package/dist/generators/output-prompt-file.js +5 -3
  17. package/dist/generators/output-prompt-file.js.map +1 -1
  18. package/dist/generators/render-helper-file.d.ts.map +1 -1
  19. package/dist/generators/render-helper-file.js +5 -3
  20. package/dist/generators/render-helper-file.js.map +1 -1
  21. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  22. package/dist/generators/routes-file-hono.js +2 -1
  23. package/dist/generators/routes-file-hono.js.map +1 -1
  24. package/dist/generators/routes-file.d.ts.map +1 -1
  25. package/dist/generators/routes-file.js +3 -1
  26. package/dist/generators/routes-file.js.map +1 -1
  27. package/dist/generators/template-doc-builder.d.ts.map +1 -1
  28. package/dist/generators/template-doc-builder.js +14 -11
  29. package/dist/generators/template-doc-builder.js.map +1 -1
  30. package/dist/generators/trace-helper-file.d.ts.map +1 -1
  31. package/dist/generators/trace-helper-file.js +11 -5
  32. package/dist/generators/trace-helper-file.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/naming.d.ts +10 -0
  38. package/dist/naming.d.ts.map +1 -1
  39. package/dist/naming.js +47 -0
  40. package/dist/naming.js.map +1 -1
  41. package/dist/payload-codegen.d.ts.map +1 -1
  42. package/dist/payload-codegen.js +12 -9
  43. package/dist/payload-codegen.js.map +1 -1
  44. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  45. package/dist/projection/extract-view-spec.js +26 -3
  46. package/dist/projection/extract-view-spec.js.map +1 -1
  47. package/dist/projection/projection-detector.d.ts.map +1 -1
  48. package/dist/projection/projection-detector.js +4 -0
  49. package/dist/projection/projection-detector.js.map +1 -1
  50. package/dist/relation-resolver.d.ts.map +1 -1
  51. package/dist/relation-resolver.js +20 -12
  52. package/dist/relation-resolver.js.map +1 -1
  53. package/dist/source-detect.d.ts.map +1 -1
  54. package/dist/source-detect.js +4 -1
  55. package/dist/source-detect.js.map +1 -1
  56. package/dist/template-codegen/template-data.js +1 -1
  57. package/dist/template-codegen/template-data.js.map +1 -1
  58. package/dist/templates/callable-file.d.ts.map +1 -1
  59. package/dist/templates/callable-file.js +8 -4
  60. package/dist/templates/callable-file.js.map +1 -1
  61. package/dist/templates/entity-constants.d.ts.map +1 -1
  62. package/dist/templates/entity-constants.js +15 -11
  63. package/dist/templates/entity-constants.js.map +1 -1
  64. package/dist/templates/entity-file.d.ts.map +1 -1
  65. package/dist/templates/entity-file.js +7 -2
  66. package/dist/templates/entity-file.js.map +1 -1
  67. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  68. package/dist/templates/extract-delegate-emitter.js +2 -1
  69. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  70. package/dist/templates/extractor.d.ts.map +1 -1
  71. package/dist/templates/extractor.js +8 -4
  72. package/dist/templates/extractor.js.map +1 -1
  73. package/dist/templates/field-meta.d.ts.map +1 -1
  74. package/dist/templates/field-meta.js +22 -4
  75. package/dist/templates/field-meta.js.map +1 -1
  76. package/dist/templates/find-templates.d.ts.map +1 -1
  77. package/dist/templates/find-templates.js +3 -0
  78. package/dist/templates/find-templates.js.map +1 -1
  79. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  80. package/dist/templates/fr010-field-mapping.js +5 -2
  81. package/dist/templates/fr010-field-mapping.js.map +1 -1
  82. package/dist/templates/inferred-types.d.ts.map +1 -1
  83. package/dist/templates/inferred-types.js +18 -14
  84. package/dist/templates/inferred-types.js.map +1 -1
  85. package/dist/templates/output-format-spec-emitter.js +7 -3
  86. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  87. package/dist/templates/output-parser.d.ts.map +1 -1
  88. package/dist/templates/output-parser.js +9 -5
  89. package/dist/templates/output-parser.js.map +1 -1
  90. package/dist/templates/output-prompt.d.ts.map +1 -1
  91. package/dist/templates/output-prompt.js +8 -4
  92. package/dist/templates/output-prompt.js.map +1 -1
  93. package/dist/templates/projection-decl.d.ts.map +1 -1
  94. package/dist/templates/projection-decl.js +4 -1
  95. package/dist/templates/projection-decl.js.map +1 -1
  96. package/dist/templates/queries-file.d.ts.map +1 -1
  97. package/dist/templates/queries-file.js +12 -1
  98. package/dist/templates/queries-file.js.map +1 -1
  99. package/dist/templates/queries.d.ts +19 -1
  100. package/dist/templates/queries.d.ts.map +1 -1
  101. package/dist/templates/queries.js +49 -7
  102. package/dist/templates/queries.js.map +1 -1
  103. package/dist/templates/render-helper.d.ts.map +1 -1
  104. package/dist/templates/render-helper.js +17 -10
  105. package/dist/templates/render-helper.js.map +1 -1
  106. package/dist/templates/routes-file.js +2 -1
  107. package/dist/templates/routes-file.js.map +1 -1
  108. package/dist/templates/tph-discriminator.d.ts.map +1 -1
  109. package/dist/templates/tph-discriminator.js +7 -0
  110. package/dist/templates/tph-discriminator.js.map +1 -1
  111. package/dist/templates/zod-validators.d.ts.map +1 -1
  112. package/dist/templates/zod-validators.js +62 -16
  113. package/dist/templates/zod-validators.js.map +1 -1
  114. package/package.json +6 -6
  115. package/src/column-mapper.ts +37 -16
  116. package/src/generators/api-model.ts +21 -13
  117. package/src/generators/docs-data-builder.ts +27 -22
  118. package/src/generators/docs-file.ts +2 -1
  119. package/src/generators/extractor-file.ts +4 -2
  120. package/src/generators/output-prompt-file.ts +5 -3
  121. package/src/generators/render-helper-file.ts +5 -3
  122. package/src/generators/routes-file-hono.ts +2 -1
  123. package/src/generators/routes-file.ts +2 -1
  124. package/src/generators/template-doc-builder.ts +14 -11
  125. package/src/generators/trace-helper-file.ts +11 -5
  126. package/src/index.ts +2 -0
  127. package/src/naming.ts +51 -0
  128. package/src/payload-codegen.ts +12 -9
  129. package/src/projection/extract-view-spec.ts +26 -3
  130. package/src/projection/projection-detector.ts +4 -0
  131. package/src/reference/queries.ts +9 -0
  132. package/src/reference/routes.ts +2 -1
  133. package/src/relation-resolver.ts +20 -12
  134. package/src/source-detect.ts +4 -1
  135. package/src/template-codegen/template-data.ts +1 -1
  136. package/src/templates/callable-file.ts +8 -4
  137. package/src/templates/entity-constants.ts +15 -11
  138. package/src/templates/entity-file.ts +7 -2
  139. package/src/templates/extract-delegate-emitter.ts +2 -1
  140. package/src/templates/extractor.ts +8 -4
  141. package/src/templates/field-meta.ts +25 -3
  142. package/src/templates/find-templates.ts +3 -0
  143. package/src/templates/fr010-field-mapping.ts +6 -1
  144. package/src/templates/inferred-types.ts +19 -13
  145. package/src/templates/output-format-spec-emitter.ts +7 -3
  146. package/src/templates/output-parser.ts +9 -5
  147. package/src/templates/output-prompt.ts +8 -4
  148. package/src/templates/projection-decl.ts +4 -1
  149. package/src/templates/queries-file.ts +14 -0
  150. package/src/templates/queries.ts +68 -8
  151. package/src/templates/render-helper.ts +17 -10
  152. package/src/templates/routes-file.ts +2 -1
  153. package/src/templates/tph-discriminator.ts +7 -0
  154. package/src/templates/zod-validators.ts +64 -15
@@ -14,6 +14,8 @@ import {
14
14
  TYPE_FIELD,
15
15
  FIELD_SUBTYPE_STRING,
16
16
  FIELD_SUBTYPE_UUID,
17
+ FIELD_SUBTYPE_URI,
18
+ FIELD_SUBTYPE_INET,
17
19
  FIELD_SUBTYPE_DATE,
18
20
  FIELD_SUBTYPE_TIME,
19
21
  FIELD_SUBTYPE_TIMESTAMP,
@@ -41,6 +43,9 @@ export function scalarKind(subType: string): string | null {
41
43
  switch (subType) {
42
44
  case FIELD_SUBTYPE_STRING:
43
45
  case FIELD_SUBTYPE_UUID:
46
+ // ADR-0036/0037 Wave 3: uri/inet are string scalars on the wire.
47
+ case FIELD_SUBTYPE_URI:
48
+ case FIELD_SUBTYPE_INET:
44
49
  case FIELD_SUBTYPE_DATE:
45
50
  case FIELD_SUBTYPE_TIME:
46
51
  case FIELD_SUBTYPE_TIMESTAMP:
@@ -71,7 +76,7 @@ export function fields(vo: MetaData): MetaData[] {
71
76
 
72
77
  /** isArray is a native (reserved) property on MetaData, not an attr. */
73
78
  export function isArray(field: MetaData): boolean {
74
- return field.isArray === true;
79
+ return field.resolvedIsArray();
75
80
  }
76
81
 
77
82
  /** True iff the field's @required is explicitly true (or the string "true"). */
@@ -25,6 +25,8 @@ import {
25
25
  FIELD_SUBTYPE_TIME,
26
26
  FIELD_SUBTYPE_TIMESTAMP,
27
27
  FIELD_SUBTYPE_UUID,
28
+ FIELD_SUBTYPE_URI,
29
+ FIELD_SUBTYPE_INET,
28
30
  FIELD_ATTR_REQUIRED,
29
31
  FIELD_ATTR_OBJECT_REF,
30
32
  FIELD_ATTR_VALUE_TYPE,
@@ -149,6 +151,10 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
149
151
  const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
150
152
  [FIELD_SUBTYPE_STRING]: "string",
151
153
  [FIELD_SUBTYPE_UUID]: "string",
154
+ // ADR-0036/0037 Wave 3: uri/inet bind to TS `string` (TS has no native URI/IP
155
+ // type, same as uuid). Other ports bind to their native URI/IP type.
156
+ [FIELD_SUBTYPE_URI]: "string",
157
+ [FIELD_SUBTYPE_INET]: "string",
152
158
  [FIELD_SUBTYPE_INT]: "number",
153
159
  [FIELD_SUBTYPE_LONG]: "number",
154
160
  [FIELD_SUBTYPE_DOUBLE]: "number",
@@ -184,15 +190,15 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
184
190
  // the column is a bare `jsonb()` returning any parsed JSON value, so the TS type
185
191
  // stays in lock-step with the `z.unknown()` Zod emission (NOT `string`).
186
192
  if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
187
- return field.isArray ? "unknown[]" : "unknown";
193
+ return field.resolvedIsArray() ? "unknown[]" : "unknown";
188
194
  }
189
195
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
190
196
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
191
197
  if (typeof ref === "string" && ref.length > 0) {
192
198
  const base = stripPackage(ref);
193
- return field.isArray ? `${base}[]` : base;
199
+ return field.resolvedIsArray() ? `${base}[]` : base;
194
200
  }
195
- return field.isArray ? "unknown[]" : "unknown";
201
+ return field.resolvedIsArray() ? "unknown[]" : "unknown";
196
202
  }
197
203
  if (field.subType === FIELD_SUBTYPE_ENUM) {
198
204
  const values = enumValues(field);
@@ -202,12 +208,12 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
202
208
  // self-contained (an agent sees the exact allowed values, not an opaque
203
209
  // alias name). Array enums wrap the parenthesized union: `(A | B)[]`.
204
210
  const union = enumUnionString(values);
205
- return field.isArray ? `(${union})[]` : union;
211
+ return field.resolvedIsArray() ? `(${union})[]` : union;
206
212
  }
207
- return field.isArray ? "string[]" : "string";
213
+ return field.resolvedIsArray() ? "string[]" : "string";
208
214
  }
209
215
  const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
210
- return field.isArray ? `${scalar}[]` : scalar;
216
+ return field.resolvedIsArray() ? `${scalar}[]` : scalar;
211
217
  }
212
218
 
213
219
  /**
@@ -219,7 +225,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
219
225
  // `@dbColumnType: jsonb` (open JSON bag) → `unknown`, in lock-step with
220
226
  // fieldTsTypeString above and the `z.unknown()` Zod emission.
221
227
  if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
222
- return field.isArray ? code`unknown[]` : code`unknown`;
228
+ return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
223
229
  }
224
230
  // field.object: import the referenced TS interface from its sibling module
225
231
  // so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
@@ -237,9 +243,9 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
237
243
  ? valueObjectModuleSpecifier(base, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
238
244
  : `./${base}.js`;
239
245
  const refImp = imp(`${base}@${moduleSpec}`);
240
- return field.isArray ? code`${refImp}[]` : code`${refImp}`;
246
+ return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
241
247
  }
242
- return field.isArray ? code`unknown[]` : code`unknown`;
248
+ return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
243
249
  }
244
250
 
245
251
  // field.map: Record<string, V> — V is a value-object (@objectRef) or a scalar (@valueType).
@@ -274,16 +280,16 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
274
280
  ? providedEnumImportSpecifier(ctx, shared.name)
275
281
  : sharedEnumImportSpecifier(ctx, entity.package);
276
282
  const sym = imp(`${shared.name}@${spec}`);
277
- return field.isArray ? code`${sym}[]` : code`${sym}`;
283
+ return field.resolvedIsArray() ? code`${sym}[]` : code`${sym}`;
278
284
  }
279
285
  }
280
- return field.isArray ? code`${alias}[]` : code`${alias}`;
286
+ return field.resolvedIsArray() ? code`${alias}[]` : code`${alias}`;
281
287
  }
282
- return field.isArray ? code`string[]` : code`string`;
288
+ return field.resolvedIsArray() ? code`string[]` : code`string`;
283
289
  }
284
290
 
285
291
  const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
286
- return field.isArray ? code`${scalar}[]` : code`${scalar}`;
292
+ return field.resolvedIsArray() ? code`${scalar}[]` : code`${scalar}`;
287
293
  }
288
294
 
289
295
  /**
@@ -42,12 +42,14 @@ export function specLiteral(vo: MetaData, template: MetaData, rootName: string):
42
42
  }
43
43
 
44
44
  function resolveFormat(template: MetaData): string {
45
- const f = template.ownAttr(TEMPLATE_ATTR_FORMAT);
45
+ // ADR-0039: resolving — a template may inherit @format via extends.
46
+ const f = template.attr(TEMPLATE_ATTR_FORMAT);
46
47
  return typeof f === "string" && f.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
47
48
  }
48
49
 
49
50
  function resolvePromptStyle(template: MetaData): string {
50
- switch (template.ownAttr(TEMPLATE_ATTR_PROMPT_STYLE)) {
51
+ // ADR-0039: resolving — a template may inherit @promptStyle via extends.
52
+ switch (template.attr(TEMPLATE_ATTR_PROMPT_STYLE)) {
51
53
  case PROMPT_STYLE_INLINE:
52
54
  return "PromptStyle.INLINE";
53
55
  case PROMPT_STYLE_EXAMPLE_ONLY:
@@ -92,6 +94,8 @@ function promptFieldLiteral(field: MetaData): string {
92
94
  }
93
95
 
94
96
  function optStringAttr(field: MetaData, attrName: string): string {
95
- const v = field.ownAttr(attrName);
97
+ // ADR-0039: resolving — a field may inherit @example/@instruction via extends
98
+ // (mirrors the Java OutputFormatSpecEmitter own→resolving flip).
99
+ const v = field.attr(attrName);
96
100
  return typeof v === "string" ? jsonStringLiteral(v) : "null";
97
101
  }
@@ -42,20 +42,22 @@ const SCALAR_ZOD: Record<string, string> = {
42
42
  boolean: "z.boolean()",
43
43
  };
44
44
 
45
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
45
46
  function findObject(root: MetaData, name: string): MetaData | undefined {
46
47
  // FR-032 — @payloadRef is FQN after the desugar/sweep; match on the effective
47
48
  // FQN resolution key (with bare back-compat).
48
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
49
+ return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
49
50
  }
50
51
 
52
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
51
53
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
52
- return root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
54
+ return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
53
55
  }
54
56
 
55
57
  /** Render the Zod expression for a single field; recurses on @objectRef. */
56
58
  function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
57
59
  // isArray is a native (reserved) property on MetaData, not an attr.
58
- const isArray = field.isArray === true;
60
+ const isArray = field.resolvedIsArray();
59
61
  let base: string;
60
62
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
61
63
  const refName = field.attr(FIELD_ATTR_OBJECT_REF);
@@ -100,7 +102,8 @@ export function renderOutputParser(root: MetaData, templateName: string): string
100
102
  if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
101
103
  throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
102
104
  }
103
- const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
105
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
106
+ const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
104
107
  if (typeof payloadRef !== "string") {
105
108
  throw new Error(`template "${templateName}" missing @payloadRef`);
106
109
  }
@@ -119,7 +122,8 @@ export function renderOutputParser(root: MetaData, templateName: string): string
119
122
  // FR-010: emit the tolerant extract() API alongside the strict Zod parser when the
120
123
  // template targets json/xml. The @payloadRef already resolved to a value-object above,
121
124
  // so a ExtractSchema can always be baked. text-format outputs get no extract.
122
- const format = (tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
125
+ // ADR-0039: resolving a template may inherit its @* refs/format/kind via extends.
126
+ const format = (tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
123
127
  const lc = format.toLowerCase();
124
128
  const emitExtractLenient = lc === "json" || lc === "xml";
125
129
 
@@ -21,17 +21,20 @@ import {
21
21
  } from "@metaobjectsdev/metadata";
22
22
  import { specLiteral } from "./output-format-spec-emitter.js";
23
23
 
24
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
24
25
  function findObject(root: MetaData, name: string): MetaData | undefined {
25
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
26
+ return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
26
27
  }
27
28
 
29
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
28
30
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
29
- return root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
31
+ return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
30
32
  }
31
33
 
32
34
  /** True iff the template.output's @format is json or xml (the renderable structured formats). */
33
35
  export function templateSupportsPrompt(tmpl: MetaData): boolean {
34
- const f = ((tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
36
+ // ADR-0039: resolving a template may inherit its @* refs/format/kind via extends.
37
+ const f = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
35
38
  return f === "json" || f === "xml";
36
39
  }
37
40
 
@@ -51,7 +54,8 @@ export function renderOutputPrompt(root: MetaData, templateName: string): string
51
54
  if (!templateSupportsPrompt(tmpl)) {
52
55
  throw new Error(`template "${templateName}" @format is not json/xml — no prompt fragment`);
53
56
  }
54
- const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
57
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
58
+ const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
55
59
  if (typeof payloadRef !== "string") {
56
60
  throw new Error(`template "${templateName}" missing @payloadRef`);
57
61
  }
@@ -131,6 +131,9 @@ export function renderProjectionDecl(
131
131
  for (const f of baseObj.fields()) allFields.push(f);
132
132
  }
133
133
  }
134
+ // ADR-0039: own — category 1 (emit-declared-here): the super's effective fields
135
+ // were already collected above; append only the projection's OWN new fields so
136
+ // inherited fields are not duplicated.
134
137
  for (const f of projection.ownFields()) allFields.push(f);
135
138
 
136
139
  // A field.object passthrough carries the value-object's Zod schema (so the
@@ -149,7 +152,7 @@ export function renderProjectionDecl(
149
152
  const refBase = objectRefOf(f);
150
153
  if (refBase) {
151
154
  const schemaSym = imp(`${refBase}InsertSchema@${voModule(refBase)}`);
152
- const base = f.isArray
155
+ const base = f.resolvedIsArray()
153
156
  ? code`${z}.array(${schemaSym})`
154
157
  : code`${schemaSym}`;
155
158
  return code` ${f.name}: ${base}${nullable}`;
@@ -15,6 +15,8 @@ import {
15
15
  renderCreateFn,
16
16
  renderUpdateFn,
17
17
  renderDeleteByIdFn,
18
+ renderReverseFinderFns,
19
+ reverseFksFor,
18
20
  getPkInfo,
19
21
  } from "./queries.js";
20
22
  import { pluralize, findByIdFnName, listFnName } from "../naming.js";
@@ -90,6 +92,16 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
90
92
  renderDeleteByIdFn(obj, ctx),
91
93
  ];
92
94
 
95
+ // ADR-0038 — reverse-relationship navigation via explicit FK finders. Each FK
96
+ // this entity holds (`identity.reference`) gets `find<Plural>By<FkField>` +
97
+ // its batched `…In` sibling, so the referenced entity can navigate back to its
98
+ // referencing rows with a plain, single-query, framework-free read. FK field
99
+ // names are unique within the entity, so the finder names never collide — the
100
+ // same-pair case (3 FKs to one target) yields 3 distinct finders.
101
+ for (const fk of reverseFksFor(obj)) {
102
+ sections.push(renderReverseFinderFns(obj, fk, ctx));
103
+ }
104
+
93
105
  // Render ts-poet body first, then prepend the @generated header so it lands
94
106
  // at line 1 ahead of any imports.
95
107
  const body = joinCode(sections, { on: "\n" }).toString();
@@ -178,6 +190,7 @@ export async function ${listFnName(entityName)}(db: Db, opts?: { limit?: number;
178
190
  function renderTphQueriesFile(base: MetaObject, ctx: RenderContext): string {
179
191
  const baseName = base.name;
180
192
  const tableVar = ctx.collectionName(baseName);
193
+ // ADR-0039: own — TPH discriminator, read own to identify the base level.
181
194
  const discField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR) as string;
182
195
  const { fieldName: pkField, tsType: pkType } = getPkInfo(base, ctx);
183
196
 
@@ -225,6 +238,7 @@ export async function list${pluralize(baseName)}(db: Db, opts?: { limit?: number
225
238
  // --- Per-subtype CRUD against the single base table ---
226
239
  const subtypeSections: Code[] = [];
227
240
  for (const sub of tphConcreteSubtypes(base, ctx.loadedRoot)) {
241
+ // ADR-0039: own — each concrete subtype declares its OWN @discriminatorValue.
228
242
  const value = sub.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) as string;
229
243
  const valueLit = JSON.stringify(value);
230
244
  const subFileSpec = entityModuleSpecifier(
@@ -2,7 +2,7 @@
2
2
  // Each returns a ts-poet Code block; composed into a file by queries-file.ts.
3
3
 
4
4
  import { code, imp, type Code } from "ts-poet";
5
- import type { MetaObject } from "@metaobjectsdev/metadata";
5
+ import { type MetaObject, stripPackage } from "@metaobjectsdev/metadata";
6
6
  import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
7
7
  import type { RenderContext } from "../render-context.js";
8
8
  import {
@@ -11,8 +11,19 @@ import {
11
11
  createFnName,
12
12
  updateFnName,
13
13
  deleteByIdFnName,
14
+ reverseFinderFnName,
15
+ reverseFinderInFnName,
14
16
  } from "../naming.js";
15
17
 
18
+ /** Map a field subType to the generated TS scalar type for keys/values. */
19
+ function subTypeToTsType(subType: string): "number" | "boolean" | "string" {
20
+ return subType === "long" || subType === "int" || subType === "short" || subType === "byte"
21
+ ? "number"
22
+ : subType === "boolean"
23
+ ? "boolean"
24
+ : "string";
25
+ }
26
+
16
27
  /** Get the PK field name and its TS type for a given entity. */
17
28
  export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
18
29
  // Use primaryIdentity() to find the primary identity (may be inherited from extends:/super:).
@@ -22,13 +33,7 @@ export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName:
22
33
  const pkFieldName = fields?.[0] ?? "id";
23
34
  const pkInfo = ctx.pkMap.get(entity.name);
24
35
  const subType = pkInfo?.fieldSubType ?? "long";
25
- const tsType =
26
- subType === "long" || subType === "int" || subType === "short" || subType === "byte"
27
- ? "number"
28
- : subType === "boolean"
29
- ? "boolean"
30
- : "string";
31
- return { fieldName: pkFieldName, tsType };
36
+ return { fieldName: pkFieldName, tsType: subTypeToTsType(subType) };
32
37
  }
33
38
 
34
39
  export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
@@ -114,3 +119,58 @@ export async function ${fnName}(db: Db, ${pkField}: ${pkType}): Promise<boolean>
114
119
  }
115
120
  `;
116
121
  }
122
+
123
+ /**
124
+ * One reverse FK finder pair (ADR-0038): the entity holding this FK (`E`) gains
125
+ * `find<EPlural>By<FkField>(value)` (single, `WHERE fk = ?`) and
126
+ * `find<EPlural>By<FkField>In(values)` (batched, `WHERE fk IN (…)`, anti-N+1) so
127
+ * the referenced entity `T` can navigate to its referencing `E` rows by calling
128
+ * the finder with a `T` id. Both are plain, framework-free, single-query reads.
129
+ */
130
+ interface ReverseFk {
131
+ /** FK FIELD name on this entity (logical), e.g. `currentSceneId`. */
132
+ fkField: string;
133
+ /** Target entity (the `T` referenced), e.g. `Scene`. Drives the value TS type. */
134
+ targetEntity: string;
135
+ }
136
+
137
+ /** Collect this entity's OWN reverse FK targets, in declaration order. */
138
+ export function reverseFksFor(entity: MetaObject): ReverseFk[] {
139
+ const out: ReverseFk[] = [];
140
+ for (const ref of entity.referenceIdentities()) {
141
+ const fkField = ref.fields[0];
142
+ const target = ref.targetEntity;
143
+ if (!fkField || !target) continue;
144
+ out.push({ fkField, targetEntity: stripPackage(target) });
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /** Render the single + batched reverse finders for one FK on `entity`. */
150
+ export function renderReverseFinderFns(entity: MetaObject, fk: ReverseFk, ctx: RenderContext): Code {
151
+ const varName = ctx.collectionName(entity.name);
152
+ const entityName = entity.name;
153
+ // The Drizzle table object is keyed by the LOGICAL field name (the DB column
154
+ // name is the argument to integer()/text()), so column access uses fk.fkField.
155
+ // The FK value type is the target entity's PK type (the FK references it). Fall
156
+ // back to the FK field's own subType, then to number (long-shaped key default).
157
+ const targetPk = ctx.pkMap.get(fk.targetEntity);
158
+ const ownField = entity.findField(fk.fkField);
159
+ const valueType = subTypeToTsType(targetPk?.fieldSubType ?? ownField?.subType ?? "long");
160
+
161
+ const singleName = reverseFinderFnName(entityName, fk.fkField);
162
+ const batchName = reverseFinderInFnName(entityName, fk.fkField);
163
+ const eqSym = imp("eq@drizzle-orm");
164
+ const inArraySym = imp("inArray@drizzle-orm");
165
+
166
+ return code`
167
+ export async function ${singleName}(db: Db, ${fk.fkField}: ${valueType}): Promise<${entityName}[]> {
168
+ return db.select().from(${varName}).where(${eqSym}(${varName}.${fk.fkField}, ${fk.fkField}));
169
+ }
170
+
171
+ export async function ${batchName}(db: Db, ${fk.fkField}s: ${valueType}[]): Promise<${entityName}[]> {
172
+ if (${fk.fkField}s.length === 0) return [];
173
+ return db.select().from(${varName}).where(${inArraySym}(${varName}.${fk.fkField}, ${fk.fkField}s));
174
+ }
175
+ `;
176
+ }
@@ -53,12 +53,14 @@ import {
53
53
  type VerifyError,
54
54
  } from "@metaobjectsdev/render";
55
55
 
56
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
56
57
  function findObject(root: MetaData, name: string): MetaData | undefined {
57
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
58
+ return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
58
59
  }
59
60
 
61
+ // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
60
62
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
61
- return root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
63
+ return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
62
64
  }
63
65
 
64
66
  /**
@@ -150,7 +152,8 @@ export function renderRenderHelper(
150
152
  `template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`,
151
153
  );
152
154
  }
153
- const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
155
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
156
+ const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
154
157
  if (typeof payloadRef !== "string") {
155
158
  throw new Error(`template "${templateName}" missing @payloadRef`);
156
159
  }
@@ -165,13 +168,15 @@ export function renderRenderHelper(
165
168
  const ft = fieldTreeLiteral(fields);
166
169
  const fnName = `render${templateName}`;
167
170
 
168
- const kind = ((tmpl.ownAttr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT)
171
+ // ADR-0039: resolving a template may inherit its @* refs/format/kind via extends.
172
+ const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT)
169
173
  .toLowerCase();
170
174
 
171
175
  if (kind === TEMPLATE_KIND_EMAIL) {
172
- const subjectRef = tmpl.ownAttr(TEMPLATE_ATTR_SUBJECT_REF);
173
- const htmlBodyRef = tmpl.ownAttr(TEMPLATE_ATTR_HTML_BODY_REF);
174
- const textBodyRef = tmpl.ownAttr(TEMPLATE_ATTR_TEXT_BODY_REF);
176
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
177
+ const subjectRef = tmpl.attr(TEMPLATE_ATTR_SUBJECT_REF);
178
+ const htmlBodyRef = tmpl.attr(TEMPLATE_ATTR_HTML_BODY_REF);
179
+ const textBodyRef = tmpl.attr(TEMPLATE_ATTR_TEXT_BODY_REF);
175
180
  if (typeof subjectRef !== "string") {
176
181
  throw new Error(`template "${templateName}" (email) missing @subjectRef`);
177
182
  }
@@ -210,12 +215,14 @@ export function ${fnName}(payload: ${payloadRef}, provider: Provider): EmailDocu
210
215
  }
211
216
 
212
217
  // --- document kind ---
213
- const textRef = tmpl.ownAttr(TEMPLATE_ATTR_TEXT_REF);
218
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
219
+ const textRef = tmpl.attr(TEMPLATE_ATTR_TEXT_REF);
214
220
  if (typeof textRef !== "string") {
215
221
  throw new Error(`template "${templateName}" (document) missing @textRef`);
216
222
  }
217
- const format = ((tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
218
- const maxCharsAttr = tmpl.ownAttr(TEMPLATE_ATTR_MAX_CHARS);
223
+ // ADR-0039: resolving a template may inherit its @* refs/format/kind via extends.
224
+ const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
225
+ const maxCharsAttr = tmpl.attr(TEMPLATE_ATTR_MAX_CHARS);
219
226
  const maxChars =
220
227
  typeof maxCharsAttr === "number"
221
228
  ? maxCharsAttr
@@ -285,7 +285,8 @@ function renderM2mMount(
285
285
 
286
286
  /** Resolve a field's physical column name on an entity (defaults if missing). */
287
287
  function resolveJunctionColumn(entity: MetaObject, fieldName: string, ctx: RenderContext): string {
288
- const field = entity.ownChildren().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
288
+ // ADR-0039: resolving a junction FK field may be inherited via extends.
289
+ const field = entity.children().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
289
290
  if (!field) return fieldName;
290
291
  return resolveColumnName(field, ctx.columnNamingStrategy);
291
292
  }
@@ -74,6 +74,9 @@ export function tphRouteSegment(discriminatorValue: string): string {
74
74
  export function tphPlan(base: MetaObject, root: MetaRoot): TphPlan | null {
75
75
  const cached = _tphPlanCache.get(base);
76
76
  if (cached !== undefined) return cached;
77
+ // ADR-0039: own — TPH super-resolution walk (mirrors C# TphPlan). @discriminator
78
+ // must be read own to identify WHICH hierarchy level owns it (the discriminator
79
+ // base); a subtype must not be treated as a base by inheriting it.
77
80
  const discriminatorField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
78
81
  let plan: TphPlan | null = null;
79
82
  if (typeof discriminatorField === "string" && discriminatorField !== "") {
@@ -117,6 +120,7 @@ export function tphConcreteSubtypes(base: MetaObject, root: MetaRoot): MetaObjec
117
120
  * caller emits each as a nullable column (rows of other subtypes store NULL).
118
121
  */
119
122
  export function collectTphSubtypeFields(base: MetaObject, root: MetaRoot): MetaField[] {
123
+ // ADR-0039: own — TPH super-resolution walk; @discriminator identifies the base level.
120
124
  const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
121
125
  if (typeof discFieldName !== "string" || discFieldName === "") return [];
122
126
 
@@ -140,6 +144,7 @@ export function renderTphDiscriminatorUnion(
140
144
  base: MetaObject,
141
145
  root: MetaRoot,
142
146
  ): Code | null {
147
+ // ADR-0039: own — TPH super-resolution walk; @discriminator identifies the base level.
143
148
  const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
144
149
  if (typeof discFieldName !== "string" || discFieldName === "") return null;
145
150
 
@@ -209,6 +214,8 @@ function collectConcreteSubtypes(base: MetaObject, root: MetaRoot): SubtypeBindi
209
214
  if (obj.isAbstract === true) continue;
210
215
  if (obj === base) continue;
211
216
 
217
+ // ADR-0039: own — each concrete subtype declares its OWN @discriminatorValue;
218
+ // it must NOT inherit a parent's value (super-resolution walk, category 3).
212
219
  const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
213
220
  if (typeof value !== "string" || value === "") continue;
214
221