@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
@@ -5,20 +5,28 @@
5
5
  // D5: the notes attr is NEVER emitted. Per the Documentation Provider design.
6
6
 
7
7
  import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
8
- import { DOC_ATTR_DESCRIPTION } from "@metaobjectsdev/metadata";
8
+ import {
9
+ DOC_ATTR_DESCRIPTION,
10
+ FIELD_SUBTYPE_OBJECT,
11
+ FIELD_ATTR_OBJECT_REF,
12
+ OBJECT_SUBTYPE_VALUE,
13
+ } from "@metaobjectsdev/metadata";
9
14
  import { readDocAttrs } from "./jsdoc.js";
10
15
 
11
- /** Render a docs/model.md body: Mermaid erDiagram + per-entity prose. Abstract
12
- * entities are excludedthey have no physical table to put in a diagram
13
- * (matches migrate-ts/expected-schema.ts's same filter). */
14
- export function renderMermaidModel(root: MetaRoot): string {
16
+ /** Render JUST the fenced ```mermaid erDiagram``` block for the whole model
17
+ * (entities + identity.reference relationships) NO per-entity prose. This is
18
+ * the single neutral ER-diagram builder; both `renderMermaidModel()` (the
19
+ * standalone model.md body) AND the neutral docs OVERVIEW page (`README.md`,
20
+ * emitted by the Tier-2 `meta docs` engine) consume it, so there is exactly
21
+ * ONE place ER topology is computed — no duplicate graph logic (ADR-0020).
22
+ *
23
+ * Abstract entities are excluded — they have no physical table to put in a
24
+ * diagram (matches migrate-ts/expected-schema.ts's same filter). */
25
+ export function renderMermaidErBlock(root: MetaRoot): string {
15
26
  const entities = root
16
27
  .objects()
17
28
  .filter((o) => o.isEntity() && !o.isAbstract);
18
29
  const parts: string[] = [];
19
-
20
- parts.push("# Data Model");
21
- parts.push("");
22
30
  parts.push("```mermaid");
23
31
  parts.push("erDiagram");
24
32
  for (const line of renderRelationships(entities)) parts.push(` ${line}`);
@@ -27,6 +35,22 @@ export function renderMermaidModel(root: MetaRoot): string {
27
35
  for (const line of renderEntityBlock(entity)) parts.push(` ${line}`);
28
36
  }
29
37
  parts.push("```");
38
+ return parts.join("\n");
39
+ }
40
+
41
+ /** Render a docs/model.md body: Mermaid erDiagram + per-entity prose. Abstract
42
+ * entities are excluded — they have no physical table to put in a diagram
43
+ * (matches migrate-ts/expected-schema.ts's same filter). Reuses the shared
44
+ * `renderMermaidErBlock()` for the diagram so the ER logic is never duplicated. */
45
+ export function renderMermaidModel(root: MetaRoot): string {
46
+ const entities = root
47
+ .objects()
48
+ .filter((o) => o.isEntity() && !o.isAbstract);
49
+ const parts: string[] = [];
50
+
51
+ parts.push("# Data Model");
52
+ parts.push("");
53
+ parts.push(renderMermaidErBlock(root));
30
54
  parts.push("");
31
55
 
32
56
  for (const entity of entities) {
@@ -37,6 +61,150 @@ export function renderMermaidModel(root: MetaRoot): string {
37
61
  return parts.join("\n");
38
62
  }
39
63
 
64
+ /** Render a Mermaid flowchart for ONE focal entity and its direct neighbors
65
+ * (1-hop). Three node kinds are surfaced and styled distinctly:
66
+ *
67
+ * - **focal** — the entity this page is about (deeper blue)
68
+ * - **same** — entity in the focal's own package (blue)
69
+ * - **external** — entity in a different package (dashed gray)
70
+ * - **vo** — value object referenced via `field.object` (rounded
71
+ * purple) — composition rather than FK
72
+ *
73
+ * Every node has a Mermaid `click <node> "./<node>.md"` directive, so the
74
+ * rendered SVG is a true navigation surface — click any neighbor to jump to
75
+ * its page. Edges are labeled with the field name that connects them.
76
+ *
77
+ * This replaces the previous erDiagram-based renderer: Mermaid 10's erDiagram
78
+ * has near-zero styling and no click support, while flowchart supports both.
79
+ * The crow's-foot one-to-many notation is dropped — for a 1-hop context
80
+ * diagram, "what kind of node is this" matters more than ER cardinality.
81
+ *
82
+ * Returns `undefined` when the focal entity has no neighbors at all, so a
83
+ * template can `{{#mini}}…{{/mini}}` and skip the section cleanly. Abstract
84
+ * entities are excluded as neighbors (no physical rows). */
85
+ export function renderEntityNeighborhoodErBlock(
86
+ focal: MetaObject,
87
+ root: MetaRoot,
88
+ ): string | undefined {
89
+ if (focal.isAbstract) return undefined;
90
+
91
+ // Resolve neighbor kind so the flowchart can color/shape it accordingly.
92
+ // Same-package = focal's effective package; differing or unknown = external.
93
+ // Value object = object.value subtype. Aborts return classify→undefined.
94
+ //
95
+ // `node.package` is the OWN attr only — entities that take their package
96
+ // from the file-default (`metadata.root: { package: ... }`) leave it
97
+ // undefined. Use `fileDefaultPackage` as the fallback so cross-file
98
+ // boundary detection works in the common case where adopters set the
99
+ // package once at the root, not on every entity. Mirrors the same
100
+ // resolution rule docs-paths.ts uses for page placement.
101
+ const byName = new Map<string, MetaObject>();
102
+ for (const obj of root.objects()) byName.set(obj.name, obj);
103
+ const effectivePkg = (n: MetaObject) => n.package ?? n.fileDefaultPackage;
104
+ const focalPackage = effectivePkg(focal);
105
+
106
+ const sameDomain = new Set<string>();
107
+ const external = new Set<string>();
108
+ const valueObjs = new Set<string>();
109
+
110
+ type NeighborKind = "same" | "external" | "vo";
111
+ function classify(name: string): NeighborKind | undefined {
112
+ if (name === focal.name) return "same"; // self-reference (parent-id etc.)
113
+ const target = byName.get(name);
114
+ if (!target) return undefined;
115
+ if (target.subType === OBJECT_SUBTYPE_VALUE) {
116
+ valueObjs.add(name);
117
+ return "vo";
118
+ }
119
+ if (target.isAbstract) return undefined;
120
+ if (effectivePkg(target) === focalPackage) {
121
+ sameDomain.add(name);
122
+ return "same";
123
+ }
124
+ external.add(name);
125
+ return "external";
126
+ }
127
+
128
+ const edges: Array<{ from: string; to: string; label: string }> = [];
129
+
130
+ // Outgoing FK — focal references other entities.
131
+ for (const ref of focal.referenceIdentities()) {
132
+ const target = ref.targetEntity;
133
+ const field = ref.fields[0];
134
+ if (typeof target !== "string" || typeof field !== "string") continue;
135
+ const targetName = target.split("::").pop()!;
136
+ if (classify(targetName) === undefined) continue;
137
+ edges.push({ from: focal.name, to: targetName, label: field });
138
+ }
139
+
140
+ // Incoming FK — entities that reference the focal.
141
+ for (const other of root.objects().filter(o => o.isEntity() && !o.isAbstract)) {
142
+ if (other.name === focal.name) continue;
143
+ for (const ref of other.referenceIdentities()) {
144
+ const target = ref.targetEntity;
145
+ if (typeof target !== "string") continue;
146
+ if (target.split("::").pop() !== focal.name) continue;
147
+ const field = ref.fields[0];
148
+ if (typeof field !== "string") continue;
149
+ if (classify(other.name) === undefined) continue;
150
+ edges.push({ from: other.name, to: focal.name, label: field });
151
+ }
152
+ }
153
+
154
+ // Value-object composition — `field.object @objectRef ContactInfo` etc.
155
+ // These aren't FKs (no row-level identity); they're embedded composition.
156
+ // Surfaced because adopters care: "what's inside this entity's `jsonb`
157
+ // column?" Click-through to the VO docs answers that.
158
+ for (const field of focal.fields()) {
159
+ if (field.subType !== FIELD_SUBTYPE_OBJECT) continue;
160
+ const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
161
+ if (typeof ref !== "string" || ref.length === 0) continue;
162
+ const targetName = ref.split("::").pop()!;
163
+ if (classify(targetName) === undefined) continue;
164
+ edges.push({ from: focal.name, to: targetName, label: field.name });
165
+ }
166
+
167
+ if (edges.length === 0) return undefined;
168
+
169
+ // Build the flowchart. Nodes first (so all are declared before edges),
170
+ // then edges, then click directives, then classDefs + class assignments.
171
+ const allNodes = new Set<string>([focal.name, ...sameDomain, ...external, ...valueObjs]);
172
+ const parts: string[] = ["```mermaid", "flowchart TB"];
173
+
174
+ for (const name of allNodes) {
175
+ if (valueObjs.has(name)) {
176
+ // Stadium (rounded-rect) shape signals composition / embedded value.
177
+ parts.push(` ${name}(["${name}"])`);
178
+ } else {
179
+ // Sharp rectangle for entities.
180
+ parts.push(` ${name}["${name}"]`);
181
+ }
182
+ }
183
+ for (const { from, to, label } of edges) {
184
+ parts.push(` ${from} -->|"${label}"| ${to}`);
185
+ }
186
+ // Click directives — link every node (focal + neighbors) to its docs page.
187
+ // Flat-layout assumption: `./<Name>.md` lives next to the focal's page.
188
+ // Package-layout adopters can override the template; the data plumbing
189
+ // is identical either way.
190
+ for (const name of allNodes) {
191
+ parts.push(` click ${name} "./${name}.md"`);
192
+ }
193
+ // Visual styling: 4 node kinds, 4 classes. Color choices are deliberately
194
+ // muted so the diagram reads as documentation, not infographic art.
195
+ parts.push(" classDef focal fill:#dbeafe,stroke:#1e40af,stroke-width:2px,color:#1e293b;");
196
+ parts.push(" classDef same fill:#eff6ff,stroke:#3b82f6,color:#1e293b;");
197
+ parts.push(" classDef external fill:#f3f4f6,stroke:#9ca3af,stroke-dasharray:4 3,color:#374151;");
198
+ parts.push(" classDef vo fill:#faf5ff,stroke:#9333ea,color:#1e293b;");
199
+ parts.push(` class ${focal.name} focal;`);
200
+ const sameOthers = [...sameDomain].filter(n => n !== focal.name);
201
+ if (sameOthers.length > 0) parts.push(` class ${sameOthers.join(",")} same;`);
202
+ if (external.size > 0) parts.push(` class ${[...external].join(",")} external;`);
203
+ if (valueObjs.size > 0) parts.push(` class ${[...valueObjs].join(",")} vo;`);
204
+ parts.push("```");
205
+ return parts.join("\n");
206
+ }
207
+
40
208
  function renderRelationships(entities: MetaObject[]): string[] {
41
209
  const lines: string[] = [];
42
210
  for (const entity of entities) {
@@ -2,11 +2,12 @@
2
2
  //
3
3
  // Per-template renderer for template.output codegen. Walks the @payloadRef's
4
4
  // value-object into a Zod schema and emits a dual-API parser (parse + safeParse)
5
- // alongside the schema. The emitted file is self-contained: it derives a
6
- // local data type via `z.infer<typeof Schema>` and exports it as
7
- // `<TemplateName>Data`. Consumers wiring `promptRender()` get a structurally
8
- // identical payload-VO interface in `prompts.ts`; either type can be used
9
- // interchangeably with parse results.
5
+ // alongside the schema, plus (for json/xml outputs) a single tolerant
6
+ // loader-delegating `extractLenient<Name>WithLoader(root, text)` that delegates to
7
+ // the metadata-driven runtime extract. The emitted file derives a local data type
8
+ // via `z.infer<typeof Schema>` and exports it as `<TemplateName>Data`. Consumers
9
+ // wiring `promptRender()` get a structurally identical payload-VO interface in
10
+ // `prompts.ts`; either type can be used interchangeably with parse results.
10
11
 
11
12
  import {
12
13
  type MetaData,
@@ -18,12 +19,8 @@ import {
18
19
  FIELD_ATTR_OBJECT_REF,
19
20
  TEMPLATE_ATTR_PAYLOAD_REF,
20
21
  TEMPLATE_ATTR_FORMAT,
22
+ refMatchesObject,
21
23
  } from "@metaobjectsdev/metadata";
22
- import {
23
- schemaLiteral,
24
- mirrorInitializer,
25
- } from "./extract-schema-emitter.js";
26
- import { extractMapHelpersUsed } from "./fr010-field-mapping.js";
27
24
  import {
28
25
  nestedMirrorInterfaces,
29
26
  nestedMappers,
@@ -46,7 +43,9 @@ const SCALAR_ZOD: Record<string, string> = {
46
43
  };
47
44
 
48
45
  function findObject(root: MetaData, name: string): MetaData | undefined {
49
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
46
+ // FR-032 @payloadRef is FQN after the desugar/sweep; match on the effective
47
+ // FQN resolution key (with bare back-compat).
48
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
50
49
  }
51
50
 
52
51
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
@@ -166,96 +165,52 @@ export function ${safeParseName}(
166
165
 
167
166
  // ---- FR-010 tolerant extract block (json/xml only) ----
168
167
  const extractedName = `${templateName}Extracted`;
169
- const extractLenientFnName = `extractLenient${templateName}`;
170
- const tryExtractLenientName = `tryExtractLenient${templateName}`;
171
168
  const extractLenientWithName = `extractLenient${templateName}WithLoader`;
172
- const schemaConstName = `${templateName}ExtractSchema`;
173
169
  const payloadFqnConst = `${templateName.toUpperCase()}_PAYLOAD_NAME`;
174
170
  const formatEnum = format.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
175
- const schemaLit = schemaLiteral(vo, format, payloadRef);
176
- const initializer = mirrorInitializer(vo);
177
- const mapHelpers = extractMapHelpersUsed(vo);
178
171
 
179
- // The nullable mirror is shared by BOTH extract paths. Use the nested-aware emitter so the
180
- // payload mirror's nested-object / array-of-object components are typed (not `unknown`), and
181
- // so a mirror interface is emitted for every reachable nested value-object. The payload mirror
182
- // keeps the canonical `<Template>Extracted` name (instead of `<PayloadVO>Extracted`) so the
183
- // existing self-contained extract<Name>() initializer continues to satisfy it.
172
+ // The nullable mirror is the return shape of the delegating extract. Use the nested-aware
173
+ // emitter so the payload mirror's nested-object / array-of-object components are typed (not
174
+ // `unknown`), and so a mirror interface is emitted for every reachable nested value-object.
175
+ // The payload mirror keeps the canonical `<Template>Extracted` name.
184
176
  const mirrorDecls = nestedMirrorInterfaces(vo, root, extractedName);
185
- const payloadHasNested = hasNested(vo, root);
186
-
187
- // Render-package imports the extract block needs. Only pull in the names the emitted
188
- // source actually references, so the file has no unused imports (tsc noUnusedLocals-safe).
189
- const renderImports = ["extract", "extractSchema", "Format"];
190
- if (schemaLit.includes("scalar(")) renderImports.push("scalar");
191
- if (schemaLit.includes("enumField(")) renderImports.push("enumField");
192
- if (schemaLit.includes("FieldKind.")) renderImports.push("FieldKind");
193
- renderImports.push("type ExtractSchema", "type ExtractOptions", "type ExtractionResult");
194
- renderImports.push(...mapHelpers);
195
-
196
- const selfContained = `/** Baked extract descriptor for the ${templateName} output. */
197
- const ${schemaConstName}: ExtractSchema = ${schemaLit};
198
-
199
- ${mirrorDecls}
200
-
201
- /**
202
- * Self-contained tolerant best-effort extraction of a dirty LLM response; never throws.
203
- * Returns a nullable mirror (\`${extractedName}\`) with fields null where lost/malformed,
204
- * plus the per-field extraction report. Does NOT populate nested-object / array-of-object
205
- * components (those stay null — the historical FR-010 gap). For full nested extraction, use
206
- * \`${extractLenientWithName}(root, text)\`, which delegates to the runtime extract.
207
- */
208
- export function ${extractLenientFnName}(
209
- text: string,
210
- opts?: ExtractOptions,
211
- ): ExtractionResult<${extractedName}> {
212
- const outcome = extract(text, ${schemaConstName}, opts);
213
- const d = outcome.data;
214
- const data: ${extractedName} = ${initializer};
215
- return { data, report: outcome.report };
216
- }
217
177
 
218
- /**
219
- * Extraction as a bool gate: \`true\` when the response was non-empty and no required
220
- * field was lost. On success, \`result\` carries the extracted mirror + report.
221
- */
222
- export function ${tryExtractLenientName}(
223
- text: string,
224
- ): { ok: boolean; result: ExtractionResult<${extractedName}> } {
225
- const result = ${extractLenientFnName}(text);
226
- const ok = !result.report.isEmpty() && !result.report.hasLostRequired();
227
- return { ok, result };
228
- }
229
- `;
178
+ // Render-package imports the (single, loader-delegating) extract block needs. Kept minimal so
179
+ // the file has no unused imports (tsc noUnusedLocals-safe).
180
+ const renderImports = ["Format", "type ExtractOptions", "type ExtractionResult"];
230
181
 
231
- // ---- Runtime-delegating extract (closes the nested gap) ----
182
+ // ---- Runtime-delegating extract (the single metadata-driven extract path) ----
232
183
  // Resolves this payload's MetaObject from a loaded MetaRoot by its baked simple name and
233
184
  // delegates to extractObject() in @metaobjectsdev/runtime-ts, which assembles the FULL nested
234
- // object graph reflection-free. The assembled ValueObject graph is then mapped into the typed
235
- // nullable mirror graph by the generated from<VO>Extracted mappers. Codegen-wrapping-runtime
236
- // (a generated DAO calling the dynamic-metadata runtime) — mirrors the Java/Kotlin pilots.
185
+ // object graph reflection-free by reading the live metadata directly. The assembled ValueObject
186
+ // graph is then mapped into the typed nullable mirror graph by the generated from<VO>Extracted
187
+ // mappers. Codegen-wrapping-runtime (a generated DAO calling the dynamic-metadata runtime).
237
188
  //
238
189
  // The baked PAYLOAD_NAME is the resolved payload VO's SIMPLE name (root.findObject matches on
239
190
  // the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so it returns
240
191
  // the canonically-named `<Template>Extracted` mirror); nested mappers use their VO names.
241
192
  const payloadName = vo.name;
242
193
  const rootMapper = rootMapperName(templateName);
194
+ void hasNested;
243
195
  const delegating = `
244
196
  /** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime. */
245
197
  export const ${payloadFqnConst} = ${JSON.stringify(payloadName)};
246
198
 
199
+ ${mirrorDecls}
200
+
247
201
  ${nestedMappers(vo, root, rootMapper, extractedName)}
248
202
 
249
203
  ${delegateHelpers(usedHelpers(vo, root))}
250
204
 
251
205
  /**
252
- * Runtime-delegating tolerant extraction; never throws. Unlike \`${extractLenientFnName}(text)\`, this FULLY
253
- * populates nested-object and array-of-object components by delegating to the metadata-driven
254
- * runtime \`extractObject\` (which assembles the whole graph reflection-free via the Phase A object
255
- * model), then maps the assembled graph into the typed \`${extractedName}\` mirror.
206
+ * Runtime-delegating tolerant best-effort extraction; never throws. FULLY populates
207
+ * nested-object and array-of-object components by delegating to the metadata-driven runtime
208
+ * \`extractObject\` (which assembles the whole graph reflection-free via the Phase A object
209
+ * model, reading the live metadata directly), then maps the assembled graph into the typed
210
+ * \`${extractedName}\` mirror.
256
211
  *
257
212
  * @param root a loaded MetaRoot (e.g. \`(await new MetaDataLoader().load(...)).root\`) that declares
258
- * the \`${payloadRef}\` value-object.
213
+ * the \`${payloadName}\` value-object.
259
214
  */
260
215
  export function ${extractLenientWithName}(
261
216
  root: MetaRoot,
@@ -272,9 +227,6 @@ export function ${extractLenientWithName}(
272
227
  `;
273
228
 
274
229
  // The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata.
275
- // It is always emitted (the gap-closing path), regardless of whether THIS payload has nested
276
- // fields — a flat payload still benefits from the loader-driven path and keeps the API uniform.
277
- void payloadHasNested;
278
230
  const metadataImport = `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
279
231
  const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
280
232
 
@@ -285,7 +237,6 @@ export function ${extractLenientWithName}(
285
237
  runtimeImport +
286
238
  `\n` +
287
239
  `${strictBody}\n` +
288
- `${selfContained}\n` +
289
240
  `${delegating}`
290
241
  );
291
242
  }
@@ -17,11 +17,12 @@ import {
17
17
  TEMPLATE_SUBTYPE_OUTPUT,
18
18
  TEMPLATE_ATTR_PAYLOAD_REF,
19
19
  TEMPLATE_ATTR_FORMAT,
20
+ refMatchesObject,
20
21
  } from "@metaobjectsdev/metadata";
21
22
  import { specLiteral } from "./output-format-spec-emitter.js";
22
23
 
23
24
  function findObject(root: MetaData, name: string): MetaData | undefined {
24
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
25
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
25
26
  }
26
27
 
27
28
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
@@ -1,8 +1,12 @@
1
1
  // Queries file composer — composes all CRUD function renderers (from queries.ts) into
2
2
  // a complete <Entity>.queries.ts file with @generated header and correct imports.
3
3
 
4
- import { code, joinCode, type Code } from "ts-poet";
5
- import { MetaObject } from "@metaobjectsdev/metadata";
4
+ import { code, joinCode, imp, type Code } from "ts-poet";
5
+ import {
6
+ MetaObject,
7
+ OBJECT_ATTR_DISCRIMINATOR,
8
+ OBJECT_ATTR_DISCRIMINATOR_VALUE,
9
+ } from "@metaobjectsdev/metadata";
6
10
  import { type RenderContext } from "../render-context.js";
7
11
  import { entityModuleSpecifier } from "../import-path.js";
8
12
  import {
@@ -11,11 +15,21 @@ import {
11
15
  renderCreateFn,
12
16
  renderUpdateFn,
13
17
  renderDeleteByIdFn,
18
+ getPkInfo,
14
19
  } from "./queries.js";
15
- import { variableNameFromEntity } from "../naming.js";
20
+ import { variableNameFromEntity, pluralize } from "../naming.js";
16
21
  import { GENERATED_HEADER } from "../constants.js";
22
+ import { isTphDiscriminatorBase, tphConcreteSubtypes } from "./tph-discriminator.js";
17
23
 
18
24
  export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
25
+ // FR-017 Tier 2 — a TPH discriminator base gets a polymorphic queries file:
26
+ // base reads dispatch through parse<Base>, and per-subtype CRUD targets the
27
+ // single base table scoped to the discriminator value. (Subtype entities are
28
+ // filtered out of the queries generator entirely — their CRUD lives here.)
29
+ if (isTphDiscriminatorBase(obj, ctx.loadedRoot)) {
30
+ return renderTphQueriesFile(obj, ctx);
31
+ }
32
+
19
33
  const entityName = obj.name;
20
34
  // Import the entity's own file. Same target → relative "./Entity"; cross
21
35
  // target → importBase-qualified package path.
@@ -68,3 +82,118 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
68
82
  `// Customize via ${entityName}.extra.ts in this directory (additional queries, custom logic).\n`;
69
83
  return header + body;
70
84
  }
85
+
86
+ /**
87
+ * FR-017 Tier 2 — the polymorphic + per-subtype queries file for a TPH base.
88
+ *
89
+ * Base reads (`find<Base>ById`, `list<BasePlural>`) project every row through
90
+ * `parse<Base>` so they return the discriminated union. There is intentionally
91
+ * NO `create<Base>` / `update<Base>` — you cannot instantiate an abstract base.
92
+ *
93
+ * Each concrete subtype gets list / findById (filtered to the discriminator
94
+ * value, parsed with `<Sub>Schema`) plus create / updateById / deleteById, all
95
+ * against the single base table. Creates inject the discriminator value;
96
+ * updates strip it (a row's subtype is immutable).
97
+ */
98
+ function renderTphQueriesFile(base: MetaObject, ctx: RenderContext): string {
99
+ const baseName = base.name;
100
+ const tableVar = variableNameFromEntity(baseName);
101
+ const discField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR) as string;
102
+ const { fieldName: pkField, tsType: pkType } = getPkInfo(base, ctx);
103
+
104
+ const baseFileSpec = entityModuleSpecifier(
105
+ ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
106
+ );
107
+ const tableSym = imp(`${tableVar}@${baseFileSpec}`);
108
+ const baseTypeSym = imp(`t:${baseName}@${baseFileSpec}`);
109
+ const parseSym = imp(`parse${baseName}@${baseFileSpec}`);
110
+ const eqSym = imp("eq@drizzle-orm");
111
+ const andSym = imp("and@drizzle-orm");
112
+
113
+ const dbTypeImport =
114
+ ctx.dialect === "postgres"
115
+ ? `import type { NodePgDatabase } from "drizzle-orm/node-postgres";`
116
+ : `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
117
+ const dbTypeAlias =
118
+ ctx.dialect === "postgres"
119
+ ? `type Db = NodePgDatabase<Record<string, never>>;`
120
+ : `type Db = BaseSQLiteDatabase<"async", Record<string, never>>;`;
121
+
122
+ // --- Polymorphic base reads ---
123
+ const polymorphic = code`
124
+ export async function find${baseName}ById(db: Db, ${pkField}: ${pkType}): Promise<${baseTypeSym} | null> {
125
+ const [row] = await db.select().from(${tableSym}).where(${eqSym}(${tableSym}.${pkField}, ${pkField})).limit(1);
126
+ return row ? ${parseSym}(row) : null;
127
+ }
128
+
129
+ export async function list${pluralize(baseName)}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${baseTypeSym}[]> {
130
+ let q = db.select().from(${tableSym}).$dynamic();
131
+ if (opts?.limit !== undefined) q = q.limit(opts.limit);
132
+ if (opts?.offset !== undefined) q = q.offset(opts.offset);
133
+ const rows = await q;
134
+ return rows.map((r) => ${parseSym}(r));
135
+ }
136
+ `;
137
+
138
+ // --- Per-subtype CRUD against the single base table ---
139
+ const subtypeSections: Code[] = [];
140
+ for (const sub of tphConcreteSubtypes(base, ctx.loadedRoot)) {
141
+ const value = sub.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) as string;
142
+ const valueLit = JSON.stringify(value);
143
+ const subFileSpec = entityModuleSpecifier(
144
+ ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
145
+ );
146
+ const subTypeSym = imp(`t:${sub.name}@${subFileSpec}`);
147
+ const subSchemaSym = imp(`${sub.name}Schema@${subFileSpec}`);
148
+ const subInsertSym = imp(`${sub.name}InsertSchema@${subFileSpec}`);
149
+
150
+ subtypeSections.push(code`
151
+ export async function list${pluralize(sub.name)}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${subTypeSym}[]> {
152
+ let q = db.select().from(${tableSym}).where(${eqSym}(${tableSym}.${discField}, ${valueLit})).$dynamic();
153
+ if (opts?.limit !== undefined) q = q.limit(opts.limit);
154
+ if (opts?.offset !== undefined) q = q.offset(opts.offset);
155
+ const rows = await q;
156
+ return rows.map((r) => ${subSchemaSym}.parse(r));
157
+ }
158
+
159
+ export async function find${sub.name}ById(db: Db, ${pkField}: ${pkType}): Promise<${subTypeSym} | null> {
160
+ const [row] = await db.select().from(${tableSym})
161
+ .where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).limit(1);
162
+ return row ? ${subSchemaSym}.parse(row) : null;
163
+ }
164
+
165
+ export async function create${sub.name}(db: Db, data: unknown): Promise<${subTypeSym}> {
166
+ const validated = ${subInsertSym}.parse(data);
167
+ const [row] = await db.insert(${tableSym}).values({ ...validated, ${discField}: ${valueLit} }).returning();
168
+ return ${subSchemaSym}.parse(row!);
169
+ }
170
+
171
+ export async function update${sub.name}ById(db: Db, ${pkField}: ${pkType}, data: unknown): Promise<${subTypeSym} | null> {
172
+ const validated = ${subInsertSym}.partial().parse(data) as Record<string, unknown>;
173
+ // The discriminator is immutable — a ${sub.name} can never become another subtype.
174
+ const { [${JSON.stringify(discField)}]: _disc, ...safe } = validated;
175
+ const [row] = await db.update(${tableSym}).set(safe)
176
+ .where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).returning();
177
+ return row ? ${subSchemaSym}.parse(row) : null;
178
+ }
179
+
180
+ export async function delete${sub.name}ById(db: Db, ${pkField}: ${pkType}): Promise<boolean> {
181
+ const deleted = await db.delete(${tableSym})
182
+ .where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).returning();
183
+ return deleted.length > 0;
184
+ }
185
+ `);
186
+ }
187
+
188
+ const literalImports = code`
189
+ ${dbTypeImport}
190
+ ${dbTypeAlias}
191
+ `;
192
+
193
+ const body = joinCode([literalImports, polymorphic, ...subtypeSections], { on: "\n" }).toString();
194
+ const header =
195
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
196
+ `// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
197
+ `// Customize via ${baseName}.extra.ts in this directory (additional queries, custom logic).\n`;
198
+ return header + body;
199
+ }
@@ -5,10 +5,18 @@ import { code, imp, type Code } from "ts-poet";
5
5
  import type { MetaObject } from "@metaobjectsdev/metadata";
6
6
  import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
7
7
  import type { RenderContext } from "../render-context.js";
8
- import { variableNameFromEntity, pluralize } from "../naming.js";
8
+ import {
9
+ variableNameFromEntity,
10
+ pluralize,
11
+ findByIdFnName,
12
+ listFnName,
13
+ createFnName,
14
+ updateFnName,
15
+ deleteByIdFnName,
16
+ } from "../naming.js";
9
17
 
10
18
  /** Get the PK field name and its TS type for a given entity. */
11
- function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
19
+ export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
12
20
  // Use primaryIdentity() to find the primary identity (may be inherited from extends:/super:).
13
21
  const primary = entity.primaryIdentity();
14
22
  const rawFields = primary?.ownAttr(IDENTITY_ATTR_FIELDS);
@@ -30,7 +38,7 @@ export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
30
38
  const entityName = entity.name;
31
39
  const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
32
40
  const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
33
- const fnName = `find${entityName}ById`;
41
+ const fnName = findByIdFnName(entityName);
34
42
  const eqSym = imp("eq@drizzle-orm");
35
43
 
36
44
  return code`
@@ -46,7 +54,7 @@ export function renderListFn(entity: MetaObject, _ctx: RenderContext): Code {
46
54
  const entityName = entity.name;
47
55
  // Pluralize the PascalCase entity name, preserving capitalization
48
56
  // (e.g., "Category" -> "Categories", not "Categorys").
49
- const fnName = `list${pluralize(entityName)}`;
57
+ const fnName = listFnName(entityName);
50
58
 
51
59
  return code`
52
60
  export async function ${fnName}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${entityName}[]> {
@@ -62,7 +70,7 @@ export function renderCreateFn(entity: MetaObject, _ctx: RenderContext): Code {
62
70
  const varName = variableNameFromEntity(entity.name);
63
71
  const entityName = entity.name;
64
72
  const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
65
- const fnName = `create${entityName}`;
73
+ const fnName = createFnName(entityName);
66
74
  const schemaName = `${entityName}InsertSchema`;
67
75
 
68
76
  return code`
@@ -79,7 +87,7 @@ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
79
87
  const entityName = entity.name;
80
88
  const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
81
89
  const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
82
- const fnName = `update${entityName}`;
90
+ const fnName = updateFnName(entityName);
83
91
  const schemaName = `${entityName}InsertSchema`;
84
92
  const eqSym = imp("eq@drizzle-orm");
85
93
 
@@ -96,7 +104,7 @@ export function renderDeleteByIdFn(entity: MetaObject, ctx: RenderContext): Code
96
104
  const varName = variableNameFromEntity(entity.name);
97
105
  const entityName = entity.name;
98
106
  const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
99
- const fnName = `delete${entityName}ById`;
107
+ const fnName = deleteByIdFnName(entityName);
100
108
  const eqSym = imp("eq@drizzle-orm");
101
109
 
102
110
  return code`
@@ -46,6 +46,23 @@ function renderRelationEntry(
46
46
  thisVarName: string,
47
47
  thisEntityPackage: string | undefined,
48
48
  ): Code {
49
+ // FR-018 M:N: the source navigates through the junction table, so the Drizzle
50
+ // many() targets the JUNCTION, not the target entity (the relational query API
51
+ // then hops junction → target via the junction's one() sides). The
52
+ // mountM2mRoute the routes generator emits performs the flattened two-stage
53
+ // traversal for the REST contract.
54
+ if (entry.cardinality === CARDINALITY_MANY && entry.junctionEntity !== undefined) {
55
+ const junctionSpec = crossEntitySpecifier(
56
+ ctx.outputLayout,
57
+ thisEntityPackage,
58
+ ctx.packageOf.get(entry.junctionEntity),
59
+ entry.junctionEntity,
60
+ ctx.extStyle,
61
+ );
62
+ const junctionVarSym = imp(`${variableNameFromEntity(entry.junctionEntity)}@${junctionSpec}`);
63
+ return code` ${entry.name}: many(${junctionVarSym})`;
64
+ }
65
+
49
66
  // Use imp() for cross-entity references so ts-poet tracks and emits the import.
50
67
  const targetSpec = crossEntitySpecifier(
51
68
  ctx.outputLayout,