@metaobjectsdev/codegen-ts 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +24 -8
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/constants.d.ts +8 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +16 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/docs-paths.d.ts +58 -0
  10. package/dist/docs-paths.d.ts.map +1 -0
  11. package/dist/docs-paths.js +89 -0
  12. package/dist/docs-paths.js.map +1 -0
  13. package/dist/enum-import.d.ts +14 -0
  14. package/dist/enum-import.d.ts.map +1 -0
  15. package/dist/enum-import.js +35 -0
  16. package/dist/enum-import.js.map +1 -0
  17. package/dist/enum-shared.d.ts +32 -0
  18. package/dist/enum-shared.d.ts.map +1 -0
  19. package/dist/enum-shared.js +83 -0
  20. package/dist/enum-shared.js.map +1 -0
  21. package/dist/generator-registry.d.ts +22 -0
  22. package/dist/generator-registry.d.ts.map +1 -0
  23. package/dist/generator-registry.js +161 -0
  24. package/dist/generator-registry.js.map +1 -0
  25. package/dist/generator.d.ts +6 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/api-doc-render.d.ts +17 -0
  29. package/dist/generators/api-doc-render.d.ts.map +1 -0
  30. package/dist/generators/api-doc-render.js +431 -0
  31. package/dist/generators/api-doc-render.js.map +1 -0
  32. package/dist/generators/api-docs-file.d.ts +21 -0
  33. package/dist/generators/api-docs-file.d.ts.map +1 -0
  34. package/dist/generators/api-docs-file.js +112 -0
  35. package/dist/generators/api-docs-file.js.map +1 -0
  36. package/dist/generators/api-field-shape.d.ts +39 -0
  37. package/dist/generators/api-field-shape.d.ts.map +1 -0
  38. package/dist/generators/api-field-shape.js +92 -0
  39. package/dist/generators/api-field-shape.js.map +1 -0
  40. package/dist/generators/api-label.d.ts +3 -0
  41. package/dist/generators/api-label.d.ts.map +1 -0
  42. package/dist/generators/api-label.js +8 -0
  43. package/dist/generators/api-label.js.map +1 -0
  44. package/dist/generators/api-model.d.ts +122 -0
  45. package/dist/generators/api-model.d.ts.map +1 -0
  46. package/dist/generators/api-model.js +809 -0
  47. package/dist/generators/api-model.js.map +1 -0
  48. package/dist/generators/docs-data-builder.d.ts +26 -4
  49. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  50. package/dist/generators/docs-data-builder.js +436 -164
  51. package/dist/generators/docs-data-builder.js.map +1 -1
  52. package/dist/generators/docs-data.d.ts +136 -27
  53. package/dist/generators/docs-data.d.ts.map +1 -1
  54. package/dist/generators/docs-data.js +1 -1
  55. package/dist/generators/docs-data.js.map +1 -1
  56. package/dist/generators/docs-file.d.ts +19 -0
  57. package/dist/generators/docs-file.d.ts.map +1 -1
  58. package/dist/generators/docs-file.js +154 -27
  59. package/dist/generators/docs-file.js.map +1 -1
  60. package/dist/generators/entity-file.d.ts.map +1 -1
  61. package/dist/generators/entity-file.js +29 -14
  62. package/dist/generators/entity-file.js.map +1 -1
  63. package/dist/generators/extractor-file.d.ts.map +1 -1
  64. package/dist/generators/extractor-file.js +2 -1
  65. package/dist/generators/extractor-file.js.map +1 -1
  66. package/dist/generators/field-anchor.d.ts +7 -0
  67. package/dist/generators/field-anchor.d.ts.map +1 -0
  68. package/dist/generators/field-anchor.js +23 -0
  69. package/dist/generators/field-anchor.js.map +1 -0
  70. package/dist/generators/index.d.ts +8 -1
  71. package/dist/generators/index.d.ts.map +1 -1
  72. package/dist/generators/index.js +6 -0
  73. package/dist/generators/index.js.map +1 -1
  74. package/dist/generators/mermaid-er.d.ts +14 -0
  75. package/dist/generators/mermaid-er.d.ts.map +1 -1
  76. package/dist/generators/mermaid-er.js +14 -0
  77. package/dist/generators/mermaid-er.js.map +1 -1
  78. package/dist/generators/output-parser-file.d.ts.map +1 -1
  79. package/dist/generators/output-parser-file.js +3 -4
  80. package/dist/generators/output-parser-file.js.map +1 -1
  81. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  82. package/dist/generators/output-prompt-file.js +2 -2
  83. package/dist/generators/output-prompt-file.js.map +1 -1
  84. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  85. package/dist/generators/prompt-render-file.js +3 -4
  86. package/dist/generators/prompt-render-file.js.map +1 -1
  87. package/dist/generators/queries-file.d.ts.map +1 -1
  88. package/dist/generators/queries-file.js +8 -3
  89. package/dist/generators/queries-file.js.map +1 -1
  90. package/dist/generators/render-helper-file.d.ts.map +1 -1
  91. package/dist/generators/render-helper-file.js +2 -2
  92. package/dist/generators/render-helper-file.js.map +1 -1
  93. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  94. package/dist/generators/routes-file-hono.js +5 -1
  95. package/dist/generators/routes-file-hono.js.map +1 -1
  96. package/dist/generators/routes-file.d.ts +3 -0
  97. package/dist/generators/routes-file.d.ts.map +1 -1
  98. package/dist/generators/routes-file.js +6 -1
  99. package/dist/generators/routes-file.js.map +1 -1
  100. package/dist/generators/template-doc-builder.d.ts +19 -0
  101. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  102. package/dist/generators/template-doc-builder.js +220 -0
  103. package/dist/generators/template-doc-builder.js.map +1 -0
  104. package/dist/generators/template-doc-data.d.ts +62 -0
  105. package/dist/generators/template-doc-data.d.ts.map +1 -0
  106. package/dist/generators/template-doc-data.js +16 -0
  107. package/dist/generators/template-doc-data.js.map +1 -0
  108. package/dist/generators/template-payload-tree.d.ts +15 -0
  109. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  110. package/dist/generators/template-payload-tree.js +61 -0
  111. package/dist/generators/template-payload-tree.js.map +1 -0
  112. package/dist/generators/template-source-annotate.d.ts +74 -0
  113. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  114. package/dist/generators/template-source-annotate.js +184 -0
  115. package/dist/generators/template-source-annotate.js.map +1 -0
  116. package/dist/generators/template-source-render.d.ts +24 -0
  117. package/dist/generators/template-source-render.d.ts.map +1 -0
  118. package/dist/generators/template-source-render.js +175 -0
  119. package/dist/generators/template-source-render.js.map +1 -0
  120. package/dist/generators/trace-helper-file.d.ts +9 -0
  121. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  122. package/dist/generators/trace-helper-file.js +196 -0
  123. package/dist/generators/trace-helper-file.js.map +1 -0
  124. package/dist/index.d.ts +29 -4
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +28 -2
  127. package/dist/index.js.map +1 -1
  128. package/dist/metaobjects-config.d.ts +75 -2
  129. package/dist/metaobjects-config.d.ts.map +1 -1
  130. package/dist/metaobjects-config.js +43 -0
  131. package/dist/metaobjects-config.js.map +1 -1
  132. package/dist/naming.d.ts +19 -0
  133. package/dist/naming.d.ts.map +1 -1
  134. package/dist/naming.js +41 -0
  135. package/dist/naming.js.map +1 -1
  136. package/dist/payload-codegen.d.ts.map +1 -1
  137. package/dist/payload-codegen.js +12 -4
  138. package/dist/payload-codegen.js.map +1 -1
  139. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  140. package/dist/projection/extract-view-spec.js +51 -25
  141. package/dist/projection/extract-view-spec.js.map +1 -1
  142. package/dist/relation-resolver.d.ts +16 -0
  143. package/dist/relation-resolver.d.ts.map +1 -1
  144. package/dist/relation-resolver.js +82 -1
  145. package/dist/relation-resolver.js.map +1 -1
  146. package/dist/render-context.d.ts +4 -0
  147. package/dist/render-context.d.ts.map +1 -1
  148. package/dist/render-context.js.map +1 -1
  149. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  150. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  151. package/dist/render-engine/embedded-templates.generated.js +15 -0
  152. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  153. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  154. package/dist/render-engine/framework-provider.js +26 -13
  155. package/dist/render-engine/framework-provider.js.map +1 -1
  156. package/dist/runner.d.ts.map +1 -1
  157. package/dist/runner.js +17 -0
  158. package/dist/runner.js.map +1 -1
  159. package/dist/templates/docs-file.d.ts +2 -6
  160. package/dist/templates/docs-file.d.ts.map +1 -1
  161. package/dist/templates/docs-file.js +2 -5
  162. package/dist/templates/docs-file.js.map +1 -1
  163. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  164. package/dist/templates/drizzle-schema.js +30 -2
  165. package/dist/templates/drizzle-schema.js.map +1 -1
  166. package/dist/templates/entity-constants.d.ts +7 -0
  167. package/dist/templates/entity-constants.d.ts.map +1 -1
  168. package/dist/templates/entity-constants.js +1 -1
  169. package/dist/templates/entity-constants.js.map +1 -1
  170. package/dist/templates/entity-file.d.ts.map +1 -1
  171. package/dist/templates/entity-file.js +16 -5
  172. package/dist/templates/entity-file.js.map +1 -1
  173. package/dist/templates/enums-file.d.ts +11 -0
  174. package/dist/templates/enums-file.d.ts.map +1 -0
  175. package/dist/templates/enums-file.js +44 -0
  176. package/dist/templates/enums-file.js.map +1 -0
  177. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  178. package/dist/templates/extract-delegate-emitter.js +5 -7
  179. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  180. package/dist/templates/extract-schema-emitter.d.ts.map +1 -1
  181. package/dist/templates/extract-schema-emitter.js +5 -1
  182. package/dist/templates/extract-schema-emitter.js.map +1 -1
  183. package/dist/templates/extractor.d.ts.map +1 -1
  184. package/dist/templates/extractor.js +56 -39
  185. package/dist/templates/extractor.js.map +1 -1
  186. package/dist/templates/field-meta.d.ts.map +1 -1
  187. package/dist/templates/field-meta.js +1 -5
  188. package/dist/templates/field-meta.js.map +1 -1
  189. package/dist/templates/filter-allowlist.d.ts +7 -2
  190. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  191. package/dist/templates/filter-allowlist.js +17 -9
  192. package/dist/templates/filter-allowlist.js.map +1 -1
  193. package/dist/templates/filter-type.d.ts +7 -1
  194. package/dist/templates/filter-type.d.ts.map +1 -1
  195. package/dist/templates/filter-type.js +9 -5
  196. package/dist/templates/filter-type.js.map +1 -1
  197. package/dist/templates/find-templates.d.ts +4 -0
  198. package/dist/templates/find-templates.d.ts.map +1 -0
  199. package/dist/templates/find-templates.js +15 -0
  200. package/dist/templates/find-templates.js.map +1 -0
  201. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  202. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  203. package/dist/templates/fr010-field-mapping.js +10 -6
  204. package/dist/templates/fr010-field-mapping.js.map +1 -1
  205. package/dist/templates/inferred-types.d.ts +44 -7
  206. package/dist/templates/inferred-types.d.ts.map +1 -1
  207. package/dist/templates/inferred-types.js +107 -16
  208. package/dist/templates/inferred-types.js.map +1 -1
  209. package/dist/templates/mermaid-er.d.ts +35 -2
  210. package/dist/templates/mermaid-er.d.ts.map +1 -1
  211. package/dist/templates/mermaid-er.js +174 -7
  212. package/dist/templates/mermaid-er.js.map +1 -1
  213. package/dist/templates/output-parser.d.ts.map +1 -1
  214. package/dist/templates/output-parser.js +30 -79
  215. package/dist/templates/output-parser.js.map +1 -1
  216. package/dist/templates/output-prompt.d.ts.map +1 -1
  217. package/dist/templates/output-prompt.js +2 -2
  218. package/dist/templates/output-prompt.js.map +1 -1
  219. package/dist/templates/queries-file.d.ts.map +1 -1
  220. package/dist/templates/queries-file.js +112 -4
  221. package/dist/templates/queries-file.js.map +1 -1
  222. package/dist/templates/queries.d.ts +5 -0
  223. package/dist/templates/queries.d.ts.map +1 -1
  224. package/dist/templates/queries.js +7 -7
  225. package/dist/templates/queries.js.map +1 -1
  226. package/dist/templates/recover-schema-emitter.d.ts +8 -0
  227. package/dist/templates/recover-schema-emitter.d.ts.map +1 -0
  228. package/dist/templates/recover-schema-emitter.js +64 -0
  229. package/dist/templates/recover-schema-emitter.js.map +1 -0
  230. package/dist/templates/relations-block.js +10 -0
  231. package/dist/templates/relations-block.js.map +1 -1
  232. package/dist/templates/render-helper.d.ts.map +1 -1
  233. package/dist/templates/render-helper.js +4 -4
  234. package/dist/templates/render-helper.js.map +1 -1
  235. package/dist/templates/routes-file.d.ts.map +1 -1
  236. package/dist/templates/routes-file.js +183 -6
  237. package/dist/templates/routes-file.js.map +1 -1
  238. package/dist/templates/tph-discriminator.d.ts +56 -0
  239. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  240. package/dist/templates/tph-discriminator.js +180 -0
  241. package/dist/templates/tph-discriminator.js.map +1 -0
  242. package/dist/templates/value-object-file.d.ts +2 -1
  243. package/dist/templates/value-object-file.d.ts.map +1 -1
  244. package/dist/templates/value-object-file.js +32 -4
  245. package/dist/templates/value-object-file.js.map +1 -1
  246. package/dist/templates/zod-validators.d.ts +64 -1
  247. package/dist/templates/zod-validators.d.ts.map +1 -1
  248. package/dist/templates/zod-validators.js +181 -8
  249. package/dist/templates/zod-validators.js.map +1 -1
  250. package/package.json +103 -34
  251. package/src/column-mapper.ts +25 -8
  252. package/src/constants.ts +18 -0
  253. package/src/docs-paths.ts +128 -0
  254. package/src/enum-import.ts +43 -0
  255. package/src/enum-shared.ts +95 -0
  256. package/src/generator-registry.ts +204 -0
  257. package/src/generator.ts +6 -0
  258. package/src/generators/api-doc-render.ts +572 -0
  259. package/src/generators/api-docs-file.ts +146 -0
  260. package/src/generators/api-field-shape.ts +114 -0
  261. package/src/generators/api-label.ts +7 -0
  262. package/src/generators/api-model.ts +1067 -0
  263. package/src/generators/docs-data-builder.ts +479 -185
  264. package/src/generators/docs-data.ts +139 -28
  265. package/src/generators/docs-file.ts +205 -39
  266. package/src/generators/entity-file.ts +31 -15
  267. package/src/generators/extractor-file.ts +2 -1
  268. package/src/generators/field-anchor.ts +24 -0
  269. package/src/generators/index.ts +8 -1
  270. package/src/generators/mermaid-er.ts +14 -0
  271. package/src/generators/output-parser-file.ts +3 -4
  272. package/src/generators/output-prompt-file.ts +2 -1
  273. package/src/generators/prompt-render-file.ts +3 -4
  274. package/src/generators/queries-file.ts +9 -3
  275. package/src/generators/render-helper-file.ts +2 -1
  276. package/src/generators/routes-file-hono.ts +5 -1
  277. package/src/generators/routes-file.ts +7 -1
  278. package/src/generators/template-doc-builder.ts +306 -0
  279. package/src/generators/template-doc-data.ts +85 -0
  280. package/src/generators/template-payload-tree.ts +71 -0
  281. package/src/generators/template-source-annotate.ts +290 -0
  282. package/src/generators/template-source-render.ts +203 -0
  283. package/src/generators/trace-helper-file.ts +301 -0
  284. package/src/index.ts +55 -4
  285. package/src/metaobjects-config.ts +117 -2
  286. package/src/naming.ts +48 -0
  287. package/src/payload-codegen.ts +14 -3
  288. package/src/projection/extract-view-spec.ts +49 -30
  289. package/src/relation-resolver.ts +103 -1
  290. package/src/render-context.ts +4 -0
  291. package/src/render-engine/embedded-templates.generated.ts +14 -0
  292. package/src/render-engine/framework-provider.ts +25 -11
  293. package/src/runner.ts +21 -0
  294. package/src/templates/docs-file.ts +2 -9
  295. package/src/templates/drizzle-schema.ts +31 -1
  296. package/src/templates/entity-constants.ts +1 -1
  297. package/src/templates/entity-file.ts +16 -5
  298. package/src/templates/enums-file.ts +50 -0
  299. package/src/templates/extract-delegate-emitter.ts +5 -6
  300. package/src/templates/extractor.ts +68 -38
  301. package/src/templates/field-meta.ts +0 -6
  302. package/src/templates/filter-allowlist.ts +17 -10
  303. package/src/templates/filter-type.ts +8 -6
  304. package/src/templates/find-templates.ts +15 -0
  305. package/src/templates/fr010-field-mapping.ts +10 -8
  306. package/src/templates/inferred-types.ts +108 -18
  307. package/src/templates/mermaid-er.ts +176 -8
  308. package/src/templates/output-parser.ts +30 -79
  309. package/src/templates/output-prompt.ts +2 -1
  310. package/src/templates/queries-file.ts +132 -3
  311. package/src/templates/queries.ts +15 -7
  312. package/src/templates/relations-block.ts +17 -0
  313. package/src/templates/render-helper.ts +4 -3
  314. package/src/templates/routes-file.ts +233 -6
  315. package/src/templates/tph-discriminator.ts +232 -0
  316. package/src/templates/value-object-file.ts +38 -4
  317. package/src/templates/zod-validators.ts +204 -7
  318. package/templates/api/agent-api.md.mustache +30 -0
  319. package/templates/api/entity-api.md.mustache +69 -0
  320. package/templates/api/index.md.mustache +21 -0
  321. package/templates/docs/entity-page.md.mustache +33 -21
  322. package/templates/docs/template-page.md.mustache +56 -0
  323. package/src/templates/extract-schema-emitter.ts +0 -111
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  TYPE_FIELD,
3
+ TYPE_IDENTITY,
3
4
  TYPE_ORIGIN,
4
5
  TYPE_RELATIONSHIP,
5
6
  MetaSource,
@@ -79,26 +80,55 @@ export function projectionViewName(
79
80
  return viewName(projection, { columnNamingStrategy });
80
81
  }
81
82
 
83
+ /**
84
+ * FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
85
+ * owner part of `<owner>.<child>...` resolved as an object. Mirrors the
86
+ * loader's `_refNamedOwner`: the ref names the anchor, never the physical
87
+ * declaring ancestor of an inherited child.
88
+ */
89
+ function refNamedOwner(node: MetaData, root: MetaRoot): MetaObject | undefined {
90
+ const ref = (node as { superRef?: string }).superRef;
91
+ if (ref === undefined) return undefined;
92
+ const lastSep = ref.lastIndexOf("::");
93
+ const tail = lastSep === -1 ? ref : ref.slice(lastSep + 2);
94
+ const dot = tail.indexOf(".");
95
+ if (dot <= 0) return undefined;
96
+ return root.findObject(tail.slice(0, dot)) ?? undefined;
97
+ }
98
+
82
99
  function baseEntityFor(
83
100
  projection: MetaObject,
84
101
  root: MetaRoot,
85
102
  ): MetaObject {
86
- // v1: base entity is the resolved super (set via `extends:` in metadata).
87
- const superModel = projection.superResolved;
88
- const superName = superModel?.name ?? projection.superRef;
89
- if (!superName) {
90
- throw new Error(
91
- `Projection ${projection.name}: missing extends projections must extend a writable entity in v1.`,
92
- );
103
+ // FR-024 base-anchor rules (mirror the loader's _deriveBaseEntity):
104
+ // 1) the extends-bound identity anchors the base entity;
105
+ // 2) else the single distinct entity targeted by extends-bound fields.
106
+ // The pre-FR-024 object-level `extends:` firehose is removed (B4b cutover).
107
+ //
108
+ // COUPLING NOTE: this intentionally derives the anchor ONLY from the ref's
109
+ // named owner (refNamedOwner), NOT the loader's `superResolved.parent`
110
+ // fallback for a non-dotted identity extends. That fallback is unreachable
111
+ // here because the loader gate (validate-identity-passthrough →
112
+ // ERR_PROJECTION_IDENTITY_NOT_EXTENDED) rejects any projection whose identity
113
+ // is not dotted-extends-bound before codegen runs. If that loader gate is
114
+ // ever loosened, this function must grow the same fallback.
115
+ for (const identity of projection
116
+ .ownChildren()
117
+ .filter((c) => c.type === TYPE_IDENTITY)) {
118
+ const named = refNamedOwner(identity, root);
119
+ if (named !== undefined) return named;
93
120
  }
94
- const base =
95
- superModel instanceof MetaObject ? superModel : root.findObject(superName);
96
- if (!base) {
97
- throw new Error(
98
- `Projection ${projection.name}: extends "${superName}" does not resolve to any entity.`,
99
- );
121
+ const targets = new Set<MetaObject>();
122
+ for (const f of projection.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
123
+ const named = refNamedOwner(f, root);
124
+ if (named !== undefined && named !== projection) targets.add(named);
100
125
  }
101
- return base;
126
+ if (targets.size === 1) return [...targets][0]!;
127
+ throw new Error(
128
+ `Projection ${projection.name}: cannot derive the base entity — declare an ` +
129
+ `extends-bound identity (identity.primary { name, extends: "<Entity>.<identity>" }) ` +
130
+ `to anchor the base (FR-024).`,
131
+ );
102
132
  }
103
133
 
104
134
  function sourceColumnNameFor(
@@ -270,22 +300,11 @@ function buildSelectSpec(
270
300
  ): SelectSpec {
271
301
  const columns: SelectColumn[] = [];
272
302
 
273
- // Inherited fields from extends parent emit as passthrough on baseAlias.
274
- // Skip fields that the projection has overridden with an explicit origin.
275
- // fields() is effective-by-default, so multi-level inheritance (base → BaseEntity) works.
276
- for (const baseField of base.fields()) {
277
- const overridden = projection.ownChildren().find(
278
- (c) => c.type === TYPE_FIELD && c.name === baseField.name,
279
- );
280
- if (overridden) continue;
281
- columns.push({
282
- kind: "passthrough",
283
- fieldName: baseField.name,
284
- dbColAlias: sourceColumnNameFor(baseField, ctx),
285
- sourceAlias: joinTree.baseAlias,
286
- sourceColumn: sourceColumnNameFor(baseField, ctx),
287
- });
288
- }
303
+ // FR-024 (ADR-0028): the projection's DECLARED field set IS the exposure
304
+ // the inclusive list, fail-closed by construction. The pre-FR-024 loop that
305
+ // emitted every base-entity field as an implicit passthrough (the firehose)
306
+ // is removed with the B4b cutover: base columns are declared explicitly as
307
+ // extends-bound fields (`{ field.int: { name: id, extends: "Program.id" } }`).
289
308
 
290
309
  // Fields explicitly declared on the projection.
291
310
  for (const field of projection.ownChildren()) {
@@ -5,11 +5,14 @@
5
5
  //
6
6
  // Reads identity.reference declarations to determine the physical reference side.
7
7
 
8
- import type { MetaRoot } from "@metaobjectsdev/metadata";
8
+ import type { MetaRoot, MetaObject, MetaRelationship } from "@metaobjectsdev/metadata";
9
9
  import {
10
10
  RELATIONSHIP_ATTR_CARDINALITY,
11
11
  RELATIONSHIP_ATTR_OBJECT_REF,
12
+ RELATIONSHIP_ATTR_THROUGH,
12
13
  CARDINALITY_ONE,
14
+ CARDINALITY_MANY,
15
+ deriveM2MFields,
13
16
  stripPackage,
14
17
  } from "@metaobjectsdev/metadata";
15
18
  import { variableNameFromEntity } from "./naming.js";
@@ -26,6 +29,22 @@ export interface RelationEntry {
26
29
  fkField?: string;
27
30
  /** For cardinality 'one': the target entity's PK field (e.g., "id") */
28
31
  targetPkField?: string;
32
+ /**
33
+ * FR-018 M:N navigation fields. Present only for a many-to-many navigation (a
34
+ * `@cardinality: "many"` relationship that declares `@through`). The Drizzle
35
+ * relations() block emits `many(<junction>)` for these; the routes file emits
36
+ * a `mountM2mRoute(...)` traversal. The junction FK fields are DERIVED from the
37
+ * junction entity's two `identity.reference` children (the SSOT), never
38
+ * restated on the relationship.
39
+ */
40
+ /** The junction/through entity name (e.g., "PostTag"). M:N entries only. */
41
+ junctionEntity?: string;
42
+ /** Junction FK field holding the source-side key (logical field name, e.g. "postId"). M:N only. */
43
+ sourceJoinField?: string;
44
+ /** Junction FK field holding the target-side key (logical field name, e.g. "tagId"). M:N only. */
45
+ targetJoinField?: string;
46
+ /** Undirected self-join: union both junction FK columns at read time. M:N only. */
47
+ symmetric?: boolean;
29
48
  }
30
49
 
31
50
  /** Map from entity name → list of relations for that entity's relations() block */
@@ -51,6 +70,16 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
51
70
 
52
71
  for (const child of obj.relationships()) {
53
72
  const cardinality = child.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
73
+
74
+ // FR-018 M:N: `@cardinality: "many"` + `@through` — derive the junction FK
75
+ // columns from the junction's identity.reference children and register a
76
+ // many(junction) navigation on the source.
77
+ if (cardinality === CARDINALITY_MANY && child.ownAttr(RELATIONSHIP_ATTR_THROUGH) !== undefined) {
78
+ const m2m = buildM2mEntry(obj, child as MetaRelationship, root);
79
+ if (m2m) ensure(obj.name).push(m2m);
80
+ continue;
81
+ }
82
+
54
83
  if (cardinality !== CARDINALITY_ONE) continue;
55
84
 
56
85
  const targetEntityRaw = child.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
@@ -83,5 +112,78 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
83
112
  }
84
113
  }
85
114
 
115
+ // FR-018: junction entities reached via @through need their two belongs-to
116
+ // one() sides so the through-table is navigable in the Drizzle relational
117
+ // query API (db.query.posts.findMany({ with: { tags: { with: { tag: true }}}})).
118
+ // A junction is any entity named by some M:N relationship's @through.
119
+ for (const junctionName of collectJunctionNames(root)) {
120
+ const junction = root.findObject(junctionName);
121
+ if (!junction) continue;
122
+ const entries = ensure(junctionName);
123
+ for (const ref of junction.referenceIdentities()) {
124
+ const targetRaw = ref.targetEntity;
125
+ const fkField = ref.fields[0];
126
+ if (!targetRaw || !fkField) continue;
127
+ const targetEntity = stripPackage(targetRaw);
128
+ // The relation member is named after the target entity (camel singular);
129
+ // multiple references to the same entity (self-join junction) are
130
+ // disambiguated by the FK field name.
131
+ const refName = ref.name && ref.name.length > 0
132
+ ? ref.name
133
+ : variableNameFromEntity(targetEntity);
134
+ entries.push({
135
+ name: refName,
136
+ cardinality: "one",
137
+ targetEntity,
138
+ fkField,
139
+ targetPkField: "id",
140
+ });
141
+ }
142
+ }
143
+
86
144
  return result;
87
145
  }
146
+
147
+ /** Names of all entities that are the `@through` junction of some M:N relationship. */
148
+ function collectJunctionNames(root: MetaRoot): Set<string> {
149
+ const names = new Set<string>();
150
+ for (const obj of root.objects()) {
151
+ for (const rel of obj.relationships()) {
152
+ if (rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
153
+ const through = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
154
+ if (through) names.add(stripPackage(through));
155
+ }
156
+ }
157
+ return names;
158
+ }
159
+
160
+ /**
161
+ * Build the source-side M:N navigation entry: derive the junction FK fields from
162
+ * the junction's two identity.reference children (the SSOT), handling hetero /
163
+ * directed-self-join / symmetric. Returns null (skips the entry) if derivation
164
+ * fails — the loader validation pass surfaces the actionable error separately.
165
+ */
166
+ function buildM2mEntry(
167
+ source: MetaObject,
168
+ rel: MetaRelationship,
169
+ root: MetaRoot,
170
+ ): RelationEntry | null {
171
+ const targetRaw = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
172
+ const throughRaw = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
173
+ if (!targetRaw || !throughRaw) return null;
174
+ let fields;
175
+ try {
176
+ fields = deriveM2MFields(rel, source, root);
177
+ } catch {
178
+ return null;
179
+ }
180
+ return {
181
+ name: rel.name,
182
+ cardinality: "many",
183
+ targetEntity: stripPackage(targetRaw),
184
+ junctionEntity: stripPackage(throughRaw),
185
+ sourceJoinField: fields.sourceField,
186
+ targetJoinField: fields.targetField,
187
+ symmetric: rel.symmetric,
188
+ };
189
+ }
@@ -52,6 +52,10 @@ export interface RenderContext {
52
52
  relationMap: RelationMap;
53
53
  /** Entity name → its metadata package (undefined if the entity has no package). Built once per run. */
54
54
  packageOf: Map<string, string | undefined>;
55
+ /** FR-019: module specifier to import externally-PROVIDED shared enums from
56
+ * (`@provided: true` declarations). Undefined when unset — referencing a
57
+ * provided enum without it is a codegen-time error. */
58
+ providedEnumModule?: string;
55
59
  }
56
60
 
57
61
  /** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). */
@@ -0,0 +1,14 @@
1
+ // @generated from templates/*/*.mustache — DO NOT EDIT.
2
+ // Regenerate: bun run scripts/generate-embedded-templates.ts (or scripts/sync-doc-templates.sh).
3
+ //
4
+ // Embedded framework doc templates so they resolve inside the
5
+ // `bun build --compile` standalone `meta` binary, where the on-disk
6
+ // `templates/` directory is unavailable. Keys are provider resolve refs
7
+ // (path under templates/ minus the .mustache suffix).
8
+ export const EMBEDDED_FRAMEWORK_TEMPLATES: Record<string, string> = {
9
+ "api/agent-api.md": "{{{generatedMarker}}}\n\n# {{title}}\n\nGenerated API reference for {{project}}; call these exactly as written. {{importNote}}\n{{#hasSetup}}\n\n## Setup\n{{#setup}}\n- `{{handle}}` — {{{note}}} `{{{snippetInline}}}`\n{{/setup}}\n{{/hasSetup}}\n{{#units}}\n\n## {{node}}\n{{#groups}}\n\n`{{importHeader}}`\n{{#symbols}}\n- `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}\n{{/symbols}}\n{{/groups}}\n{{#example}}\n\nExample:\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/units}}\n",
10
+ "api/entity-api.md": "{{{generatedMarker}}}\n\n# {{node}} API\n{{#modelPageHref}}\n\n**Model / metadata:** [{{node}}]({{modelPageHref}})\n{{/modelPageHref}}\n\n> Import paths are relative to your generated-output directory.\n{{#hasSetup}}\n\n## Setup\n\nObtain the runtime handles the calls below need:\n{{#setup}}\n\n- `{{handle}}` — {{{note}}}\n\n```ts\n{{{snippet}}}\n```\n{{/setup}}\n{{/hasSetup}}\n{{#unitExample}}\n\n## Example\n\n```ts\n{{{unitExample}}}\n```\n{{/unitExample}}\n{{#sections}}\n\n## {{heading}}\n{{#symbols}}\n\n### `{{signature}}`\n\n{{usage}}\n\n```ts\n{{importLine}}\n```\n{{#hasFields}}\n\n{{fieldsCaption}}:\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n{{#fieldRows}}\n| `{{field}}` | `{{{type}}}` | {{required}} | {{notes}} |\n{{/fieldRows}}\n{{/hasFields}}\n{{#mountNote}}\n\nMount: {{{mountNote}}}\n{{/mountNote}}\n{{#throws}}\n\nThrows: {{throws}}\n{{/throws}}\n{{#example}}\n\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/symbols}}\n{{/sections}}\n",
11
+ "api/index.md": "{{{generatedMarker}}}\n\n# {{title}}\n\n{{intro}}\n{{#hasEntities}}\n\n## Entities\n\n{{#entities}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/entities}}\n{{/hasEntities}}\n{{#hasTemplates}}\n\n## Templates\n\n{{#templates}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/templates}}\n{{/hasTemplates}}\n",
12
+ "docs/entity-page.md": "{{{generatedMarker}}}\n\n# {{entity.name}}\n{{#summaryLead}}\n\n{{{.}}}\n{{/summaryLead}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n{{#apiRefs.0}}\n\n**API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}\n{{/apiRefs.0}}\n\n{{{preambleHeader}}}\n{{#hasIdentities}}\n\n## Identity\n\n{{#identities}}\n- {{{bullet}}}\n{{/identities}}\n{{/hasIdentities}}\n{{#hasNeighborhoodEr}}\n\n## In context\n\n{{{neighborhoodErBlock}}}\n{{/hasNeighborhoodEr}}\n{{#fields.hasFields}}\n\n## Fields\n\n| Field | Type | Required | Column | Rules |\n|---|---|---|---|---|\n{{#fields.rows}}\n| {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |\n{{/fields.rows}}\n{{/fields.hasFields}}\n{{#fieldDetails.hasDetails}}\n\n## Field details\n\n{{#fieldDetails.rows}}\n{{{block}}}\n\n{{/fieldDetails.rows}}\n{{/fieldDetails.hasDetails}}\n{{#hasRelationships}}\n\n## Relationships\n\n{{#relationships}}\n- {{{bullet}}}\n{{/relationships}}\n{{/hasRelationships}}\n{{#hasUsedBy}}\n\n## Used by\n\n{{#usedBy}}\n- {{{bullet}}}\n{{/usedBy}}\n{{/hasUsedBy}}\n",
13
+ "docs/template-page.md": "{{{generatedMarker}}}\n\n# {{name}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n\n**Kind:** {{kind}}\n\n## Output\n{{^isEmail}}\n\n- Format: `{{format}}`\n{{/isEmail}}\n{{#isEmail}}\n\nMultipart email — rendered as the following parts:\n\n| Part | Source | Format | Escaping |\n|---|---|---|---|\n{{#parts}}\n| {{label}} | `{{ref}}` | `{{format}}` | {{#escaped}}escaped{{/escaped}}{{^escaped}}raw{{/escaped}} |\n{{/parts}}\n{{/isEmail}}\n\n## Input\n\n- Payload: [`{{payload.name}}`]({{payload.link}})\n{{#hasRequiredTags}}\n- Required fields:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Render contract\n\n- Every field referenced by the template is validated against the payload at generation time; an unknown field fails generation.\n{{#maxChars}}\n- Maximum length: {{.}} characters (rendering longer output fails).\n{{/maxChars}}\n{{#hasRequiredTags}}\n- Required tags must be present:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Source\n\n{{#sourceRefs}}\n- `{{.}}`\n{{/sourceRefs}}\n{{#templateSourceSection}}\n\n{{{.}}}\n{{/templateSourceSection}}\n\n## Capability\n\n{{capability}}\n",
14
+ };
@@ -16,6 +16,7 @@ import type { Provider } from "@metaobjectsdev/render";
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import { join, resolve, dirname } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
+ import { EMBEDDED_FRAMEWORK_TEMPLATES } from "./embedded-templates.generated.js";
19
20
 
20
21
  /** Canonical shipped template — used to verify a candidate framework
21
22
  * templates directory actually contains our defaults. Without this check a
@@ -31,8 +32,11 @@ const CANONICAL_TEMPLATE_REL = "docs/entity-page.md.mustache";
31
32
  *
32
33
  * Returns `undefined` when no on-disk templates dir can be found — e.g. inside
33
34
  * the `bun build --compile` standalone binary, whose `import.meta.url` is a
34
- * `/$bunfs/root` virtual path with no real `package.json` alongside it. The
35
- * embedded-template fallback (see `FileSystemProvider`) covers that case. */
35
+ * `/$bunfs/root` virtual path with no real `package.json` alongside it. In that
36
+ * case `FrameworkTemplatesProvider.resolve` falls back to the bundled
37
+ * `EMBEDDED_FRAMEWORK_TEMPLATES` (embedded-templates.generated.ts), a plain
38
+ * string module generated from the canonical templates/docs/*.mustache and
39
+ * compiled into the binary. */
36
40
  function findFrameworkTemplatesDir(start: string): string | undefined {
37
41
  let dir = start;
38
42
  while (true) {
@@ -81,23 +85,33 @@ export class FileSystemProvider implements Provider {
81
85
  }
82
86
 
83
87
  /** The framework defaults provider — resolves refs against codegen-ts's own
84
- * on-disk `templates/` directory.
88
+ * on-disk `templates/` directory, falling back to the bundled
89
+ * `EMBEDDED_FRAMEWORK_TEMPLATES` when no on-disk dir exists.
85
90
  *
86
91
  * Resolution is lazy: the directory is located on first `resolve()`, not at
87
92
  * module import. This keeps merely importing this module side-effect-free,
88
93
  * which matters for the `bun build --compile` standalone `meta` binary — its
89
94
  * `import.meta.url` is a `/$bunfs/root` virtual path with no on-disk
90
- * `templates/` dir, so eager resolution at import time used to throw before
91
- * any command (even `--help` or the schema ops `migrate`/`verify --db`) could
92
- * run. Now non-codegen commands import cleanly; only the codegen doc path
93
- * (which the standalone binary doesn't target) needs the on-disk dir. */
95
+ * `templates/` dir.
96
+ *
97
+ * ON-DISK FIRST, then embedded: the source/install layout (and adopter
98
+ * overrides chained ahead of this provider via `projectProvider`) always wins,
99
+ * so local edits to the shipped package `templates/` still take effect. Only
100
+ * when the on-disk dir is unresolved (the compiled binary) — or a ref the dir
101
+ * doesn't contain — do we consult the embedded map, which is generated from
102
+ * the same canonical templates and compiled into the binary. Unknown refs are
103
+ * `undefined` in the map, which is the correct miss. */
94
104
  class FrameworkTemplatesProvider implements Provider {
95
105
  resolve(ref: string): string | undefined {
106
+ // On-disk first: dev/install layout, plus shipped-package edits.
96
107
  const dir = frameworkTemplatesDir();
97
- if (dir === undefined) return undefined;
98
- const path = join(dir, `${ref}.mustache`);
99
- if (!existsSync(path)) return undefined;
100
- return readFileSync(path, "utf-8");
108
+ if (dir !== undefined) {
109
+ const path = join(dir, `${ref}.mustache`);
110
+ if (existsSync(path)) return readFileSync(path, "utf-8");
111
+ }
112
+ // Embedded fallback: the binary case (no on-disk dir) or a ref the on-disk
113
+ // dir doesn't carry. `undefined` for unknown refs is the correct miss.
114
+ return EMBEDDED_FRAMEWORK_TEMPLATES[ref];
101
115
  }
102
116
  }
103
117
 
package/src/runner.ts CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  * from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
21
21
  const VALID_ENTITY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
22
 
23
+ /** ADR-0025: doc generators whose single door is `meta docs`. If a `meta gen`
24
+ * config still lists one (by its stable `name`), the runner warns + skips it. */
25
+ const DEPRECATED_DOC_GENERATORS = new Set(["docs-file", "api-docs"]);
26
+
23
27
  export interface RunGenOpts {
24
28
  config: MetaobjectsGenConfig;
25
29
  metadata: MetaData;
@@ -143,9 +147,24 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
143
147
  root.objects().map((o) => [o.name, o.package]),
144
148
  );
145
149
 
150
+ // Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
151
+ // surface it on every generator's ctx.config so api-docs documents the Hono
152
+ // CRUD surface it actually emits (rather than silently omitting it).
153
+ const includeHonoRoutes = config.generators.some((g) => g.emitsHonoRoutes === true);
154
+
146
155
  // 4. Run each generator with a per-target render context; collect with full path.
147
156
  const emitted: { fullPath: string; content: string; generatedBy: string }[] = [];
148
157
  for (const generator of config.generators) {
158
+ // ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
159
+ // still lists a deprecated doc generator is warned + skipped, not run — the
160
+ // generator stays as `meta docs`'s internal engine.
161
+ if (DEPRECATED_DOC_GENERATORS.has(generator.name)) {
162
+ warnings.push(
163
+ `[${generator.name}] docs are produced by 'meta docs' (ADR-0025); ` +
164
+ `remove ${generator.name === "api-docs" ? "apiDocsFile()" : "docsFile()"} from generators. Skipped.`,
165
+ );
166
+ continue;
167
+ }
149
168
  const selfTarget = targetOf(generator);
150
169
  const renderContext = makeRenderContext({
151
170
  dialect: config.dialect,
@@ -162,6 +181,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
162
181
  packageOf,
163
182
  selfTarget,
164
183
  entityModuleTarget,
184
+ ...(config.providedEnumModule !== undefined && { providedEnumModule: config.providedEnumModule }),
165
185
  });
166
186
  const ctx: GenContext = {
167
187
  entities: safeEntities,
@@ -173,6 +193,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
173
193
  dbImport: selfTarget.dbImport,
174
194
  dialect: config.dialect,
175
195
  outputLayout: selfTarget.outputLayout,
196
+ includeHonoRoutes,
176
197
  },
177
198
  renderContext,
178
199
  ...(projectRoot !== undefined && { projectRoot }),
@@ -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",
@@ -16,6 +16,7 @@ import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
16
16
  import { tableNameFromEntity, variableNameFromEntity, 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:
@@ -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);
95
+ const fieldDocs = renderDocsFor(child);
96
+ const columnLine = renderColumn(
97
+ spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, 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
 
@@ -212,6 +236,9 @@ function renderColumn(
212
236
  isComposite: boolean,
213
237
  isUnique: boolean = false,
214
238
  entityPackage: string | undefined = undefined,
239
+ // FR-017 Tier 2 — TPH subtype-only column: force nullable (drop .notNull())
240
+ // and suppress any DB default (other-subtype rows must stay NULL here).
241
+ forceNullable: boolean = false,
215
242
  ): Code {
216
243
  const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
217
244
 
@@ -261,6 +288,9 @@ function renderColumn(
261
288
  if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
262
289
  // Avoid double-emitting .unique() if it was already appended above.
263
290
  if (isUnique && m === ".unique()") continue;
291
+ // TPH subtype-only column: never .notNull() / .unique() — rows of other
292
+ // subtypes store NULL, so neither constraint can hold across the table.
293
+ if (forceNullable && (m === ".notNull()" || m === ".unique()")) continue;
264
294
  modifiersStr += m;
265
295
  }
266
296
 
@@ -268,7 +298,7 @@ function renderColumn(
268
298
  // the `sql` import via imp(); a raw `.default(sql`...`)` would leave `sql`
269
299
  // unresolved in the generated file.
270
300
  let sqlDefaultSegment: Code | null = null;
271
- if (spec.defaultExpr !== undefined && !isPk) {
301
+ if (spec.defaultExpr !== undefined && !isPk && !forceNullable) {
272
302
  if (spec.defaultExpr.kind === "now") {
273
303
  if (ctx.dialect === "sqlite") {
274
304
  const sqlSym = imp("sql@drizzle-orm");
@@ -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}`;
@@ -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),
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,11 +27,12 @@ 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. */