@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
@@ -43,6 +43,7 @@ import {
43
43
  TEMPLATE_ATTR_SUBJECT_REF,
44
44
  TEMPLATE_ATTR_HTML_BODY_REF,
45
45
  TEMPLATE_ATTR_TEXT_BODY_REF,
46
+ refMatchesObject,
46
47
  } from "@metaobjectsdev/metadata";
47
48
  import {
48
49
  verify,
@@ -53,7 +54,7 @@ import {
53
54
  } from "@metaobjectsdev/render";
54
55
 
55
56
  function findObject(root: MetaData, name: string): MetaData | undefined {
56
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
57
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
57
58
  }
58
59
 
59
60
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
@@ -192,7 +193,7 @@ export function renderRenderHelper(
192
193
 
193
194
  return `import { render } from "@metaobjectsdev/render";
194
195
  import type { Provider, EmailDocument } from "@metaobjectsdev/render";
195
- import type { ${payloadRef} } from "./payloads.js";
196
+ import type { ${payloadRef} } from "./${payloadRef}.js";
196
197
 
197
198
  /**
198
199
  * Render the ${templateName} email (subject + html body${typeof textBodyRef === "string" ? " + text body" : ""}) from a
@@ -230,7 +231,7 @@ export function ${fnName}(payload: ${payloadRef}, provider: Provider): EmailDocu
230
231
 
231
232
  return `import { render } from "@metaobjectsdev/render";
232
233
  import type { Provider } from "@metaobjectsdev/render";
233
- import type { ${payloadRef} } from "./payloads.js";
234
+ import type { ${payloadRef} } from "./${payloadRef}.js";
234
235
 
235
236
  /**
236
237
  * Render the ${templateName} document from a typed ${payloadRef} payload. Wraps the
@@ -15,17 +15,31 @@
15
15
  // the existing queries-file template). The entity's Drizzle table const is
16
16
  // imported alongside the Zod schemas + constants from the sibling Entity.ts.
17
17
 
18
- import { code, imp } from "ts-poet";
18
+ import { code, imp, joinCode, type Code } from "ts-poet";
19
19
  import type { MetaObject } from "@metaobjectsdev/metadata";
20
+ import {
21
+ TYPE_FIELD,
22
+ resolveColumnName,
23
+ } from "@metaobjectsdev/metadata";
20
24
  import { type RenderContext } from "../render-context.js";
21
- import { entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
25
+ import { crossEntitySpecifier, entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
22
26
  import { GENERATED_HEADER } from "../constants.js";
23
- import { variableNameFromEntity } from "../naming.js";
27
+ import { variableNameFromEntity, routesHandlerName } from "../naming.js";
24
28
  import { isProjection } from "../projection/projection-detector.js";
29
+ import type { RelationEntry } from "../relation-resolver.js";
30
+ import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
25
31
 
26
32
  export function renderRoutesFile(entity: MetaObject, ctx: RenderContext): string {
33
+ // FR-017 Tier 2 — a TPH discriminator base mounts polymorphic list/get at the
34
+ // base path plus a full per-subtype CRUD route set scoped to each
35
+ // discriminator value. (Subtype entities are filtered out of the routes
36
+ // generator entirely — their routes live here.)
37
+ if (isTphDiscriminatorBase(entity, ctx.loadedRoot)) {
38
+ return renderTphRoutesFile(entity, ctx);
39
+ }
40
+
27
41
  const entityName = entity.name;
28
- const handlerName = `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Routes`;
42
+ const handlerName = routesHandlerName(entityName);
29
43
  // Import the entity's own file. Same target → relative "./Entity"; cross
30
44
  // target → importBase-qualified package path.
31
45
  const entityFileSpec = entityModuleSpecifier(
@@ -113,6 +127,19 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
113
127
  const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
114
128
  const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
115
129
 
130
+ // FR-018 M:N traversal: for each many-to-many navigation declared on this
131
+ // entity, emit a mountM2mRoute(...) that traverses the junction. The junction
132
+ // FK columns were derived from the junction's identity.reference children (the
133
+ // SSOT) by the relation-resolver pre-pass; here we resolve them to physical
134
+ // column names for the Drizzle two-stage join.
135
+ const m2mEntries = (ctx.relationMap.get(entityName) ?? []).filter(
136
+ (e): e is RelationEntry & { junctionEntity: string } => e.junctionEntity !== undefined,
137
+ );
138
+ // Two fastify-scope variants: under an apiPrefix the mounts live inside the
139
+ // register-block (`instance`); otherwise they bind directly to `fastify`.
140
+ const m2mMountsPrefixed = renderM2mMounts(m2mEntries, entity, ctx, "instance");
141
+ const m2mMountsFlat = renderM2mMounts(m2mEntries, entity, ctx, "fastify");
142
+
116
143
  const literalImports = code`
117
144
  import { db } from ${JSON.stringify(dbImportSpec)};
118
145
  import {
@@ -148,7 +175,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
148
175
  sortAllowlist: ${entityName}SortAllowlist,
149
176
  dialect: ${JSON.stringify(ctx.dialect)},
150
177
  });
151
- }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
178
+ ${m2mMountsPrefixed} }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
152
179
  }
153
180
  `
154
181
  : code`
@@ -172,8 +199,208 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
172
199
  sortAllowlist: ${entityName}SortAllowlist,
173
200
  dialect: ${JSON.stringify(ctx.dialect)},
174
201
  });
175
- }
202
+ ${m2mMountsFlat}}
176
203
  `;
177
204
 
178
205
  return header + literalImports.toString() + body.toString();
179
206
  }
207
+
208
+ /**
209
+ * Render the M:N traversal mounts for an entity as a single Code fragment to
210
+ * interpolate INTO the handler-body code template (so the junction/target table
211
+ * + mountM2mRoute imports hoist with the rest of the body's imports, not inline
212
+ * mid-function). `fastifyVar` is the in-scope Fastify reference (`instance`
213
+ * under an apiPrefix register-block, else `fastify`). Returns "" when the entity
214
+ * has no M:N relationships — CRUD-only output stays byte-identical to before.
215
+ */
216
+ function renderM2mMounts(
217
+ entries: ReadonlyArray<RelationEntry & { junctionEntity: string }>,
218
+ source: MetaObject,
219
+ ctx: RenderContext,
220
+ fastifyVar: string,
221
+ ): Code | string {
222
+ if (entries.length === 0) return "";
223
+ const mounts = entries.map((e) => renderM2mMount(e, source, ctx, fastifyVar));
224
+ return code`${joinCode(mounts, { on: "\n", trim: false })}
225
+ `;
226
+ }
227
+
228
+ /**
229
+ * Render one M:N traversal mount. The junction + target Drizzle table consts are
230
+ * imported from their sibling entity files (imp() lets ts-poet track + emit the
231
+ * import). The source/target FK columns + the target PK are resolved to PHYSICAL
232
+ * column names via resolveColumnName (the runtime two-stage join queries by
233
+ * column). mountM2mRoute appends `/:id/<relationName>` to the source $path.
234
+ */
235
+ function renderM2mMount(
236
+ entry: RelationEntry & { junctionEntity: string },
237
+ source: MetaObject,
238
+ ctx: RenderContext,
239
+ fastifyVar: string,
240
+ ): Code {
241
+ const junctionVarSym = imp(
242
+ `${variableNameFromEntity(entry.junctionEntity)}@${crossEntitySpecifier(
243
+ ctx.outputLayout,
244
+ source.package,
245
+ ctx.packageOf.get(entry.junctionEntity),
246
+ entry.junctionEntity,
247
+ ctx.extStyle,
248
+ )}`,
249
+ );
250
+ const targetVarSym = imp(
251
+ `${variableNameFromEntity(entry.targetEntity)}@${crossEntitySpecifier(
252
+ ctx.outputLayout,
253
+ source.package,
254
+ ctx.packageOf.get(entry.targetEntity),
255
+ entry.targetEntity,
256
+ ctx.extStyle,
257
+ )}`,
258
+ );
259
+ const mountM2mRouteSym = imp("mountM2mRoute@@metaobjectsdev/runtime-ts/drizzle-fastify");
260
+ const junction = ctx.loadedRoot.findObject(entry.junctionEntity);
261
+ const target = ctx.loadedRoot.findObject(entry.targetEntity);
262
+ const sourceColumn = junction
263
+ ? resolveJunctionColumn(junction, entry.sourceJoinField!, ctx)
264
+ : entry.sourceJoinField!;
265
+ const targetColumn = junction
266
+ ? resolveJunctionColumn(junction, entry.targetJoinField!, ctx)
267
+ : entry.targetJoinField!;
268
+ const targetPkColumn = target
269
+ ? resolveJunctionColumn(target, ctx.pkMap.get(entry.targetEntity)?.fieldName ?? "id", ctx)
270
+ : "id";
271
+
272
+ return code` ${mountM2mRouteSym}({
273
+ fastify: ${fastifyVar},
274
+ path: ${source.name}.$path,
275
+ relationName: ${JSON.stringify(entry.name)},
276
+ db,
277
+ junctionTable: ${junctionVarSym},
278
+ targetTable: ${targetVarSym},
279
+ sourceColumn: ${JSON.stringify(sourceColumn)},
280
+ targetColumn: ${JSON.stringify(targetColumn)},
281
+ targetPkColumn: ${JSON.stringify(targetPkColumn)},
282
+ symmetric: ${entry.symmetric ? "true" : "false"},
283
+ });`;
284
+ }
285
+
286
+ /** Resolve a field's physical column name on an entity (defaults if missing). */
287
+ function resolveJunctionColumn(entity: MetaObject, fieldName: string, ctx: RenderContext): string {
288
+ const field = entity.ownChildren().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
289
+ if (!field) return fieldName;
290
+ return resolveColumnName(field, ctx.columnNamingStrategy);
291
+ }
292
+
293
+ /**
294
+ * FR-017 Tier 2 — the routes file for a TPH discriminator base.
295
+ *
296
+ * Mounts a polymorphic list/get route set at the base path (`GET /auths`,
297
+ * `GET /auths/:id` — rows carry the discriminator by value), then a full
298
+ * per-subtype CRUD route set at `<basePath>/<discriminatorValue lowercased>`
299
+ * (`/auths/bridge`, ...). The per-subtype create body OMITS the discriminator
300
+ * (the URL names the subtype); the runtime helper injects it. The per-subtype
301
+ * route set is scoped to its discriminator value via the `discriminator` option
302
+ * (cross-subtype get/update/delete 404; update strips the discriminator).
303
+ *
304
+ * Subtype route segment defaults to the lowercased `@discriminatorValue`
305
+ * (`"Bridge"` → `bridge`) — a robust, value-derived path that matches the
306
+ * FR-017 design's `/auths/bridge` examples. Fastify resolves the static
307
+ * `/auths/bridge` ahead of the parametric `/auths/:id`, so the two coexist.
308
+ */
309
+ function renderTphRoutesFile(base: MetaObject, ctx: RenderContext): string {
310
+ const baseName = base.name;
311
+ const handlerName = routesHandlerName(baseName);
312
+ // Single source of truth for the discriminator field + subtypes + route segments.
313
+ const plan = tphPlan(base, ctx.loadedRoot)!;
314
+ const discField = plan.discriminatorField;
315
+ const tableVar = variableNameFromEntity(baseName);
316
+
317
+ const baseFileSpec = entityModuleSpecifier(
318
+ ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
319
+ );
320
+ const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, base.package, ctx.dbImport);
321
+
322
+ const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
323
+ const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
324
+ const dbSym = imp(`db@${dbImportSpec}`);
325
+ const tableSym = imp(`${tableVar}@${baseFileSpec}`);
326
+ const baseConstSym = imp(`${baseName}@${baseFileSpec}`);
327
+ const baseInsertSym = imp(`${baseName}InsertSchema@${baseFileSpec}`);
328
+ const baseUpdateSym = imp(`${baseName}UpdateSchema@${baseFileSpec}`);
329
+ const baseFilterSym = imp(`${baseName}FilterAllowlist@${baseFileSpec}`);
330
+ const baseSortSym = imp(`${baseName}SortAllowlist@${baseFileSpec}`);
331
+
332
+ const fastifyRef = ctx.apiPrefix ? "instance" : "fastify";
333
+ const dialectLit = JSON.stringify(ctx.dialect);
334
+
335
+ const polymorphic = code`
336
+ ${mountCrudRoutesSym}({
337
+ fastify: ${fastifyRef},
338
+ path: ${baseConstSym}.$path,
339
+ db: ${dbSym},
340
+ table: ${tableSym},
341
+ insertSchema: ${baseInsertSym},
342
+ updateSchema: ${baseUpdateSym},
343
+ filterAllowlist: ${baseFilterSym},
344
+ sortAllowlist: ${baseSortSym},
345
+ dialect: ${dialectLit},
346
+ expose: ["list", "get"],
347
+ });`;
348
+
349
+ const subtypeMounts: Code[] = plan.subtypes.map(({ entity: sub, value, routeSegment: segment }) => {
350
+ const subFileSpec = entityModuleSpecifier(
351
+ ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
352
+ );
353
+ const subInsertSym = imp(`${sub.name}InsertSchema@${subFileSpec}`);
354
+ // FR-017 Tier 3: each subtype carries its OWN filter/sort allowlist
355
+ // (discriminator excluded — it's pinned by this path).
356
+ const subFilterSym = imp(`${sub.name}FilterAllowlist@${subFileSpec}`);
357
+ const subSortSym = imp(`${sub.name}SortAllowlist@${subFileSpec}`);
358
+ return code`
359
+ ${mountCrudRoutesSym}({
360
+ fastify: ${fastifyRef},
361
+ path: ${baseConstSym}.$path + ${JSON.stringify("/" + segment)},
362
+ db: ${dbSym},
363
+ table: ${tableSym},
364
+ insertSchema: ${subInsertSym}.omit({ ${discField}: true }),
365
+ updateSchema: ${subInsertSym}.omit({ ${discField}: true }).partial(),
366
+ filterAllowlist: ${subFilterSym},
367
+ sortAllowlist: ${subSortSym},
368
+ dialect: ${dialectLit},
369
+ discriminator: { column: ${JSON.stringify(discField)}, value: ${JSON.stringify(value)} },
370
+ });`;
371
+ });
372
+
373
+ const mounts = joinCode([polymorphic, ...subtypeMounts], { on: "\n" });
374
+
375
+ const fn = ctx.apiPrefix
376
+ ? code`
377
+ /**
378
+ * Mount polymorphic + per-subtype REST endpoints for the ${baseName} TPH hierarchy.
379
+ *
380
+ * GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
381
+ * /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
382
+ */
383
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
384
+ await fastify.register(async (instance) => {
385
+ ${mounts}
386
+ }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
387
+ }
388
+ `
389
+ : code`
390
+ /**
391
+ * Mount polymorphic + per-subtype REST endpoints for the ${baseName} TPH hierarchy.
392
+ *
393
+ * GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
394
+ * /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
395
+ */
396
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
397
+ ${mounts}
398
+ }
399
+ `;
400
+
401
+ const header =
402
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
403
+ `// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
404
+ `// Customize via ${baseName}.extra.ts in this directory (e.g., auth, additional handlers).\n`;
405
+ return header + fn.toString();
406
+ }
@@ -0,0 +1,232 @@
1
+ // FR-017 Tier 1 — TS discriminated-union + type guards + dispatcher emission.
2
+ //
3
+ // For an entity that carries `@discriminator`, this template emits:
4
+ // 1. `export type <Base> = <Sub1> | <Sub2> | ...` — discriminated union of
5
+ // every concrete subtype declaring @discriminatorValue against this base.
6
+ // 2. `export function is<Sub>(value: <Base>): value is <Sub>` — one type
7
+ // guard per subtype, checking the discriminator field's value.
8
+ // 3. `export function parse<Base>(row: unknown): <Base>` — runtime dispatcher
9
+ // that reads the discriminator off the raw row and parses with the
10
+ // matching subtype's Zod schema.
11
+ //
12
+ // When the entity does NOT carry @discriminator, returns null. When the entity
13
+ // carries @discriminator but has no concrete subtypes yet (refactor-in-progress
14
+ // shape — covered by FR-014 fixture `tph-discriminator-string-no-subtypes`),
15
+ // returns null too: there are no subtype names to union.
16
+
17
+ import { code, joinCode, imp, type Code } from "ts-poet";
18
+ import {
19
+ type MetaObject,
20
+ type MetaField,
21
+ type MetaRoot,
22
+ OBJECT_ATTR_DISCRIMINATOR,
23
+ OBJECT_ATTR_DISCRIMINATOR_VALUE,
24
+ OBJECT_SUBTYPE_ENTITY,
25
+ } from "@metaobjectsdev/metadata";
26
+
27
+ interface SubtypeBinding {
28
+ subtype: MetaObject;
29
+ value: string;
30
+ }
31
+
32
+ /** One concrete subtype in a {@link TphPlan}. */
33
+ export interface TphSubtypePlan {
34
+ /** The concrete subtype entity. */
35
+ entity: MetaObject;
36
+ /** Its `@discriminatorValue`. */
37
+ value: string;
38
+ /** The per-subtype REST route segment (e.g. `"bridge"`). The ONE place this
39
+ * rule is derived — see {@link tphRouteSegment}. */
40
+ routeSegment: string;
41
+ }
42
+
43
+ /**
44
+ * The single source of truth for a TPH base's polymorphic shape: the
45
+ * discriminator field name, the concrete subtypes (stable name-sorted order),
46
+ * each subtype's `@discriminatorValue`, and its per-subtype route segment.
47
+ *
48
+ * Every generator in the stack (entity, queries, routes, hooks, grid, forms)
49
+ * derives its TPH behavior from this one model rather than re-walking the root
50
+ * and re-deriving the segment / write-shape independently — so the route-segment
51
+ * rule and subtype set can never drift between, say, the generated routes and
52
+ * the generated hooks that call them.
53
+ */
54
+ export interface TphPlan {
55
+ base: MetaObject;
56
+ discriminatorField: string;
57
+ subtypes: TphSubtypePlan[];
58
+ }
59
+
60
+ // Memoized per base instance — the plan is pure over the (immutable, fully
61
+ // resolved) post-load model, and a base belongs to exactly one root, so caching
62
+ // by the base node identity is safe and erases the repeated root walks.
63
+ const _tphPlanCache = new WeakMap<MetaObject, TphPlan | null>();
64
+
65
+ /** The per-subtype REST route segment for a discriminator value. The ONE place
66
+ * this rule lives: `routesFile` and the TanStack hooks both read it through the
67
+ * plan, so generated hooks can't call a URL the generated routes don't serve. */
68
+ export function tphRouteSegment(discriminatorValue: string): string {
69
+ return discriminatorValue.toLowerCase();
70
+ }
71
+
72
+ /** The {@link TphPlan} for a discriminator base, or `null` when `base` is not a
73
+ * discriminator base (no `@discriminator`, or no concrete subtypes). */
74
+ export function tphPlan(base: MetaObject, root: MetaRoot): TphPlan | null {
75
+ const cached = _tphPlanCache.get(base);
76
+ if (cached !== undefined) return cached;
77
+ const discriminatorField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
78
+ let plan: TphPlan | null = null;
79
+ if (typeof discriminatorField === "string" && discriminatorField !== "") {
80
+ const bindings = collectConcreteSubtypes(base, root);
81
+ if (bindings.length > 0) {
82
+ plan = {
83
+ base,
84
+ discriminatorField,
85
+ subtypes: bindings.map((b) => ({
86
+ entity: b.subtype,
87
+ value: b.value,
88
+ routeSegment: tphRouteSegment(b.value),
89
+ })),
90
+ };
91
+ }
92
+ }
93
+ _tphPlanCache.set(base, plan);
94
+ return plan;
95
+ }
96
+
97
+ /** True when this entity is a TPH discriminator base — it carries
98
+ * `@discriminator` AND at least one concrete subtype declares
99
+ * `@discriminatorValue` extending it. This is the predicate the generator
100
+ * stack uses to switch into single-table-inheritance emission. */
101
+ export function isTphDiscriminatorBase(obj: MetaObject, root: MetaRoot): boolean {
102
+ return tphPlan(obj, root) !== null;
103
+ }
104
+
105
+ /** The concrete subtypes bound to this discriminator base, in stable
106
+ * (name-sorted) order. Returns `[]` when `base` is not a discriminator base. */
107
+ export function tphConcreteSubtypes(base: MetaObject, root: MetaRoot): MetaObject[] {
108
+ return tphPlan(base, root)?.subtypes.map((s) => s.entity) ?? [];
109
+ }
110
+
111
+ /**
112
+ * The subtype-only fields that must be folded into the base's single TPH table.
113
+ * For each concrete subtype, every effective field NOT already on the base is
114
+ * collected (effective, so fields declared on abstract intermediate levels of a
115
+ * multi-level hierarchy are captured too). Deduplicated by field name across
116
+ * subtypes — two subtypes sharing a column name contribute one column. The
117
+ * caller emits each as a nullable column (rows of other subtypes store NULL).
118
+ */
119
+ export function collectTphSubtypeFields(base: MetaObject, root: MetaRoot): MetaField[] {
120
+ const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
121
+ if (typeof discFieldName !== "string" || discFieldName === "") return [];
122
+
123
+ const baseFieldNames = new Set(base.fields().map((f) => f.name));
124
+ const seen = new Set<string>();
125
+ const out: MetaField[] = [];
126
+ for (const { subtype } of collectConcreteSubtypes(base, root)) {
127
+ for (const f of subtype.fields()) {
128
+ if (baseFieldNames.has(f.name)) continue; // base column — already emitted
129
+ if (seen.has(f.name)) continue; // shared subtype column — emit once
130
+ seen.add(f.name);
131
+ out.push(f);
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+
137
+ /** Render the TPH union + guards + dispatcher block, or null when the entity
138
+ * is not a discriminator-bearing base with at least one concrete subtype. */
139
+ export function renderTphDiscriminatorUnion(
140
+ base: MetaObject,
141
+ root: MetaRoot,
142
+ ): Code | null {
143
+ const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
144
+ if (typeof discFieldName !== "string" || discFieldName === "") return null;
145
+
146
+ const subtypes = collectConcreteSubtypes(base, root);
147
+ if (subtypes.length === 0) return null;
148
+
149
+ const baseName = base.name;
150
+
151
+ // 1. Union type alias. Subtype names are imported lazily via ts-poet `imp()`
152
+ // so they resolve cross-module without manual import wiring.
153
+ const unionMembers: Code[] = subtypes.map((b) => {
154
+ const sub = imp(`t:${b.subtype.name}@./${b.subtype.name}.js`);
155
+ return code`${sub}`;
156
+ });
157
+ const unionType = code`export type ${baseName} = ${joinCode(unionMembers, { on: " | " })};`;
158
+
159
+ // 2. Type guards.
160
+ const guards: Code[] = subtypes.map((b) => {
161
+ const sub = imp(`t:${b.subtype.name}@./${b.subtype.name}.js`);
162
+ return code`
163
+ /** True when value is a ${b.subtype.name} (discriminated by ${discFieldName} === "${b.value}"). */
164
+ export function is${b.subtype.name}(value: ${baseName}): value is ${sub} {
165
+ return value.${discFieldName} === "${b.value}";
166
+ }`;
167
+ });
168
+
169
+ // 3. Dispatcher. The head-read uses z.object so the discriminator is read
170
+ // without committing the row to any subtype yet.
171
+ const z = imp("z@zod");
172
+ const enumLiterals = subtypes.map((b) => JSON.stringify(b.value)).join(", ");
173
+
174
+ const caseBranches: Code[] = subtypes.map((b) => {
175
+ const schema = imp(`${b.subtype.name}Schema@./${b.subtype.name}.js`);
176
+ return code` case ${JSON.stringify(b.value)}: return ${schema}.parse(row);`;
177
+ });
178
+
179
+ const dispatcher = code`
180
+ /**
181
+ * Parse a row from the ${baseName} table, dispatching by the
182
+ * \`${discFieldName}\` discriminator value to the matching subtype's
183
+ * Zod schema. Throws on unknown discriminator values.
184
+ */
185
+ export function parse${baseName}(row: unknown): ${baseName} {
186
+ const head = ${z}.object({ ${discFieldName}: ${z}.enum([${enumLiterals}]) }).parse(row);
187
+ switch (head.${discFieldName}) {
188
+ ${joinCode(caseBranches, { on: "\n" })}
189
+ }
190
+ }
191
+ `;
192
+
193
+ return code`
194
+ ${unionType}
195
+
196
+ ${joinCode(guards, { on: "\n" })}
197
+
198
+ ${dispatcher}
199
+ `;
200
+ }
201
+
202
+ /** Walk every top-level object.entity in the root and return the concrete
203
+ * subtypes whose @discriminatorValue is bound to this base via extends.
204
+ * Abstract intermediates are skipped (they don't have polymorphic instances). */
205
+ function collectConcreteSubtypes(base: MetaObject, root: MetaRoot): SubtypeBinding[] {
206
+ const bindings: SubtypeBinding[] = [];
207
+ for (const obj of root.objects()) {
208
+ if (obj.subType !== OBJECT_SUBTYPE_ENTITY) continue;
209
+ if (obj.isAbstract === true) continue;
210
+ if (obj === base) continue;
211
+
212
+ const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
213
+ if (typeof value !== "string" || value === "") continue;
214
+
215
+ // Walk this entity's extends chain looking for `base`.
216
+ let cursor = obj.superResolved;
217
+ let found = false;
218
+ while (cursor !== undefined) {
219
+ if (cursor === base) {
220
+ found = true;
221
+ break;
222
+ }
223
+ cursor = cursor.superResolved;
224
+ }
225
+ if (!found) continue;
226
+
227
+ bindings.push({ subtype: obj, value });
228
+ }
229
+ // Stable order by subtype name so emission is deterministic.
230
+ bindings.sort((a, b) => a.subtype.name.localeCompare(b.subtype.name));
231
+ return bindings;
232
+ }
@@ -10,16 +10,50 @@
10
10
 
11
11
  import { joinCode, type Code } from "ts-poet";
12
12
  import type { MetaObject } from "@metaobjectsdev/metadata";
13
+ import type { RenderContext } from "../render-context.js";
13
14
  import { renderValueObjectInterface, renderEnumTypeAliases } from "./inferred-types.js";
14
- import { renderInsertSchemaOnly } from "./zod-validators.js";
15
+ import {
16
+ renderInsertSchemaOnly,
17
+ isTphSubtype,
18
+ renderTphSubtypeReadSchema,
19
+ tphDiscriminatorPin,
20
+ } from "./zod-validators.js";
21
+ import { renderEntityConstants } from "./entity-constants.js";
22
+ import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
23
+ import { renderFilterType } from "./filter-type.js";
15
24
  import { GENERATED_HEADER } from "../constants.js";
16
25
 
17
- export function renderValueObjectFile(obj: MetaObject): string {
18
- const enumAliases = renderEnumTypeAliases(obj);
26
+ export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: RenderContext): string {
27
+ const enumAliases = renderEnumTypeAliases(obj, ctx);
28
+ // FR-017 Tier 2: a TPH subtype lands here (it inherits the base's source.rdb
29
+ // but declares none of its own). In addition to the insert schema it emits a
30
+ // full read schema `<Sub>Schema` so parse<Base>(row) can dispatch to it.
31
+ const tphSubtype = isTphSubtype(obj);
32
+ const tphReadSchema = tphSubtype ? renderTphSubtypeReadSchema(obj) : null;
33
+ // FR-017 Tier 3: a TPH subtype also emits its field-metadata constants object
34
+ // (the `<Sub>` const), so the React form generator can render per-field
35
+ // labels / rules / inputs the same way it does for ordinary entities.
36
+ const tphConstants = tphSubtype ? renderEntityConstants(obj, apiPrefix) : null;
37
+ // FR-017 Tier 3: per-subtype filter + sort allowlists, excluding the
38
+ // discriminator (it's pinned by the per-subtype route path, so a client must
39
+ // not filter on it). Included fields are the subtype's own + inherited base
40
+ // filterable fields. Drives the per-subtype REST routes' filter layer.
41
+ const discField = tphSubtype ? tphDiscriminatorPin(obj)?.fieldName : undefined;
42
+ const tphFilterAllowlist = tphSubtype ? renderFilterAllowlist(obj, discField) : null;
43
+ const tphSortAllowlist = tphSubtype ? renderSortAllowlist(obj, discField) : null;
44
+ // FR-017 Tier 3: the per-subtype CLIENT filter type, discriminator-excluded —
45
+ // kept in lockstep with the per-subtype allowlist above so a typed
46
+ // `<Sub>Filter` can't express a filter the server allowlist would 400.
47
+ const tphFilterType = tphSubtype ? renderFilterType(obj, discField) : null;
19
48
  const sections: Code[] = [
20
- renderValueObjectInterface(obj),
49
+ renderValueObjectInterface(obj, ctx),
21
50
  ...(enumAliases !== null ? [enumAliases] : []),
51
+ ...(tphReadSchema !== null ? [tphReadSchema] : []),
22
52
  renderInsertSchemaOnly(obj),
53
+ ...(tphConstants !== null ? [tphConstants] : []),
54
+ ...(tphFilterAllowlist !== null ? [tphFilterAllowlist] : []),
55
+ ...(tphSortAllowlist !== null ? [tphSortAllowlist] : []),
56
+ ...(tphFilterType !== null ? [tphFilterType] : []),
23
57
  ];
24
58
  const body = joinCode(sections, { on: "\n" }).toString();
25
59
  const header =