@metaobjectsdev/codegen-ts 0.9.0 → 0.11.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 (342) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts +12 -6
  3. package/dist/column-mapper.d.ts.map +1 -1
  4. package/dist/column-mapper.js +68 -28
  5. package/dist/column-mapper.js.map +1 -1
  6. package/dist/constants.d.ts +8 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +16 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/docs-paths.d.ts +58 -0
  11. package/dist/docs-paths.d.ts.map +1 -0
  12. package/dist/docs-paths.js +89 -0
  13. package/dist/docs-paths.js.map +1 -0
  14. package/dist/enum-import.d.ts +14 -0
  15. package/dist/enum-import.d.ts.map +1 -0
  16. package/dist/enum-import.js +35 -0
  17. package/dist/enum-import.js.map +1 -0
  18. package/dist/enum-shared.d.ts +32 -0
  19. package/dist/enum-shared.d.ts.map +1 -0
  20. package/dist/enum-shared.js +83 -0
  21. package/dist/enum-shared.js.map +1 -0
  22. package/dist/generator-registry.d.ts +22 -0
  23. package/dist/generator-registry.d.ts.map +1 -0
  24. package/dist/generator-registry.js +161 -0
  25. package/dist/generator-registry.js.map +1 -0
  26. package/dist/generator.d.ts +6 -0
  27. package/dist/generator.d.ts.map +1 -1
  28. package/dist/generator.js.map +1 -1
  29. package/dist/generators/api-doc-render.d.ts +17 -0
  30. package/dist/generators/api-doc-render.d.ts.map +1 -0
  31. package/dist/generators/api-doc-render.js +431 -0
  32. package/dist/generators/api-doc-render.js.map +1 -0
  33. package/dist/generators/api-docs-file.d.ts +21 -0
  34. package/dist/generators/api-docs-file.d.ts.map +1 -0
  35. package/dist/generators/api-docs-file.js +112 -0
  36. package/dist/generators/api-docs-file.js.map +1 -0
  37. package/dist/generators/api-field-shape.d.ts +39 -0
  38. package/dist/generators/api-field-shape.d.ts.map +1 -0
  39. package/dist/generators/api-field-shape.js +92 -0
  40. package/dist/generators/api-field-shape.js.map +1 -0
  41. package/dist/generators/api-label.d.ts +3 -0
  42. package/dist/generators/api-label.d.ts.map +1 -0
  43. package/dist/generators/api-label.js +8 -0
  44. package/dist/generators/api-label.js.map +1 -0
  45. package/dist/generators/api-model.d.ts +122 -0
  46. package/dist/generators/api-model.d.ts.map +1 -0
  47. package/dist/generators/api-model.js +809 -0
  48. package/dist/generators/api-model.js.map +1 -0
  49. package/dist/generators/docs-data-builder.d.ts +26 -4
  50. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  51. package/dist/generators/docs-data-builder.js +439 -167
  52. package/dist/generators/docs-data-builder.js.map +1 -1
  53. package/dist/generators/docs-data.d.ts +136 -27
  54. package/dist/generators/docs-data.d.ts.map +1 -1
  55. package/dist/generators/docs-data.js +1 -1
  56. package/dist/generators/docs-data.js.map +1 -1
  57. package/dist/generators/docs-file.d.ts +19 -0
  58. package/dist/generators/docs-file.d.ts.map +1 -1
  59. package/dist/generators/docs-file.js +154 -27
  60. package/dist/generators/docs-file.js.map +1 -1
  61. package/dist/generators/entity-file.d.ts.map +1 -1
  62. package/dist/generators/entity-file.js +29 -14
  63. package/dist/generators/entity-file.js.map +1 -1
  64. package/dist/generators/extractor-file.d.ts.map +1 -1
  65. package/dist/generators/extractor-file.js +2 -1
  66. package/dist/generators/extractor-file.js.map +1 -1
  67. package/dist/generators/field-anchor.d.ts +7 -0
  68. package/dist/generators/field-anchor.d.ts.map +1 -0
  69. package/dist/generators/field-anchor.js +23 -0
  70. package/dist/generators/field-anchor.js.map +1 -0
  71. package/dist/generators/index.d.ts +8 -1
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +6 -0
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/mermaid-er.d.ts +14 -0
  76. package/dist/generators/mermaid-er.d.ts.map +1 -1
  77. package/dist/generators/mermaid-er.js +14 -0
  78. package/dist/generators/mermaid-er.js.map +1 -1
  79. package/dist/generators/output-parser-file.d.ts.map +1 -1
  80. package/dist/generators/output-parser-file.js +3 -4
  81. package/dist/generators/output-parser-file.js.map +1 -1
  82. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  83. package/dist/generators/output-prompt-file.js +2 -2
  84. package/dist/generators/output-prompt-file.js.map +1 -1
  85. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  86. package/dist/generators/prompt-render-file.js +3 -4
  87. package/dist/generators/prompt-render-file.js.map +1 -1
  88. package/dist/generators/queries-file.d.ts.map +1 -1
  89. package/dist/generators/queries-file.js +8 -3
  90. package/dist/generators/queries-file.js.map +1 -1
  91. package/dist/generators/render-helper-file.d.ts.map +1 -1
  92. package/dist/generators/render-helper-file.js +2 -2
  93. package/dist/generators/render-helper-file.js.map +1 -1
  94. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  95. package/dist/generators/routes-file-hono.js +5 -1
  96. package/dist/generators/routes-file-hono.js.map +1 -1
  97. package/dist/generators/routes-file.d.ts +3 -0
  98. package/dist/generators/routes-file.d.ts.map +1 -1
  99. package/dist/generators/routes-file.js +6 -1
  100. package/dist/generators/routes-file.js.map +1 -1
  101. package/dist/generators/template-doc-builder.d.ts +19 -0
  102. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  103. package/dist/generators/template-doc-builder.js +220 -0
  104. package/dist/generators/template-doc-builder.js.map +1 -0
  105. package/dist/generators/template-doc-data.d.ts +62 -0
  106. package/dist/generators/template-doc-data.d.ts.map +1 -0
  107. package/dist/generators/template-doc-data.js +16 -0
  108. package/dist/generators/template-doc-data.js.map +1 -0
  109. package/dist/generators/template-payload-tree.d.ts +15 -0
  110. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  111. package/dist/generators/template-payload-tree.js +61 -0
  112. package/dist/generators/template-payload-tree.js.map +1 -0
  113. package/dist/generators/template-source-annotate.d.ts +74 -0
  114. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  115. package/dist/generators/template-source-annotate.js +184 -0
  116. package/dist/generators/template-source-annotate.js.map +1 -0
  117. package/dist/generators/template-source-render.d.ts +24 -0
  118. package/dist/generators/template-source-render.d.ts.map +1 -0
  119. package/dist/generators/template-source-render.js +175 -0
  120. package/dist/generators/template-source-render.js.map +1 -0
  121. package/dist/generators/trace-helper-file.d.ts +9 -0
  122. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  123. package/dist/generators/trace-helper-file.js +196 -0
  124. package/dist/generators/trace-helper-file.js.map +1 -0
  125. package/dist/import-path.d.ts +18 -0
  126. package/dist/import-path.d.ts.map +1 -1
  127. package/dist/import-path.js +21 -0
  128. package/dist/import-path.js.map +1 -1
  129. package/dist/index.d.ts +29 -4
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +28 -2
  132. package/dist/index.js.map +1 -1
  133. package/dist/metaobjects-config.d.ts +101 -2
  134. package/dist/metaobjects-config.d.ts.map +1 -1
  135. package/dist/metaobjects-config.js +46 -0
  136. package/dist/metaobjects-config.js.map +1 -1
  137. package/dist/naming.d.ts +39 -2
  138. package/dist/naming.d.ts.map +1 -1
  139. package/dist/naming.js +52 -3
  140. package/dist/naming.js.map +1 -1
  141. package/dist/payload-codegen.d.ts.map +1 -1
  142. package/dist/payload-codegen.js +14 -6
  143. package/dist/payload-codegen.js.map +1 -1
  144. package/dist/pk-resolver.d.ts.map +1 -1
  145. package/dist/pk-resolver.js +4 -2
  146. package/dist/pk-resolver.js.map +1 -1
  147. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  148. package/dist/projection/extract-view-spec.js +52 -26
  149. package/dist/projection/extract-view-spec.js.map +1 -1
  150. package/dist/relation-resolver.d.ts +16 -0
  151. package/dist/relation-resolver.d.ts.map +1 -1
  152. package/dist/relation-resolver.js +82 -1
  153. package/dist/relation-resolver.js.map +1 -1
  154. package/dist/render-context.d.ts +25 -2
  155. package/dist/render-context.d.ts.map +1 -1
  156. package/dist/render-context.js +7 -0
  157. package/dist/render-context.js.map +1 -1
  158. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  159. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  160. package/dist/render-engine/embedded-templates.generated.js +15 -0
  161. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  162. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  163. package/dist/render-engine/framework-provider.js +26 -13
  164. package/dist/render-engine/framework-provider.js.map +1 -1
  165. package/dist/runner.d.ts.map +1 -1
  166. package/dist/runner.js +20 -0
  167. package/dist/runner.js.map +1 -1
  168. package/dist/templates/docs-file.d.ts +2 -6
  169. package/dist/templates/docs-file.d.ts.map +1 -1
  170. package/dist/templates/docs-file.js +2 -5
  171. package/dist/templates/docs-file.js.map +1 -1
  172. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  173. package/dist/templates/drizzle-schema.js +72 -23
  174. package/dist/templates/drizzle-schema.js.map +1 -1
  175. package/dist/templates/entity-constants.d.ts +7 -0
  176. package/dist/templates/entity-constants.d.ts.map +1 -1
  177. package/dist/templates/entity-constants.js +3 -3
  178. package/dist/templates/entity-constants.js.map +1 -1
  179. package/dist/templates/entity-file.d.ts.map +1 -1
  180. package/dist/templates/entity-file.js +16 -5
  181. package/dist/templates/entity-file.js.map +1 -1
  182. package/dist/templates/enums-file.d.ts +11 -0
  183. package/dist/templates/enums-file.d.ts.map +1 -0
  184. package/dist/templates/enums-file.js +44 -0
  185. package/dist/templates/enums-file.js.map +1 -0
  186. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  187. package/dist/templates/extract-delegate-emitter.js +6 -8
  188. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  189. package/dist/templates/extractor.d.ts.map +1 -1
  190. package/dist/templates/extractor.js +58 -41
  191. package/dist/templates/extractor.js.map +1 -1
  192. package/dist/templates/field-meta.d.ts.map +1 -1
  193. package/dist/templates/field-meta.js +2 -6
  194. package/dist/templates/field-meta.js.map +1 -1
  195. package/dist/templates/filter-allowlist.d.ts +7 -2
  196. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  197. package/dist/templates/filter-allowlist.js +18 -10
  198. package/dist/templates/filter-allowlist.js.map +1 -1
  199. package/dist/templates/filter-shared.js +2 -2
  200. package/dist/templates/filter-shared.js.map +1 -1
  201. package/dist/templates/filter-type.d.ts +7 -1
  202. package/dist/templates/filter-type.d.ts.map +1 -1
  203. package/dist/templates/filter-type.js +10 -6
  204. package/dist/templates/filter-type.js.map +1 -1
  205. package/dist/templates/find-templates.d.ts +4 -0
  206. package/dist/templates/find-templates.d.ts.map +1 -0
  207. package/dist/templates/find-templates.js +15 -0
  208. package/dist/templates/find-templates.js.map +1 -0
  209. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  210. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  211. package/dist/templates/fr010-field-mapping.js +15 -11
  212. package/dist/templates/fr010-field-mapping.js.map +1 -1
  213. package/dist/templates/inferred-types.d.ts +44 -7
  214. package/dist/templates/inferred-types.d.ts.map +1 -1
  215. package/dist/templates/inferred-types.js +121 -19
  216. package/dist/templates/inferred-types.js.map +1 -1
  217. package/dist/templates/mermaid-er.d.ts +35 -2
  218. package/dist/templates/mermaid-er.d.ts.map +1 -1
  219. package/dist/templates/mermaid-er.js +174 -7
  220. package/dist/templates/mermaid-er.js.map +1 -1
  221. package/dist/templates/output-format-spec-emitter.js +1 -1
  222. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  223. package/dist/templates/output-parser.d.ts.map +1 -1
  224. package/dist/templates/output-parser.js +31 -80
  225. package/dist/templates/output-parser.js.map +1 -1
  226. package/dist/templates/output-prompt.d.ts.map +1 -1
  227. package/dist/templates/output-prompt.js +2 -2
  228. package/dist/templates/output-prompt.js.map +1 -1
  229. package/dist/templates/queries-file.d.ts.map +1 -1
  230. package/dist/templates/queries-file.js +113 -5
  231. package/dist/templates/queries-file.js.map +1 -1
  232. package/dist/templates/queries.d.ts +7 -2
  233. package/dist/templates/queries.d.ts.map +1 -1
  234. package/dist/templates/queries.js +15 -15
  235. package/dist/templates/queries.js.map +1 -1
  236. package/dist/templates/relations-block.d.ts.map +1 -1
  237. package/dist/templates/relations-block.js +12 -3
  238. package/dist/templates/relations-block.js.map +1 -1
  239. package/dist/templates/render-helper.d.ts.map +1 -1
  240. package/dist/templates/render-helper.js +5 -5
  241. package/dist/templates/render-helper.js.map +1 -1
  242. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  243. package/dist/templates/routes-file-hono.js +1 -2
  244. package/dist/templates/routes-file-hono.js.map +1 -1
  245. package/dist/templates/routes-file.d.ts.map +1 -1
  246. package/dist/templates/routes-file.js +184 -7
  247. package/dist/templates/routes-file.js.map +1 -1
  248. package/dist/templates/tph-discriminator.d.ts +56 -0
  249. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  250. package/dist/templates/tph-discriminator.js +180 -0
  251. package/dist/templates/tph-discriminator.js.map +1 -0
  252. package/dist/templates/value-object-file.d.ts +2 -1
  253. package/dist/templates/value-object-file.d.ts.map +1 -1
  254. package/dist/templates/value-object-file.js +33 -5
  255. package/dist/templates/value-object-file.js.map +1 -1
  256. package/dist/templates/zod-validators.d.ts +65 -2
  257. package/dist/templates/zod-validators.d.ts.map +1 -1
  258. package/dist/templates/zod-validators.js +202 -22
  259. package/dist/templates/zod-validators.js.map +1 -1
  260. package/package.json +103 -34
  261. package/src/column-mapper.ts +79 -32
  262. package/src/constants.ts +18 -0
  263. package/src/docs-paths.ts +128 -0
  264. package/src/enum-import.ts +43 -0
  265. package/src/enum-shared.ts +95 -0
  266. package/src/generator-registry.ts +204 -0
  267. package/src/generator.ts +6 -0
  268. package/src/generators/api-doc-render.ts +572 -0
  269. package/src/generators/api-docs-file.ts +146 -0
  270. package/src/generators/api-field-shape.ts +114 -0
  271. package/src/generators/api-label.ts +7 -0
  272. package/src/generators/api-model.ts +1067 -0
  273. package/src/generators/docs-data-builder.ts +483 -189
  274. package/src/generators/docs-data.ts +139 -28
  275. package/src/generators/docs-file.ts +205 -39
  276. package/src/generators/entity-file.ts +31 -15
  277. package/src/generators/extractor-file.ts +2 -1
  278. package/src/generators/field-anchor.ts +24 -0
  279. package/src/generators/index.ts +8 -1
  280. package/src/generators/mermaid-er.ts +14 -0
  281. package/src/generators/output-parser-file.ts +3 -4
  282. package/src/generators/output-prompt-file.ts +2 -1
  283. package/src/generators/prompt-render-file.ts +3 -4
  284. package/src/generators/queries-file.ts +9 -3
  285. package/src/generators/render-helper-file.ts +2 -1
  286. package/src/generators/routes-file-hono.ts +5 -1
  287. package/src/generators/routes-file.ts +7 -1
  288. package/src/generators/template-doc-builder.ts +306 -0
  289. package/src/generators/template-doc-data.ts +85 -0
  290. package/src/generators/template-payload-tree.ts +71 -0
  291. package/src/generators/template-source-annotate.ts +290 -0
  292. package/src/generators/template-source-render.ts +203 -0
  293. package/src/generators/trace-helper-file.ts +301 -0
  294. package/src/import-path.ts +28 -0
  295. package/src/index.ts +55 -4
  296. package/src/metaobjects-config.ts +146 -2
  297. package/src/naming.ts +73 -3
  298. package/src/payload-codegen.ts +16 -5
  299. package/src/pk-resolver.ts +4 -2
  300. package/src/projection/extract-view-spec.ts +50 -31
  301. package/src/relation-resolver.ts +103 -1
  302. package/src/render-context.ts +32 -2
  303. package/src/render-engine/embedded-templates.generated.ts +14 -0
  304. package/src/render-engine/framework-provider.ts +25 -11
  305. package/src/runner.ts +24 -0
  306. package/src/templates/docs-file.ts +2 -9
  307. package/src/templates/drizzle-schema.ts +80 -28
  308. package/src/templates/entity-constants.ts +3 -3
  309. package/src/templates/entity-file.ts +16 -5
  310. package/src/templates/enums-file.ts +50 -0
  311. package/src/templates/extract-delegate-emitter.ts +6 -7
  312. package/src/templates/extractor.ts +70 -40
  313. package/src/templates/field-meta.ts +1 -7
  314. package/src/templates/filter-allowlist.ts +18 -11
  315. package/src/templates/filter-shared.ts +2 -2
  316. package/src/templates/filter-type.ts +9 -7
  317. package/src/templates/find-templates.ts +15 -0
  318. package/src/templates/fr010-field-mapping.ts +15 -13
  319. package/src/templates/inferred-types.ts +122 -21
  320. package/src/templates/mermaid-er.ts +176 -8
  321. package/src/templates/output-format-spec-emitter.ts +1 -1
  322. package/src/templates/output-parser.ts +31 -80
  323. package/src/templates/output-prompt.ts +2 -1
  324. package/src/templates/queries-file.ts +133 -4
  325. package/src/templates/queries.ts +21 -15
  326. package/src/templates/relations-block.ts +19 -3
  327. package/src/templates/render-helper.ts +5 -4
  328. package/src/templates/routes-file-hono.ts +1 -2
  329. package/src/templates/routes-file.ts +234 -7
  330. package/src/templates/tph-discriminator.ts +232 -0
  331. package/src/templates/value-object-file.ts +39 -5
  332. package/src/templates/zod-validators.ts +225 -21
  333. package/templates/api/agent-api.md.mustache +30 -0
  334. package/templates/api/entity-api.md.mustache +69 -0
  335. package/templates/api/index.md.mustache +21 -0
  336. package/templates/docs/entity-page.md.mustache +33 -21
  337. package/templates/docs/template-page.md.mustache +56 -0
  338. package/dist/templates/extract-schema-emitter.d.ts +0 -8
  339. package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
  340. package/dist/templates/extract-schema-emitter.js +0 -81
  341. package/dist/templates/extract-schema-emitter.js.map +0 -1
  342. package/src/templates/extract-schema-emitter.ts +0 -111
@@ -22,15 +22,11 @@ export interface DocsRenderOpts {
22
22
  dialect: Dialect;
23
23
  columnNamingStrategy?: ColumnNamingStrategy;
24
24
  loadedRoot: MetaRoot;
25
- /** Names of generators present in the pipeline — drives the "Generated code"
26
- * section. Always includes "entity-file" implicitly. Recognized names:
27
- * "queries-file", "routes-file", "routes-file-hono". */
28
- generatorNames?: ReadonlySet<string>;
29
25
  }
30
26
 
31
27
  /** Backward-compatible entry point: builds the EntityDocData payload and
32
- * renders it via the framework template. Byte-identical to the hand-coded
33
- * rc.11 output (gated by `docs-file-conformance.test.ts`). */
28
+ * renders it via the framework template. Output is gated by
29
+ * `docs-file-conformance.test.ts`. */
34
30
  export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string {
35
31
  const data = buildEntityDocData(entity, {
36
32
  dialect: opts.dialect,
@@ -38,9 +34,6 @@ export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string
38
34
  ? { columnNamingStrategy: opts.columnNamingStrategy }
39
35
  : {}),
40
36
  loadedRoot: opts.loadedRoot,
41
- ...(opts.generatorNames !== undefined
42
- ? { generatorNames: opts.generatorNames }
43
- : {}),
44
37
  });
45
38
  return render({
46
39
  ref: "docs/entity-page.md",
@@ -3,7 +3,7 @@
3
3
  // plus the relations() block auto-emitted at the end.
4
4
 
5
5
  import { code, imp, joinCode, type Code } from "ts-poet";
6
- import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
6
+ import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
7
7
  import {
8
8
  IDENTITY_SUBTYPE_SECONDARY, FIELD_SUBTYPE_LONG,
9
9
  IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, IDENTITY_ATTR_UNIQUE,
@@ -11,11 +11,12 @@ import {
11
11
  FIELD_ATTR_AUTO_SET,
12
12
  } from "@metaobjectsdev/metadata";
13
13
  import { type RenderContext } from "../render-context.js";
14
- import { crossEntitySpecifier } from "../import-path.js";
14
+ import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
15
15
  import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
16
- import { tableNameFromEntity, variableNameFromEntity, columnNameFromField } from "../naming.js";
16
+ import { tableNameFromEntity, columnNameFromField } from "../naming.js";
17
17
  import { renderRelationsBlock } from "./relations-block.js";
18
18
  import { renderDocsFor } from "./jsdoc.js";
19
+ import { collectTphSubtypeFields } from "./tph-discriminator.js";
19
20
 
20
21
  /**
21
22
  * Render the Drizzle table definition for one entity, including:
@@ -32,17 +33,17 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
32
33
  const tableFnSym = imp(`${tableFn}@${importModule}`);
33
34
 
34
35
  const tableName = obj.dbTable ?? tableNameFromEntity(obj.name, ctx.columnNamingStrategy);
35
- const varName = variableNameFromEntity(obj.name);
36
+ const varName = ctx.collectionName(obj.name);
36
37
 
37
38
  const primary = obj.primaryIdentity();
38
- const rawPkFields = primary?.ownAttr(IDENTITY_ATTR_FIELDS);
39
+ const rawPkFields = primary?.attr(IDENTITY_ATTR_FIELDS);
39
40
  const pkFieldsList: string[] = Array.isArray(rawPkFields)
40
41
  ? rawPkFields as string[]
41
42
  : typeof rawPkFields === "string"
42
43
  ? rawPkFields.split(",").map((f) => f.trim()).filter(Boolean)
43
44
  : [];
44
45
  const pkFieldNames = new Set<string>(pkFieldsList);
45
- const pkGeneration = primary?.ownAttr(IDENTITY_ATTR_GENERATION) as string | undefined;
46
+ const pkGeneration = primary?.attr(IDENTITY_ATTR_GENERATION) as string | undefined;
46
47
 
47
48
  const fkMap = buildFkMapForEntity(obj, ctx);
48
49
 
@@ -55,9 +56,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
55
56
  const secondaryIdentities = obj.secondaryIdentities();
56
57
  const uniqueFieldNames = new Set<string>();
57
58
  for (const sec of secondaryIdentities) {
58
- const uniqueAttr = sec.ownAttr(IDENTITY_ATTR_UNIQUE);
59
+ const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
59
60
  if (uniqueAttr === false) continue; // explicit non-unique → don't mark column
60
- const fields = sec.ownAttr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
61
+ const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
61
62
  if (!Array.isArray(fields) || fields.length !== 1) continue; // multi-col uniques use a callback index, not a column flag
62
63
  uniqueFieldNames.add(fields[0]!);
63
64
  }
@@ -71,9 +72,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
71
72
  const fkInfo = fkMap.get(child.name);
72
73
  // Compute the column spec once per field and reuse it for both the column
73
74
  // line and the CHECK collection.
74
- const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
75
+ const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
75
76
  const fieldDocs = renderDocsFor(child);
76
- const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package);
77
+ const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package, obj.name);
77
78
  columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
78
79
  if (spec.checkConstraint !== undefined) {
79
80
  checkConstraints.push({
@@ -83,6 +84,29 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
83
84
  }
84
85
  }
85
86
 
87
+ // FR-017 Tier 2 — TPH single-table inheritance. When this entity is a
88
+ // discriminator base, fold every concrete subtype's own columns into this
89
+ // one table. Subtype-only columns are ALWAYS nullable (a row of any other
90
+ // subtype stores NULL there) and never carry a DB default (a default would
91
+ // stamp onto other-subtype inserts), regardless of the field's @required.
92
+ // Subtype entities emit no table of their own (the value-object path).
93
+ for (const child of collectTphSubtypeFields(obj, ctx.loadedRoot)) {
94
+ const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
95
+ const fieldDocs = renderDocsFor(child);
96
+ const columnLine = renderColumn(
97
+ spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, obj.name, true,
98
+ );
99
+ columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
100
+ // Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
101
+ // (not false), so other-subtype rows with NULL pass the check.
102
+ if (spec.checkConstraint !== undefined) {
103
+ checkConstraints.push({
104
+ name: `chk_${tableName}_${spec.dbName}`,
105
+ expr: spec.checkConstraint,
106
+ });
107
+ }
108
+ }
109
+
86
110
  // Build all table callback entries
87
111
  const callbackEntries: Code[] = [];
88
112
 
@@ -91,13 +115,13 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
91
115
  }
92
116
 
93
117
  for (const sec of secondaryIdentities) {
94
- const fields = sec.ownAttr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
118
+ const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
95
119
  if (!Array.isArray(fields) || fields.length === 0) continue;
96
120
  const indexName = `idx_${tableName}_${fields.map((f) => columnNameFromField(f, ctx.columnNamingStrategy)).join("_")}`;
97
121
  // @unique on the identity defaults to true (preserves back-compat with
98
122
  // foundations fixtures that assumed secondary identities were always
99
123
  // unique). Explicit @unique: false → ordinary non-unique index.
100
- const uniqueAttr = sec.ownAttr(IDENTITY_ATTR_UNIQUE);
124
+ const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
101
125
  const isUnique = uniqueAttr !== false;
102
126
  const indexFn = isUnique ? "uniqueIndex" : "index";
103
127
  const indexSym = imp(`${indexFn}@${importModule}`);
@@ -160,11 +184,15 @@ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, F
160
184
  const fkField = fkFieldNames[0]!;
161
185
  const targetName = ref.targetEntity;
162
186
  if (!targetName) continue;
163
- const targetObj = ctx.loadedRoot.findObject(targetName);
187
+ // @references may be authored bare OR package-qualified, and the loader can
188
+ // resolve it to an FQN (e.g. the YAML front-end qualifies it). Strip the
189
+ // package so the lookup matches the object's bare name — mirrors the
190
+ // relation-resolver, which already does this.
191
+ const targetObj = ctx.loadedRoot.findObject(stripPackage(targetName));
164
192
  if (!targetObj) continue;
165
193
  const targetPkField = ref.resolvedTargetPkField(ctx.loadedRoot) ?? "id";
166
194
  result.set(fkField, {
167
- targetVarName: variableNameFromEntity(targetObj.name),
195
+ targetVarName: ctx.collectionName(targetObj.name),
168
196
  targetEntityName: targetObj.name,
169
197
  targetPkField,
170
198
  });
@@ -212,6 +240,12 @@ function renderColumn(
212
240
  isComposite: boolean,
213
241
  isUnique: boolean = false,
214
242
  entityPackage: string | undefined = undefined,
243
+ // Name of the entity this column belongs to — used to detect a self-referential
244
+ // FK (target entity === this entity), which Drizzle emits without a self-import.
245
+ currentEntityName: string = "",
246
+ // FR-017 Tier 2 — TPH subtype-only column: force nullable (drop .notNull())
247
+ // and suppress any DB default (other-subtype rows must stay NULL here).
248
+ forceNullable: boolean = false,
215
249
  ): Code {
216
250
  const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
217
251
 
@@ -261,6 +295,9 @@ function renderColumn(
261
295
  if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
262
296
  // Avoid double-emitting .unique() if it was already appended above.
263
297
  if (isUnique && m === ".unique()") continue;
298
+ // TPH subtype-only column: never .notNull() / .unique() — rows of other
299
+ // subtypes store NULL, so neither constraint can hold across the table.
300
+ if (forceNullable && (m === ".notNull()" || m === ".unique()")) continue;
264
301
  modifiersStr += m;
265
302
  }
266
303
 
@@ -268,7 +305,7 @@ function renderColumn(
268
305
  // the `sql` import via imp(); a raw `.default(sql`...`)` would leave `sql`
269
306
  // unresolved in the generated file.
270
307
  let sqlDefaultSegment: Code | null = null;
271
- if (spec.defaultExpr !== undefined && !isPk) {
308
+ if (spec.defaultExpr !== undefined && !isPk && !forceNullable) {
272
309
  if (spec.defaultExpr.kind === "now") {
273
310
  if (ctx.dialect === "sqlite") {
274
311
  const sqlSym = imp("sql@drizzle-orm");
@@ -289,21 +326,30 @@ function renderColumn(
289
326
  // FK .references() uses imp() so ts-poet tracks the cross-entity import.
290
327
  let fkRefSegment: Code | null = null;
291
328
  if (fkInfo !== undefined && !isPk) {
292
- const targetSpec = crossEntitySpecifier(
293
- ctx.outputLayout,
294
- entityPackage,
295
- ctx.packageOf.get(fkInfo.targetEntityName),
296
- fkInfo.targetEntityName,
297
- ctx.extStyle,
298
- );
299
- const targetVarSym = imp(`${fkInfo.targetVarName}@${targetSpec}`);
300
- fkRefSegment = code`.references(() => ${targetVarSym}.${fkInfo.targetPkField})`;
329
+ if (fkInfo.targetEntityName === currentEntityName) {
330
+ // Self-referential FK (e.g. createdBy → this same table). Drizzle requires
331
+ // referencing the local table const directly — NOT a self-import — with an
332
+ // explicit `Any*Column` return type to break the circular type inference.
333
+ const anyColType = ctx.dialect === "sqlite" ? "AnySQLiteColumn" : "AnyPgColumn";
334
+ const anyColSym = imp(`${anyColType}@${spec.importModule}`);
335
+ fkRefSegment = code`.references((): ${anyColSym} => ${fkInfo.targetVarName}.${fkInfo.targetPkField})`;
336
+ } else {
337
+ const targetSpec = crossEntitySpecifier(
338
+ ctx.outputLayout,
339
+ entityPackage,
340
+ ctx.packageOf.get(fkInfo.targetEntityName),
341
+ fkInfo.targetEntityName,
342
+ ctx.extStyle,
343
+ );
344
+ const targetVarSym = imp(`${fkInfo.targetVarName}@${targetSpec}`);
345
+ fkRefSegment = code`.references(() => ${targetVarSym}.${fkInfo.targetPkField})`;
346
+ }
301
347
  }
302
348
 
303
349
  // @autoSet fields: emit .$defaultFn(() => new Date().toISOString()) so Drizzle
304
350
  // inserts stamp the server-side timestamp automatically. This means callers don't
305
351
  // need to supply createdAt / updatedAt in INSERT calls — Drizzle fills them in.
306
- const autoSet = field.ownAttr(FIELD_ATTR_AUTO_SET);
352
+ const autoSet = field.attr(FIELD_ATTR_AUTO_SET);
307
353
  const autoSetSuffix = (autoSet === "onCreate" || autoSet === "onUpdate")
308
354
  ? `.$defaultFn(() => new Date().toISOString())`
309
355
  : "";
@@ -316,11 +362,17 @@ function renderColumn(
316
362
  let dollarTypeSegment: Code | string = "";
317
363
  if (spec.dollarTypeRef !== undefined) {
318
364
  const ref = spec.dollarTypeRef;
365
+ const suffix = ref.array ? "[]" : "";
319
366
  if (ref.kind === "scalar") {
320
- dollarTypeSegment = `.$type<${ref.tsType}[]>()`;
367
+ dollarTypeSegment = `.$type<${ref.tsType}${suffix}>()`;
321
368
  } else {
322
- const refSym = imp(`${ref.name}@${ref.module}`);
323
- dollarTypeSegment = code`.$type<${refSym}[]>()`;
369
+ // Resolve the VO module through the shared layout/package/extStyle-aware
370
+ // helper so the .$type<VO> import matches the field's TS type + Zod schema.
371
+ const moduleSpec = valueObjectModuleSpecifier(
372
+ ref.name, ctx.packageOf, entityPackage, ctx.outputLayout, ctx.extStyle,
373
+ );
374
+ const refSym = imp(`${ref.name}@${moduleSpec}`);
375
+ dollarTypeSegment = ref.array ? code`.$type<${refSym}[]>()` : code`.$type<${refSym}>()`;
324
376
  }
325
377
  }
326
378
 
@@ -67,7 +67,7 @@ function humanize(s: string): string {
67
67
  * "Subscriber" → "/subscribers"
68
68
  * "WorkoutEvent" → "/workout_events"
69
69
  */
70
- function resourcePath(entity: MetaData): string {
70
+ export function resourcePath(entity: MetaData): string {
71
71
  const overrideAttr = entity.ownAttr("routePath");
72
72
  if (typeof overrideAttr === "string" && overrideAttr.length > 0) {
73
73
  return overrideAttr.startsWith("/") ? overrideAttr : `/${overrideAttr}`;
@@ -160,12 +160,12 @@ function renderFieldRules(field: MetaField): string | undefined {
160
160
  }
161
161
 
162
162
  // Field-level @required attr (if not already covered by validator).
163
- if (!hasRequired && field.ownAttr(FIELD_ATTR_REQUIRED) === true) {
163
+ if (!hasRequired && field.attr(FIELD_ATTR_REQUIRED) === true) {
164
164
  ruleParts.push(`required: ${JSON.stringify(`${humanize(field.name)} is required`)}`);
165
165
  }
166
166
 
167
167
  // Field-level @maxLength attr (if not already covered).
168
- const maxLenAttr = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
168
+ const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
169
169
  if (!hasMaxLength && typeof maxLenAttr === "number") {
170
170
  ruleParts.push(
171
171
  `maxLength: { value: ${maxLenAttr}, message: ${JSON.stringify(`Must be ${maxLenAttr} characters or fewer`)} }`,
@@ -15,6 +15,7 @@ import { renderZodValidators } from "./zod-validators.js";
15
15
  import { renderEntityConstants } from "./entity-constants.js";
16
16
  import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
17
17
  import { renderFilterType } from "./filter-type.js";
18
+ import { renderTphDiscriminatorUnion, isTphDiscriminatorBase } from "./tph-discriminator.js";
18
19
  import { GENERATED_HEADER } from "../constants.js";
19
20
  import { isProjection } from "../projection/projection-detector.js";
20
21
  import { renderProjectionDecl } from "./projection-decl.js";
@@ -53,7 +54,7 @@ export function renderEntityFile(
53
54
  // it. The entity-file generator suppresses this entirely when
54
55
  // emitAbstractShapes is off; here we only guarantee "shape, never table".
55
56
  if (isAbstract(entity)) {
56
- return renderValueObjectFile(entity);
57
+ return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
57
58
  }
58
59
 
59
60
  // --- Projection path (read-only: view-backed entity with no table source) ---
@@ -72,19 +73,29 @@ export function renderEntityFile(
72
73
  // the shape (LLM tool_use input_schema, REST body parsing) use the Zod
73
74
  // schema; consumers that need the type use the interface.
74
75
  if (!hasWritableRdbSource(entity)) {
75
- return renderValueObjectFile(entity);
76
+ return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
76
77
  }
77
78
 
78
79
  // --- Vanilla / write-through entity path ---
79
- const enumAliases = renderEnumTypeAliases(entity);
80
+ const enumAliases = renderEnumTypeAliases(entity, ctx);
81
+ // FR-017 Tier 1: when this entity carries @discriminator AND has concrete
82
+ // subtypes, append the discriminated-union type alias, type guards, and
83
+ // the parse<Base>(row) dispatcher. Returns null otherwise (no subtypes, or
84
+ // not a discriminator-bearing entity); the section is suppressed cleanly.
85
+ const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
86
+ // FR-017: when a discriminator base also has a union block, the union owns the
87
+ // bare `<Base>` type — so the inferred Drizzle row type is emitted as
88
+ // `<Base>Row` to avoid a duplicate `export type <Base>`.
89
+ const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
80
90
  const sections: Code[] = [
81
91
  renderDrizzleSchema(entity, ctx),
82
- renderInferredTypes(entity),
92
+ renderInferredTypes(entity, tphBase, ctx),
83
93
  ...(enumAliases !== null ? [enumAliases] : []),
84
- renderZodValidators(entity),
94
+ renderZodValidators(entity, ctx),
85
95
  renderEntityConstants(entity, ctx.apiPrefix),
86
96
  ...(allowlists ? [renderFilterAllowlist(entity), renderSortAllowlist(entity)] : []),
87
97
  renderFilterType(entity),
98
+ ...(tphBlock !== null ? [tphBlock] : []),
88
99
  ];
89
100
 
90
101
  // Render ts-poet body first (ts-poet hoists imp()-tracked imports to the top),
@@ -0,0 +1,50 @@
1
+ // FR-019 — shared enums module.
2
+ //
3
+ // Emits, ONCE per run, the materialized (non-@provided) shared enum types that
4
+ // at least one concrete entity field references via `extends` of a root-level
5
+ // abstract `field.enum`. Each enum yields:
6
+ // • `export type <E> = "A" | "B";` — the cross-port type identity
7
+ // • `export const <E>Enum = z.enum(["A","B"]);` — the shared Zod validator
8
+ //
9
+ // Consuming entity files import these instead of redeclaring the union inline.
10
+ // @provided enums are NEVER materialized here (they live in hand-written code,
11
+ // imported from the configured providedEnumModule).
12
+
13
+ import { code, imp, joinCode, type Code } from "ts-poet";
14
+ import type { MetaRoot } from "@metaobjectsdev/metadata";
15
+ import { GENERATED_HEADER } from "../constants.js";
16
+ import { materializedSharedEnums, type SharedEnum } from "../enum-shared.js";
17
+ import { enumUnionString } from "./inferred-types.js";
18
+
19
+ /** Basename (no extension) of the shared-enums module emitted at the entity-module target root. */
20
+ export const SHARED_ENUMS_BASENAME = "enums";
21
+
22
+ /** The exported Zod-constant name for a shared enum (`<E>Enum`). */
23
+ export function sharedEnumZodConstName(enumName: string): string {
24
+ return `${enumName}Enum`;
25
+ }
26
+
27
+ /** One enum's two declarations (type alias + shared z.enum const). */
28
+ function renderOneSharedEnum(e: SharedEnum): Code {
29
+ const z = imp("z@zod");
30
+ const members = e.values.map((v) => JSON.stringify(v)).join(", ");
31
+ return code`
32
+ export type ${e.name} = ${enumUnionString(e.values)};
33
+ export const ${sharedEnumZodConstName(e.name)} = ${z}.enum([${members}]);
34
+ `;
35
+ }
36
+
37
+ /**
38
+ * The full shared-enums module body, or null when the model has no materialized
39
+ * shared enums (so the generator emits no file at all).
40
+ */
41
+ export function renderSharedEnumsFile(root: MetaRoot): string | null {
42
+ const enums = materializedSharedEnums(root);
43
+ if (enums.length === 0) return null;
44
+
45
+ const body = joinCode(enums.map(renderOneSharedEnum), { on: "\n" }).toString();
46
+ const header =
47
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
48
+ `// Shared enum types (FR-019): one declaration per reused package-level enum.\n`;
49
+ return header + body;
50
+ }
@@ -1,11 +1,9 @@
1
1
  // server/typescript/packages/codegen-ts/src/templates/extract-delegate-emitter.ts
2
2
  //
3
- // FR-010 Plan 2.1 (nested codegen gap) the runtime-DELEGATING extract emitter.
3
+ // FR-010 the runtime-DELEGATING extract emitter (the single metadata-driven extract path).
4
4
  //
5
- // The self-contained extract<Name>(text) path (extract-schema-emitter + the baked
6
- // ExtractSchema) covers scalars / enums / scalar-arrays but leaves nested-object and
7
- // array-of-object components NULL — the historical FR-010 codegen gap. This module emits
8
- // the additive delegating overload that CLOSES that gap by wrapping the runtime extract:
5
+ // This module emits the loader-delegating extract entry point that reads the live metadata
6
+ // directly and populates nested-object and array-of-object components in full:
9
7
  //
10
8
  // extract<Name>(root: MetaRoot, text, opts?) -> ExtractionResult<<Name>Extracted>
11
9
  //
@@ -29,16 +27,17 @@ import {
29
27
  FIELD_SUBTYPE_ENUM,
30
28
  FIELD_ATTR_OBJECT_REF,
31
29
  PACKAGE_SEPARATOR,
30
+ refMatchesObject,
32
31
  } from "@metaobjectsdev/metadata";
33
32
  import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
34
33
 
35
34
  function findObject(root: MetaData, name: string): MetaData | undefined {
36
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
35
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
37
36
  }
38
37
 
39
38
  /** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
40
39
  function refVo(field: MetaData, root: MetaData): MetaData | undefined {
41
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
40
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
42
41
  if (typeof ref !== "string") return undefined;
43
42
  const direct = findObject(root, ref);
44
43
  if (direct !== undefined) return direct;
@@ -33,6 +33,7 @@ import {
33
33
  TEMPLATE_ATTR_PAYLOAD_REF,
34
34
  TEMPLATE_ATTR_FORMAT,
35
35
  PACKAGE_SEPARATOR,
36
+ refMatchesObject,
36
37
  } from "@metaobjectsdev/metadata";
37
38
  import { fields, isArray } from "./fr010-field-mapping.js";
38
39
  import { mirrorName } from "./extract-delegate-emitter.js";
@@ -40,7 +41,7 @@ import { enumUnionAliasName } from "./inferred-types.js";
40
41
  import { enumValues } from "../enum-meta.js";
41
42
 
42
43
  function findObject(root: MetaData, name: string): MetaData | undefined {
43
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
44
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
44
45
  }
45
46
 
46
47
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
@@ -49,7 +50,7 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
49
50
 
50
51
  /** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
51
52
  function refVo(field: MetaData, root: MetaData): MetaData | undefined {
52
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
53
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
53
54
  if (typeof ref !== "string") return undefined;
54
55
  const direct = findObject(root, ref);
55
56
  if (direct !== undefined) return direct;
@@ -64,9 +65,9 @@ function isObjectField(field: MetaData): boolean {
64
65
 
65
66
  /**
66
67
  * The union-alias type name for a `field.enum` with effective `@values`, or undefined when the
67
- * field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the payload
68
- * emitter (`payload-codegen.ts`) types the field as — so the cast target resolves to the exact
69
- * alias exported from `payloads.ts`. `ownerName` is the owning value-object's interface name.
68
+ * field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the entity
69
+ * inferred-types emitter types the field as — so the cast target resolves to the exact alias
70
+ * exported from the owning VO's entity module. `ownerName` is the owning value-object's interface name.
70
71
  */
71
72
  function enumAlias(field: MetaData, ownerName: string): string | undefined {
72
73
  if (field.subType !== FIELD_SUBTYPE_ENUM) return undefined;
@@ -76,14 +77,15 @@ function enumAlias(field: MetaData, ownerName: string): string | undefined {
76
77
  }
77
78
 
78
79
  /**
79
- * True iff the field is required IN THE STRICT PAYLOAD TYPE. This MUST match
80
- * payload-codegen.ts's `isFieldRequired` predicate EXACTLY (boolean `true` only) the payload
81
- * interface decides `T` vs `T | null` by that predicate, and the mapper's optionality assumption
82
- * (`m.f!` vs `m.f ?? null`) has to agree with the type it is constructing. A `@required:"true"`
83
- * string field is therefore `T | null` in the payload AND optional here (no skew).
80
+ * True iff the field is required IN THE STRICT PAYLOAD TYPE. The strict payload IS the VO's own
81
+ * generated entity-module interface (`renderValueObjectInterface`), which types a required field
82
+ * `f: T` and an optional one `f?: T` (i.e. `T | undefined` NOT `T | null`). So the mapper's
83
+ * optionality assumption (`m.f!` vs `m.f ?? undefined`) has to agree with THAT interface, and an
84
+ * absent optional maps to `undefined`, never `null`. This predicate matches the interface's
85
+ * required test (boolean `true` only) so the two never skew.
84
86
  */
85
87
  function isFieldRequired(field: MetaData): boolean {
86
- return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
88
+ return field.attr(FIELD_ATTR_REQUIRED) === true;
87
89
  }
88
90
 
89
91
  /** The mirror→strict mapper name for a value-object (`toStrict<Name>`). */
@@ -93,8 +95,10 @@ function mapperName(vo: MetaData): string {
93
95
 
94
96
  /**
95
97
  * The mapper-body initializer expression for one field, reading mirror member `m.<name>` and
96
- * mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? null`).
97
- * Nested single/array objects recurse into their toStrict<Type> mapper, guarding null when optional.
98
+ * mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? undefined`).
99
+ * The strict payload is the VO's generated entity-module interface, whose optional fields are
100
+ * `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`.
101
+ * Nested single/array objects recurse into their toStrict<Type> mapper, guarding when optional.
98
102
  */
99
103
  function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
100
104
  const name = field.name;
@@ -104,25 +108,25 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
104
108
  const target = refVo(field, root);
105
109
  if (target === undefined) {
106
110
  // Unresolved @objectRef — the payload type would be `unknown`; pass through as-is.
107
- return required ? `m.${name}!` : `m.${name} ?? null`;
111
+ return required ? `m.${name}!` : `m.${name} ?? undefined`;
108
112
  }
109
113
  const fn = mapperName(target);
110
114
  if (isArray(field)) {
111
115
  // Required array-of-objects: each element mapped; element nulls dropped at the type level
112
116
  // via the non-null assertion (extract never yields null elements for a present array).
113
117
  if (required) return `m.${name}!.map((e) => ${fn}(e!))`;
114
- return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : null`;
118
+ return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : undefined`;
115
119
  }
116
120
  // Single nested object.
117
121
  if (required) return `${fn}(m.${name}!)`;
118
- return `m.${name} ? ${fn}(m.${name}) : null`;
122
+ return `m.${name} ? ${fn}(m.${name}) : undefined`;
119
123
  }
120
124
 
121
125
  // Scalar ARRAY (e.g. `field.string` with isArray): the mirror types it `(T | null)[] | null`
122
- // but the strict payload types it `T[]` (required) / `T[] | null` (optional). A bare `m.f!`
126
+ // but the strict payload types it `T[]` (required) / `T[]?` (optional). A bare `m.f!`
123
127
  // would leave the element type `T | null`, a `tsc --strict` TS2322 error. Filter out null
124
128
  // elements so the element type narrows to non-null (consistent with the lost-element DROP policy
125
- // already used for required arrays-of-objects above).
129
+ // already used for required arrays-of-objects above). An absent optional array maps to `undefined`.
126
130
  //
127
131
  // ENUM arrays: the mirror element is a plain `string`, but the strict payload types it as the
128
132
  // closed `<Alias>[]` union. The null-filter alone narrows to `string[]`, not `<Alias>[]` — a
@@ -136,23 +140,23 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
136
140
  return alias !== undefined ? `(${filtered}) as ${alias}[]` : filtered;
137
141
  }
138
142
  const filtered = `m.${name}.filter((x): x is NonNullable<typeof x> => x != null)`;
139
- const guarded = `m.${name} == null ? null : ${filtered}`;
143
+ const guarded = `m.${name} == null ? undefined : ${filtered}`;
140
144
  return alias !== undefined
141
- ? `m.${name} == null ? null : (${filtered}) as ${alias}[]`
145
+ ? `m.${name} == null ? undefined : (${filtered}) as ${alias}[]`
142
146
  : guarded;
143
147
  }
144
148
 
145
149
  // Scalar / enum (single): the strict payload's optionality decides the shape.
146
- // Required → non-null assertion; optional → `?? null` (matches the payload's `f?: T | null`).
150
+ // Required → non-null assertion; optional → `?? undefined` (matches the entity-module `f?: T`).
147
151
  //
148
152
  // ENUM scalar: the mirror member is a plain `string`, but the strict payload types it as the
149
153
  // closed `<Alias>` union — assigning `string` into `<Alias>` is a `tsc --strict` TS2322 error.
150
154
  // So the value is CAST to `<Alias>`. Sound for the same reason as enum arrays above: the engine
151
155
  // already validated membership (or extract throws on a lost required field).
152
156
  if (alias !== undefined) {
153
- return required ? `m.${name}! as ${alias}` : `(m.${name} ?? null) as ${alias} | null`;
157
+ return required ? `m.${name}! as ${alias}` : `(m.${name} ?? undefined) as ${alias} | undefined`;
154
158
  }
155
- return required ? `m.${name}!` : `m.${name} ?? null`;
159
+ return required ? `m.${name}!` : `m.${name} ?? undefined`;
156
160
  }
157
161
 
158
162
  /**
@@ -202,24 +206,44 @@ function emitMapper(
202
206
  }
203
207
 
204
208
  /**
205
- * Collect the strict payload-interface names reachable from `vo` (for the type-only import),
206
- * PLUS every enum union-alias reachable from those VOs. Both are exported from `payloads.ts`
207
- * (the alias is hoisted above the interface there), so the extractor's `as <Alias>` casts need
208
- * the alias names imported alongside the interface names. Deduped, in discovery order.
209
+ * One payload-type import group: the strict types (VO interface + its own enum
210
+ * union-aliases) imported from a single VO entity module.
209
211
  */
210
- function reachablePayloadTypes(vo: MetaData, root: MetaData): string[] {
211
- const order: string[] = [];
212
- const seen = new Set<string>();
212
+ interface PayloadImportGroup {
213
+ /** The VO whose entity module exports these types (`./<module>.js`). */
214
+ module: string;
215
+ /** The strict type names exported by that module, in discovery order. */
216
+ types: string[];
217
+ }
218
+
219
+ /**
220
+ * Group the strict payload types reachable from `vo` BY THE ENTITY MODULE THAT EXPORTS THEM.
221
+ *
222
+ * Each value-object gets its own generated entity module (`entityFile()` emits `<VO>.ts` exporting
223
+ * `export interface <VO>`), and `renderEnumTypeAliases` hoists every `field.enum` union-alias INTO
224
+ * the OWNING VO's module (`export type <Owner><Field> = ...` co-located with the interface). So a
225
+ * VO's interface AND the aliases for its own enum fields are imported from `./<VO>.js` — NOT from a
226
+ * single `payloads.ts` (which no generator emits). Deduped, in discovery order, one group per VO.
227
+ */
228
+ function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGroup[] {
229
+ const groups: PayloadImportGroup[] = [];
230
+ const seenVo = new Set<string>();
231
+ const seenAlias = new Set<string>();
213
232
  const visit = (cur: MetaData) => {
214
- if (seen.has(cur.name)) return;
215
- seen.add(cur.name);
216
- order.push(cur.name);
233
+ if (seenVo.has(cur.name)) return;
234
+ seenVo.add(cur.name);
235
+ // The VO interface + its OWN enum aliases share the VO's entity module.
236
+ const types: string[] = [cur.name];
217
237
  for (const f of fields(cur)) {
218
238
  const alias = enumAlias(f, cur.name);
219
- if (alias !== undefined && !seen.has(alias)) {
220
- seen.add(alias);
221
- order.push(alias);
239
+ if (alias !== undefined && !seenAlias.has(alias)) {
240
+ seenAlias.add(alias);
241
+ types.push(alias);
222
242
  }
243
+ }
244
+ groups.push({ module: cur.name, types });
245
+ // Recurse into nested object refs (their interfaces live in their own modules).
246
+ for (const f of fields(cur)) {
223
247
  if (isObjectField(f)) {
224
248
  const target = refVo(f, root);
225
249
  if (target !== undefined) visit(target);
@@ -227,7 +251,7 @@ function reachablePayloadTypes(vo: MetaData, root: MetaData): string[] {
227
251
  }
228
252
  };
229
253
  visit(vo);
230
- return order;
254
+ return groups;
231
255
  }
232
256
 
233
257
  /** Collect the mirror-interface names reachable from `vo` (root mirror + nested VO mirrors). */
@@ -286,10 +310,16 @@ export function renderExtractor(root: MetaData, templateName: string): string {
286
310
  const extractName = `extract${templateName}`;
287
311
  const rootMapper = mapperName(vo);
288
312
 
289
- const payloadTypes = reachablePayloadTypes(vo, root);
313
+ const payloadGroups = reachablePayloadGroups(vo, root);
290
314
  const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror);
291
315
  const mappers = emitMappers(vo, root, rootMirror);
292
316
 
317
+ // One type-only import per VO entity module (the VO interface + its own enum
318
+ // union-aliases co-located there). NOT a single non-existent `./payloads.js`.
319
+ const payloadImports = payloadGroups
320
+ .map((g) => `import type { ${g.types.join(", ")} } from "./${g.module}.js";`)
321
+ .join("\n");
322
+
293
323
  const lostMsg =
294
324
  `${extractName}: lost required field(s): `;
295
325
 
@@ -301,13 +331,13 @@ export function renderExtractor(root: MetaData, templateName: string): string {
301
331
  `// mapping the all-nullable mirror onto the strict payload. No registry / binding / factory.\n` +
302
332
  `\n` +
303
333
  `import {\n ${extractLenientWithName},\n type ${mirrorTypes.join(",\n type ")},\n} from "./${templateName}.output.js";\n` +
304
- `import type { ${payloadTypes.join(", ")} } from "./payloads.js";\n` +
334
+ `${payloadImports}\n` +
305
335
  `import type { MetaRoot } from "@metaobjectsdev/metadata";\n` +
306
336
  `import type { ExtractionResult } from "@metaobjectsdev/render";\n` +
307
337
  `\n` +
308
338
  `/**\n` +
309
339
  ` * Extract a fully-typed \`${strictType}\` from dirty \`text\` using the loaded \`root\` (which must\n` +
310
- ` * declare the "${payloadRef}" payload value-object). Runs the tolerant extract, then maps the\n` +
340
+ ` * declare the "${strictType}" payload value-object). Runs the tolerant extract, then maps the\n` +
311
341
  ` * extracted mirror onto the strict payload.\n` +
312
342
  ` *\n` +
313
343
  ` * @throws Error iff a \`@required\` field was lost (the strict opt-in gate).\n` +