@metaobjectsdev/codegen-ts 0.9.0 → 0.11.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts +12 -6
  3. package/dist/column-mapper.d.ts.map +1 -1
  4. package/dist/column-mapper.js +68 -28
  5. package/dist/column-mapper.js.map +1 -1
  6. package/dist/constants.d.ts +8 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +16 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/docs-paths.d.ts +58 -0
  11. package/dist/docs-paths.d.ts.map +1 -0
  12. package/dist/docs-paths.js +89 -0
  13. package/dist/docs-paths.js.map +1 -0
  14. package/dist/enum-import.d.ts +14 -0
  15. package/dist/enum-import.d.ts.map +1 -0
  16. package/dist/enum-import.js +35 -0
  17. package/dist/enum-import.js.map +1 -0
  18. package/dist/enum-shared.d.ts +32 -0
  19. package/dist/enum-shared.d.ts.map +1 -0
  20. package/dist/enum-shared.js +83 -0
  21. package/dist/enum-shared.js.map +1 -0
  22. package/dist/generator-registry.d.ts +22 -0
  23. package/dist/generator-registry.d.ts.map +1 -0
  24. package/dist/generator-registry.js +161 -0
  25. package/dist/generator-registry.js.map +1 -0
  26. package/dist/generator.d.ts +6 -0
  27. package/dist/generator.d.ts.map +1 -1
  28. package/dist/generator.js.map +1 -1
  29. package/dist/generators/api-doc-render.d.ts +17 -0
  30. package/dist/generators/api-doc-render.d.ts.map +1 -0
  31. package/dist/generators/api-doc-render.js +431 -0
  32. package/dist/generators/api-doc-render.js.map +1 -0
  33. package/dist/generators/api-docs-file.d.ts +21 -0
  34. package/dist/generators/api-docs-file.d.ts.map +1 -0
  35. package/dist/generators/api-docs-file.js +112 -0
  36. package/dist/generators/api-docs-file.js.map +1 -0
  37. package/dist/generators/api-field-shape.d.ts +39 -0
  38. package/dist/generators/api-field-shape.d.ts.map +1 -0
  39. package/dist/generators/api-field-shape.js +92 -0
  40. package/dist/generators/api-field-shape.js.map +1 -0
  41. package/dist/generators/api-label.d.ts +3 -0
  42. package/dist/generators/api-label.d.ts.map +1 -0
  43. package/dist/generators/api-label.js +8 -0
  44. package/dist/generators/api-label.js.map +1 -0
  45. package/dist/generators/api-model.d.ts +122 -0
  46. package/dist/generators/api-model.d.ts.map +1 -0
  47. package/dist/generators/api-model.js +809 -0
  48. package/dist/generators/api-model.js.map +1 -0
  49. package/dist/generators/docs-data-builder.d.ts +26 -4
  50. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  51. package/dist/generators/docs-data-builder.js +439 -167
  52. package/dist/generators/docs-data-builder.js.map +1 -1
  53. package/dist/generators/docs-data.d.ts +136 -27
  54. package/dist/generators/docs-data.d.ts.map +1 -1
  55. package/dist/generators/docs-data.js +1 -1
  56. package/dist/generators/docs-data.js.map +1 -1
  57. package/dist/generators/docs-file.d.ts +19 -0
  58. package/dist/generators/docs-file.d.ts.map +1 -1
  59. package/dist/generators/docs-file.js +154 -27
  60. package/dist/generators/docs-file.js.map +1 -1
  61. package/dist/generators/entity-file.d.ts.map +1 -1
  62. package/dist/generators/entity-file.js +29 -14
  63. package/dist/generators/entity-file.js.map +1 -1
  64. package/dist/generators/extractor-file.d.ts.map +1 -1
  65. package/dist/generators/extractor-file.js +2 -1
  66. package/dist/generators/extractor-file.js.map +1 -1
  67. package/dist/generators/field-anchor.d.ts +7 -0
  68. package/dist/generators/field-anchor.d.ts.map +1 -0
  69. package/dist/generators/field-anchor.js +23 -0
  70. package/dist/generators/field-anchor.js.map +1 -0
  71. package/dist/generators/index.d.ts +8 -1
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +6 -0
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/mermaid-er.d.ts +14 -0
  76. package/dist/generators/mermaid-er.d.ts.map +1 -1
  77. package/dist/generators/mermaid-er.js +14 -0
  78. package/dist/generators/mermaid-er.js.map +1 -1
  79. package/dist/generators/output-parser-file.d.ts.map +1 -1
  80. package/dist/generators/output-parser-file.js +3 -4
  81. package/dist/generators/output-parser-file.js.map +1 -1
  82. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  83. package/dist/generators/output-prompt-file.js +2 -2
  84. package/dist/generators/output-prompt-file.js.map +1 -1
  85. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  86. package/dist/generators/prompt-render-file.js +3 -4
  87. package/dist/generators/prompt-render-file.js.map +1 -1
  88. package/dist/generators/queries-file.d.ts.map +1 -1
  89. package/dist/generators/queries-file.js +8 -3
  90. package/dist/generators/queries-file.js.map +1 -1
  91. package/dist/generators/render-helper-file.d.ts.map +1 -1
  92. package/dist/generators/render-helper-file.js +2 -2
  93. package/dist/generators/render-helper-file.js.map +1 -1
  94. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  95. package/dist/generators/routes-file-hono.js +5 -1
  96. package/dist/generators/routes-file-hono.js.map +1 -1
  97. package/dist/generators/routes-file.d.ts +3 -0
  98. package/dist/generators/routes-file.d.ts.map +1 -1
  99. package/dist/generators/routes-file.js +6 -1
  100. package/dist/generators/routes-file.js.map +1 -1
  101. package/dist/generators/template-doc-builder.d.ts +19 -0
  102. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  103. package/dist/generators/template-doc-builder.js +220 -0
  104. package/dist/generators/template-doc-builder.js.map +1 -0
  105. package/dist/generators/template-doc-data.d.ts +62 -0
  106. package/dist/generators/template-doc-data.d.ts.map +1 -0
  107. package/dist/generators/template-doc-data.js +16 -0
  108. package/dist/generators/template-doc-data.js.map +1 -0
  109. package/dist/generators/template-payload-tree.d.ts +15 -0
  110. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  111. package/dist/generators/template-payload-tree.js +61 -0
  112. package/dist/generators/template-payload-tree.js.map +1 -0
  113. package/dist/generators/template-source-annotate.d.ts +74 -0
  114. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  115. package/dist/generators/template-source-annotate.js +184 -0
  116. package/dist/generators/template-source-annotate.js.map +1 -0
  117. package/dist/generators/template-source-render.d.ts +24 -0
  118. package/dist/generators/template-source-render.d.ts.map +1 -0
  119. package/dist/generators/template-source-render.js +175 -0
  120. package/dist/generators/template-source-render.js.map +1 -0
  121. package/dist/generators/trace-helper-file.d.ts +9 -0
  122. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  123. package/dist/generators/trace-helper-file.js +196 -0
  124. package/dist/generators/trace-helper-file.js.map +1 -0
  125. package/dist/import-path.d.ts +18 -0
  126. package/dist/import-path.d.ts.map +1 -1
  127. package/dist/import-path.js +21 -0
  128. package/dist/import-path.js.map +1 -1
  129. package/dist/index.d.ts +29 -4
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +28 -2
  132. package/dist/index.js.map +1 -1
  133. package/dist/metaobjects-config.d.ts +101 -2
  134. package/dist/metaobjects-config.d.ts.map +1 -1
  135. package/dist/metaobjects-config.js +46 -0
  136. package/dist/metaobjects-config.js.map +1 -1
  137. package/dist/naming.d.ts +39 -2
  138. package/dist/naming.d.ts.map +1 -1
  139. package/dist/naming.js +52 -3
  140. package/dist/naming.js.map +1 -1
  141. package/dist/payload-codegen.d.ts.map +1 -1
  142. package/dist/payload-codegen.js +14 -6
  143. package/dist/payload-codegen.js.map +1 -1
  144. package/dist/pk-resolver.d.ts.map +1 -1
  145. package/dist/pk-resolver.js +4 -2
  146. package/dist/pk-resolver.js.map +1 -1
  147. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  148. package/dist/projection/extract-view-spec.js +52 -26
  149. package/dist/projection/extract-view-spec.js.map +1 -1
  150. package/dist/relation-resolver.d.ts +16 -0
  151. package/dist/relation-resolver.d.ts.map +1 -1
  152. package/dist/relation-resolver.js +82 -1
  153. package/dist/relation-resolver.js.map +1 -1
  154. package/dist/render-context.d.ts +25 -2
  155. package/dist/render-context.d.ts.map +1 -1
  156. package/dist/render-context.js +7 -0
  157. package/dist/render-context.js.map +1 -1
  158. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  159. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  160. package/dist/render-engine/embedded-templates.generated.js +15 -0
  161. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  162. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  163. package/dist/render-engine/framework-provider.js +26 -13
  164. package/dist/render-engine/framework-provider.js.map +1 -1
  165. package/dist/runner.d.ts.map +1 -1
  166. package/dist/runner.js +20 -0
  167. package/dist/runner.js.map +1 -1
  168. package/dist/templates/docs-file.d.ts +2 -6
  169. package/dist/templates/docs-file.d.ts.map +1 -1
  170. package/dist/templates/docs-file.js +2 -5
  171. package/dist/templates/docs-file.js.map +1 -1
  172. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  173. package/dist/templates/drizzle-schema.js +72 -23
  174. package/dist/templates/drizzle-schema.js.map +1 -1
  175. package/dist/templates/entity-constants.d.ts +7 -0
  176. package/dist/templates/entity-constants.d.ts.map +1 -1
  177. package/dist/templates/entity-constants.js +3 -3
  178. package/dist/templates/entity-constants.js.map +1 -1
  179. package/dist/templates/entity-file.d.ts.map +1 -1
  180. package/dist/templates/entity-file.js +16 -5
  181. package/dist/templates/entity-file.js.map +1 -1
  182. package/dist/templates/enums-file.d.ts +11 -0
  183. package/dist/templates/enums-file.d.ts.map +1 -0
  184. package/dist/templates/enums-file.js +44 -0
  185. package/dist/templates/enums-file.js.map +1 -0
  186. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  187. package/dist/templates/extract-delegate-emitter.js +6 -8
  188. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  189. package/dist/templates/extractor.d.ts.map +1 -1
  190. package/dist/templates/extractor.js +58 -41
  191. package/dist/templates/extractor.js.map +1 -1
  192. package/dist/templates/field-meta.d.ts.map +1 -1
  193. package/dist/templates/field-meta.js +2 -6
  194. package/dist/templates/field-meta.js.map +1 -1
  195. package/dist/templates/filter-allowlist.d.ts +7 -2
  196. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  197. package/dist/templates/filter-allowlist.js +18 -10
  198. package/dist/templates/filter-allowlist.js.map +1 -1
  199. package/dist/templates/filter-shared.js +2 -2
  200. package/dist/templates/filter-shared.js.map +1 -1
  201. package/dist/templates/filter-type.d.ts +7 -1
  202. package/dist/templates/filter-type.d.ts.map +1 -1
  203. package/dist/templates/filter-type.js +10 -6
  204. package/dist/templates/filter-type.js.map +1 -1
  205. package/dist/templates/find-templates.d.ts +4 -0
  206. package/dist/templates/find-templates.d.ts.map +1 -0
  207. package/dist/templates/find-templates.js +15 -0
  208. package/dist/templates/find-templates.js.map +1 -0
  209. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  210. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  211. package/dist/templates/fr010-field-mapping.js +15 -11
  212. package/dist/templates/fr010-field-mapping.js.map +1 -1
  213. package/dist/templates/inferred-types.d.ts +44 -7
  214. package/dist/templates/inferred-types.d.ts.map +1 -1
  215. package/dist/templates/inferred-types.js +121 -19
  216. package/dist/templates/inferred-types.js.map +1 -1
  217. package/dist/templates/mermaid-er.d.ts +35 -2
  218. package/dist/templates/mermaid-er.d.ts.map +1 -1
  219. package/dist/templates/mermaid-er.js +174 -7
  220. package/dist/templates/mermaid-er.js.map +1 -1
  221. package/dist/templates/output-format-spec-emitter.js +1 -1
  222. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  223. package/dist/templates/output-parser.d.ts.map +1 -1
  224. package/dist/templates/output-parser.js +31 -80
  225. package/dist/templates/output-parser.js.map +1 -1
  226. package/dist/templates/output-prompt.d.ts.map +1 -1
  227. package/dist/templates/output-prompt.js +2 -2
  228. package/dist/templates/output-prompt.js.map +1 -1
  229. package/dist/templates/queries-file.d.ts.map +1 -1
  230. package/dist/templates/queries-file.js +113 -5
  231. package/dist/templates/queries-file.js.map +1 -1
  232. package/dist/templates/queries.d.ts +7 -2
  233. package/dist/templates/queries.d.ts.map +1 -1
  234. package/dist/templates/queries.js +15 -15
  235. package/dist/templates/queries.js.map +1 -1
  236. package/dist/templates/relations-block.d.ts.map +1 -1
  237. package/dist/templates/relations-block.js +12 -3
  238. package/dist/templates/relations-block.js.map +1 -1
  239. package/dist/templates/render-helper.d.ts.map +1 -1
  240. package/dist/templates/render-helper.js +5 -5
  241. package/dist/templates/render-helper.js.map +1 -1
  242. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  243. package/dist/templates/routes-file-hono.js +1 -2
  244. package/dist/templates/routes-file-hono.js.map +1 -1
  245. package/dist/templates/routes-file.d.ts.map +1 -1
  246. package/dist/templates/routes-file.js +184 -7
  247. package/dist/templates/routes-file.js.map +1 -1
  248. package/dist/templates/tph-discriminator.d.ts +56 -0
  249. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  250. package/dist/templates/tph-discriminator.js +180 -0
  251. package/dist/templates/tph-discriminator.js.map +1 -0
  252. package/dist/templates/value-object-file.d.ts +2 -1
  253. package/dist/templates/value-object-file.d.ts.map +1 -1
  254. package/dist/templates/value-object-file.js +33 -5
  255. package/dist/templates/value-object-file.js.map +1 -1
  256. package/dist/templates/zod-validators.d.ts +65 -2
  257. package/dist/templates/zod-validators.d.ts.map +1 -1
  258. package/dist/templates/zod-validators.js +202 -22
  259. package/dist/templates/zod-validators.js.map +1 -1
  260. package/package.json +103 -34
  261. package/src/column-mapper.ts +79 -32
  262. package/src/constants.ts +18 -0
  263. package/src/docs-paths.ts +128 -0
  264. package/src/enum-import.ts +43 -0
  265. package/src/enum-shared.ts +95 -0
  266. package/src/generator-registry.ts +204 -0
  267. package/src/generator.ts +6 -0
  268. package/src/generators/api-doc-render.ts +572 -0
  269. package/src/generators/api-docs-file.ts +146 -0
  270. package/src/generators/api-field-shape.ts +114 -0
  271. package/src/generators/api-label.ts +7 -0
  272. package/src/generators/api-model.ts +1067 -0
  273. package/src/generators/docs-data-builder.ts +483 -189
  274. package/src/generators/docs-data.ts +139 -28
  275. package/src/generators/docs-file.ts +205 -39
  276. package/src/generators/entity-file.ts +31 -15
  277. package/src/generators/extractor-file.ts +2 -1
  278. package/src/generators/field-anchor.ts +24 -0
  279. package/src/generators/index.ts +8 -1
  280. package/src/generators/mermaid-er.ts +14 -0
  281. package/src/generators/output-parser-file.ts +3 -4
  282. package/src/generators/output-prompt-file.ts +2 -1
  283. package/src/generators/prompt-render-file.ts +3 -4
  284. package/src/generators/queries-file.ts +9 -3
  285. package/src/generators/render-helper-file.ts +2 -1
  286. package/src/generators/routes-file-hono.ts +5 -1
  287. package/src/generators/routes-file.ts +7 -1
  288. package/src/generators/template-doc-builder.ts +306 -0
  289. package/src/generators/template-doc-data.ts +85 -0
  290. package/src/generators/template-payload-tree.ts +71 -0
  291. package/src/generators/template-source-annotate.ts +290 -0
  292. package/src/generators/template-source-render.ts +203 -0
  293. package/src/generators/trace-helper-file.ts +301 -0
  294. package/src/import-path.ts +28 -0
  295. package/src/index.ts +55 -4
  296. package/src/metaobjects-config.ts +146 -2
  297. package/src/naming.ts +73 -3
  298. package/src/payload-codegen.ts +16 -5
  299. package/src/pk-resolver.ts +4 -2
  300. package/src/projection/extract-view-spec.ts +50 -31
  301. package/src/relation-resolver.ts +103 -1
  302. package/src/render-context.ts +32 -2
  303. package/src/render-engine/embedded-templates.generated.ts +14 -0
  304. package/src/render-engine/framework-provider.ts +25 -11
  305. package/src/runner.ts +24 -0
  306. package/src/templates/docs-file.ts +2 -9
  307. package/src/templates/drizzle-schema.ts +80 -28
  308. package/src/templates/entity-constants.ts +3 -3
  309. package/src/templates/entity-file.ts +16 -5
  310. package/src/templates/enums-file.ts +50 -0
  311. package/src/templates/extract-delegate-emitter.ts +6 -7
  312. package/src/templates/extractor.ts +70 -40
  313. package/src/templates/field-meta.ts +1 -7
  314. package/src/templates/filter-allowlist.ts +18 -11
  315. package/src/templates/filter-shared.ts +2 -2
  316. package/src/templates/filter-type.ts +9 -7
  317. package/src/templates/find-templates.ts +15 -0
  318. package/src/templates/fr010-field-mapping.ts +15 -13
  319. package/src/templates/inferred-types.ts +122 -21
  320. package/src/templates/mermaid-er.ts +176 -8
  321. package/src/templates/output-format-spec-emitter.ts +1 -1
  322. package/src/templates/output-parser.ts +31 -80
  323. package/src/templates/output-prompt.ts +2 -1
  324. package/src/templates/queries-file.ts +133 -4
  325. package/src/templates/queries.ts +21 -15
  326. package/src/templates/relations-block.ts +19 -3
  327. package/src/templates/render-helper.ts +5 -4
  328. package/src/templates/routes-file-hono.ts +1 -2
  329. package/src/templates/routes-file.ts +234 -7
  330. package/src/templates/tph-discriminator.ts +232 -0
  331. package/src/templates/value-object-file.ts +39 -5
  332. package/src/templates/zod-validators.ts +225 -21
  333. package/templates/api/agent-api.md.mustache +30 -0
  334. package/templates/api/entity-api.md.mustache +69 -0
  335. package/templates/api/index.md.mustache +21 -0
  336. package/templates/docs/entity-page.md.mustache +33 -21
  337. package/templates/docs/template-page.md.mustache +56 -0
  338. package/dist/templates/extract-schema-emitter.d.ts +0 -8
  339. package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
  340. package/dist/templates/extract-schema-emitter.js +0 -81
  341. package/dist/templates/extract-schema-emitter.js.map +0 -1
  342. package/src/templates/extract-schema-emitter.ts +0 -111
@@ -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, ctx) : 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] : []),
22
- renderInsertSchemaOnly(obj),
51
+ ...(tphReadSchema !== null ? [tphReadSchema] : []),
52
+ renderInsertSchemaOnly(obj, ctx),
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 =
@@ -10,7 +10,7 @@
10
10
  // downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
11
11
 
12
12
  import { code, joinCode, imp, type Code } from "ts-poet";
13
- import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
13
+ import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
14
14
  import {
15
15
  FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
16
16
  FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
@@ -24,18 +24,92 @@ import {
24
24
  AUTO_SET_ON_CREATE, AUTO_SET_ON_UPDATE,
25
25
  VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_PATTERN,
26
26
  GENERATION_INCREMENT, GENERATION_UUID,
27
+ OBJECT_ATTR_DISCRIMINATOR, OBJECT_ATTR_DISCRIMINATOR_VALUE,
27
28
  } from "@metaobjectsdev/metadata";
28
29
  import { enumValues, zodEnumExpr } from "../enum-meta.js";
29
30
  import { renderDocsFor } from "./jsdoc.js";
31
+ import { sharedEnumForField } from "../enum-shared.js";
32
+ import { sharedEnumImportSpecifier } from "../enum-import.js";
33
+ import { sharedEnumZodConstName } from "./enums-file.js";
34
+ import type { RenderContext } from "../render-context.js";
35
+ import { valueObjectModuleSpecifier } from "../import-path.js";
36
+
37
+ /**
38
+ * FR-017 Tier 1 — when this object is a TPH subtype (@discriminatorValue set
39
+ * and an ancestor carries @discriminator), return the discriminator-field-name
40
+ * → pinned-literal-value pair. Subtypes emit `<field>: z.literal("<value>")`
41
+ * instead of the inherited field's normal type expression. Returns undefined
42
+ * when the object is not a TPH subtype.
43
+ */
44
+ export function tphDiscriminatorPin(obj: MetaObject): { fieldName: string; value: string } | undefined {
45
+ const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
46
+ if (typeof value !== "string" || value === "") return undefined;
47
+
48
+ // Walk the extends chain to find the root carrying @discriminator.
49
+ let cursor = obj.superResolved;
50
+ while (cursor !== undefined) {
51
+ const fieldName = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
52
+ if (typeof fieldName === "string" && fieldName !== "") {
53
+ return { fieldName, value };
54
+ }
55
+ cursor = cursor.superResolved;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /** True when this object is a TPH subtype — it declares @discriminatorValue
61
+ * and an ancestor carries @discriminator. */
62
+ export function isTphSubtype(obj: MetaObject): boolean {
63
+ return tphDiscriminatorPin(obj) !== undefined;
64
+ }
65
+
66
+ /**
67
+ * FR-017 Tier 2 — the per-subtype FULL read schema `<Sub>Schema`. Unlike the
68
+ * insert schema, this includes every effective field (PK included) so a raw DB
69
+ * row parses through it. The discriminator field is pinned to its literal value
70
+ * (`type: z.literal("Bridge")`) so the schema rejects a row of another subtype.
71
+ *
72
+ * This is the schema parse<Base>(row) dispatches to (see tph-discriminator.ts).
73
+ * Non-required columns are `.nullable()`-tolerant: a nullable TPH column read
74
+ * back from the DB arrives as `null`, not `undefined`, so the read schema must
75
+ * accept null (the insert schema, by contrast, makes them `.optional()`).
76
+ */
77
+ export function renderTphSubtypeReadSchema(obj: MetaObject, ctx?: RenderContext): Code {
78
+ const z = imp("z@zod");
79
+ const tphPin = tphDiscriminatorPin(obj);
80
+
81
+ const fieldLines: Code[] = [];
82
+ for (const child of obj.fields()) {
83
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
84
+ fieldLines.push(code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`);
85
+ continue;
86
+ }
87
+ const expr = zodFieldExpr(child, obj, ctx);
88
+ // zodFieldExpr already appends `.optional()` for non-required fields; add
89
+ // `.nullable()` on top so a NULL column value (the TPH default for any
90
+ // subtype-only column) parses cleanly.
91
+ fieldLines.push(
92
+ fieldWillBeOptional(child) ? code` ${child.name}: ${expr}.nullable()` : code` ${child.name}: ${expr}`,
93
+ );
94
+ }
95
+
96
+ const docs = renderDocsFor(obj);
97
+ const docsPrefix = docs ? `${docs}\n` : "";
98
+ return code`
99
+ ${docsPrefix}export const ${obj.name}Schema = ${z}.object({
100
+ ${joinCode(fieldLines, { on: ",\n" })}
101
+ });
102
+ `;
103
+ }
30
104
 
31
105
  /** Auto-generated PK field names that should be omitted from InsertSchema. */
32
106
  function autoGenPkFieldNames(obj: MetaObject): Set<string> {
33
107
  const out = new Set<string>();
34
108
  const primary = obj.primaryIdentity();
35
109
  if (primary) {
36
- const generation = primary.ownAttr(IDENTITY_ATTR_GENERATION);
110
+ const generation = primary.attr(IDENTITY_ATTR_GENERATION);
37
111
  if (generation === GENERATION_INCREMENT || generation === GENERATION_UUID) {
38
- const fields = primary.ownAttr(IDENTITY_ATTR_FIELDS);
112
+ const fields = primary.attr(IDENTITY_ATTR_FIELDS);
39
113
  const fieldsList = Array.isArray(fields) ? fields : (typeof fields === "string" ? [fields] : []);
40
114
  for (const f of fieldsList) out.add(String(f));
41
115
  }
@@ -52,9 +126,10 @@ function autoGenPkFieldNames(obj: MetaObject): Set<string> {
52
126
  * so consumer imports don't churn. A future polish PR could add a `<Name>Schema`
53
127
  * alias for clarity.
54
128
  */
55
- export function renderInsertSchemaOnly(obj: MetaObject): Code {
129
+ export function renderInsertSchemaOnly(obj: MetaObject, ctx?: RenderContext): Code {
56
130
  const z = imp("z@zod");
57
131
  const autoGenPkFields = autoGenPkFieldNames(obj);
132
+ const tphPin = tphDiscriminatorPin(obj);
58
133
 
59
134
  const insertFieldLines: Code[] = [];
60
135
  for (const child of obj.fields()) {
@@ -62,16 +137,24 @@ export function renderInsertSchemaOnly(obj: MetaObject): Code {
62
137
  // FR-013: @readOnly fields are populated by DB / replication / external
63
138
  // owner; the application has no path to write them. Exclude from the
64
139
  // create-shape schema entirely.
65
- if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
140
+ if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
66
141
 
67
- const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
142
+ // FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
143
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
144
+ insertFieldLines.push(
145
+ code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
146
+ );
147
+ continue;
148
+ }
149
+
150
+ const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
68
151
 
69
152
  if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
70
153
  insertFieldLines.push(
71
154
  code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
72
155
  );
73
156
  } else {
74
- insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child)}`);
157
+ insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
75
158
  }
76
159
  }
77
160
 
@@ -86,9 +169,90 @@ ${joinCode(insertFieldLines, { on: ",\n" })}
86
169
  `;
87
170
  }
88
171
 
89
- export function renderZodValidators(obj: MetaObject): Code {
172
+ /** One documented field in an Insert/Update schema's accepted shape. */
173
+ export interface SchemaFieldShape {
174
+ /** The field name (the schema property key). */
175
+ name: string;
176
+ /** Whether the property is optional in the schema (`.optional()` / omitted-OK). */
177
+ optional: boolean;
178
+ /** For the @discriminator field on a TPH subtype's InsertSchema: the pinned
179
+ * literal value (`z.literal("Bridge")`). Undefined otherwise. */
180
+ pinnedLiteral?: string;
181
+ /** True for @autoSet timestamp fields the schema fills server-side
182
+ * (`z.string().optional().transform(...)`). */
183
+ autoSet?: boolean;
184
+ }
185
+
186
+ /**
187
+ * The field SET (name + optionality) the `<Name>InsertSchema` accepts — derived
188
+ * by the SAME iteration + skip rules `renderInsertSchemaOnly` /
189
+ * `renderZodValidators` use to EMIT that schema, so a documented create-payload
190
+ * shape can never drift from the real schema:
191
+ * • auto-generated PK fields are omitted (caller doesn't provide them);
192
+ * • @readOnly fields are omitted (DB / replication owns the write path);
193
+ * • a TPH subtype's @discriminator field is a pinned `z.literal(value)`;
194
+ * • @autoSet fields are present but optional (server fills them);
195
+ * • every other field's optionality is `fieldWillBeOptional` (not required, or
196
+ * carries a @default).
197
+ */
198
+ export function insertSchemaFields(obj: MetaObject): SchemaFieldShape[] {
199
+ const autoGenPkFields = autoGenPkFieldNames(obj);
200
+ const tphPin = tphDiscriminatorPin(obj);
201
+ const out: SchemaFieldShape[] = [];
202
+ for (const child of obj.fields()) {
203
+ if (autoGenPkFields.has(child.name)) continue;
204
+ if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
205
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
206
+ out.push({ name: child.name, optional: false, pinnedLiteral: tphPin.value });
207
+ continue;
208
+ }
209
+ const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
210
+ if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
211
+ out.push({ name: child.name, optional: true, autoSet: true });
212
+ } else {
213
+ out.push({ name: child.name, optional: fieldWillBeOptional(child) });
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+
219
+ /**
220
+ * The field SET the `<Name>UpdateSchema` accepts — same iteration + skip rules
221
+ * as `insertSchemaFields`, but mirroring the UpdateSchema branch of
222
+ * `renderZodValidators`:
223
+ * • a TPH subtype's @discriminator field is OMITTED (clients can't change subtype);
224
+ * • @autoSet onCreate fields are OMITTED (creation timestamps are immutable);
225
+ * • @autoSet onUpdate fields are present + optional (server fills them);
226
+ * • every other field is optional (PATCH semantics).
227
+ */
228
+ export function updateSchemaFields(obj: MetaObject): SchemaFieldShape[] {
229
+ const autoGenPkFields = autoGenPkFieldNames(obj);
230
+ const tphPin = tphDiscriminatorPin(obj);
231
+ const out: SchemaFieldShape[] = [];
232
+ for (const child of obj.fields()) {
233
+ if (autoGenPkFields.has(child.name)) continue;
234
+ if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
235
+ // TPH subtype discriminator: omitted from the update schema entirely.
236
+ if (tphPin !== undefined && child.name === tphPin.fieldName) continue;
237
+ const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
238
+ if (autoSet === AUTO_SET_ON_CREATE) {
239
+ // Omitted: creation timestamps cannot change after creation.
240
+ continue;
241
+ }
242
+ if (autoSet === AUTO_SET_ON_UPDATE) {
243
+ out.push({ name: child.name, optional: true, autoSet: true });
244
+ continue;
245
+ }
246
+ // All non-autoSet fields are optional in the update schema (PATCH semantics).
247
+ out.push({ name: child.name, optional: true });
248
+ }
249
+ return out;
250
+ }
251
+
252
+ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code {
90
253
  const z = imp("z@zod");
91
254
  const autoGenPkFields = autoGenPkFieldNames(obj);
255
+ const tphPin = tphDiscriminatorPin(obj);
92
256
 
93
257
  const insertFieldLines: Code[] = [];
94
258
  const updateFieldLines: Code[] = [];
@@ -98,9 +262,20 @@ export function renderZodValidators(obj: MetaObject): Code {
98
262
  // The DB / trigger / replication owns the write path; the app must not
99
263
  // pass these values in POST/PATCH bodies (routesFile enforces the same
100
264
  // contract at the boundary with a 400 response).
101
- if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
265
+ if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
266
+
267
+ // FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
268
+ // The discriminator is implicit on subtype rows (controlled by URL / insert
269
+ // path) — the app never writes it via the body and never updates it.
270
+ // Insert: pinned literal. Update: omitted entirely (clients can't change subtype).
271
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
272
+ insertFieldLines.push(
273
+ code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
274
+ );
275
+ continue;
276
+ }
102
277
 
103
- const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
278
+ const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
104
279
 
105
280
  // Insert schema: @autoSet fields use transform (always override client input).
106
281
  if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
@@ -108,7 +283,7 @@ export function renderZodValidators(obj: MetaObject): Code {
108
283
  code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
109
284
  );
110
285
  } else {
111
- insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child)}`);
286
+ insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
112
287
  }
113
288
 
114
289
  // Update schema: @autoSet onCreate → omit entirely; onUpdate → transform
@@ -122,7 +297,7 @@ export function renderZodValidators(obj: MetaObject): Code {
122
297
  // All non-autoSet fields are optional in the update schema (PATCH semantics).
123
298
  // zodFieldExpr already appends .optional() when the field is non-required
124
299
  // OR has a default; only append once more when it didn't.
125
- const baseExpr = zodFieldExpr(child);
300
+ const baseExpr = zodFieldExpr(child, obj, ctx);
126
301
  updateFieldLines.push(
127
302
  fieldWillBeOptional(child) ? code` ${child.name}: ${baseExpr}` : code` ${child.name}: ${baseExpr}.optional()`,
128
303
  );
@@ -146,15 +321,24 @@ ${joinCode(updateFieldLines, { on: ",\n" })}
146
321
  `;
147
322
  }
148
323
 
149
- function zodFieldExpr(field: MetaField): Code {
324
+ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext): Code {
150
325
  // FIELD_SUBTYPE_OBJECT: emit z.array(<Ref>InsertSchema) / <Ref>InsertSchema
151
326
  // via an imp() so ts-poet hoists the cross-module import. Without this the
152
327
  // field used to collapse to z.string() / z.array(z.string()) and downstream
153
328
  // JSON Schema (e.g. LLM tool_use input_schema) lost the nested object shape.
154
329
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
155
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
330
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
156
331
  if (typeof ref === "string" && ref.length > 0) {
157
- const refImp = imp(`${ref}InsertSchema@./${ref}.js`);
332
+ // @objectRef may be authored fully-qualified or bare — the referenced
333
+ // <Ref>InsertSchema is named by the BARE short name. The import MODULE is
334
+ // resolved via the shared layout/package/extStyle-aware helper (the SAME
335
+ // one the field's TS type + Drizzle .$type<> use) so all three agree.
336
+ // Without owner/ctx (bare unit-test calls) fall back to the flat same-dir.
337
+ const refBase = stripPackage(ref);
338
+ const moduleSpec = (ctx && owner)
339
+ ? valueObjectModuleSpecifier(refBase, ctx.packageOf, owner.package, ctx.outputLayout, ctx.extStyle)
340
+ : `./${refBase}.js`;
341
+ const refImp = imp(`${refBase}InsertSchema@${moduleSpec}`);
158
342
  let base: Code = code`${refImp}`;
159
343
  if (field.isArray) base = code`z.array(${base})`;
160
344
  return appendValidatorChain(base, field);
@@ -187,7 +371,27 @@ function zodFieldExpr(field: MetaField): Code {
187
371
  break;
188
372
  case FIELD_SUBTYPE_ENUM: {
189
373
  const values = enumValues(field);
190
- baseStr = values !== undefined ? zodEnumExpr(values) : "z.string()";
374
+ if (values === undefined) {
375
+ baseStr = "z.string()";
376
+ break;
377
+ }
378
+ // FR-019: a field extending a MATERIALIZED root-level abstract enum uses the
379
+ // shared `<E>Enum` Zod const (imported from ./enums) instead of inlining
380
+ // z.enum([...]). A @provided enum keeps inline z.enum([...]) — validation
381
+ // stays metaobjects-owned (the @values SSOT); only the TS type is external.
382
+ // Inline enums (and bare-ctx unit-test calls) keep inlining as before.
383
+ if (ctx !== undefined) {
384
+ const shared = sharedEnumForField(field);
385
+ if (shared !== undefined && !shared.provided) {
386
+ const constName = sharedEnumZodConstName(shared.name);
387
+ const spec = sharedEnumImportSpecifier(ctx, owner?.package);
388
+ const sharedConst = imp(`${constName}@${spec}`);
389
+ let base: Code = code`${sharedConst}`;
390
+ if (field.isArray) base = code`z.array(${base})`;
391
+ return appendValidatorChain(base, field);
392
+ }
393
+ }
394
+ baseStr = zodEnumExpr(values);
191
395
  break;
192
396
  }
193
397
  case FIELD_SUBTYPE_STRING:
@@ -204,11 +408,11 @@ function zodFieldExpr(field: MetaField): Code {
204
408
  /** Mirrors the optional-or-not decision inside appendValidatorChain so the update-schema
205
409
  * caller can avoid stacking a second `.optional()` onto an already-optional expression. */
206
410
  function fieldWillBeOptional(field: MetaField): boolean {
207
- let isRequired = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
411
+ let isRequired = field.attr(FIELD_ATTR_REQUIRED) === true;
208
412
  for (const child of field.validators()) {
209
413
  if (child.subType === VALIDATOR_SUBTYPE_REQUIRED) isRequired = true;
210
414
  }
211
- const hasDefault = field.ownAttr(FIELD_ATTR_DEFAULT) !== undefined;
415
+ const hasDefault = field.attr(FIELD_ATTR_DEFAULT) !== undefined;
212
416
  return !isRequired || hasDefault;
213
417
  }
214
418
 
@@ -226,8 +430,8 @@ const NUMERIC_FIELD_SUBTYPES = new Set<string>([
226
430
  * - array (any element) → .min/.max = element count (validator.array)
227
431
  */
228
432
  function appendValidatorChain(base: Code, field: MetaField): Code {
229
- let isRequired = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
230
- let maxLen: number | undefined = field.ownAttr(FIELD_ATTR_MAX_LENGTH) as number | undefined;
433
+ let isRequired = field.attr(FIELD_ATTR_REQUIRED) === true;
434
+ let maxLen: number | undefined = field.attr(FIELD_ATTR_MAX_LENGTH) as number | undefined;
231
435
  let minLen: number | undefined;
232
436
  let pattern: string | undefined;
233
437
  let numMin: number | undefined;
@@ -278,7 +482,7 @@ function appendValidatorChain(base: Code, field: MetaField): Code {
278
482
  // Fields with DB-level defaults are optional in the InsertSchema: the caller
279
483
  // can omit them and the DB will fill in. Otherwise required-with-default
280
484
  // would force callers to repeat the default at every call site.
281
- const hasDefault = field.ownAttr(FIELD_ATTR_DEFAULT) !== undefined;
485
+ const hasDefault = field.attr(FIELD_ATTR_DEFAULT) !== undefined;
282
486
  if (!isRequired || hasDefault) chain = code`${chain}.optional()`;
283
487
  return chain;
284
488
  }
@@ -0,0 +1,30 @@
1
+ {{{generatedMarker}}}
2
+
3
+ # {{title}}
4
+
5
+ Generated API reference for {{project}}; call these exactly as written. {{importNote}}
6
+ {{#hasSetup}}
7
+
8
+ ## Setup
9
+ {{#setup}}
10
+ - `{{handle}}` — {{{note}}} `{{{snippetInline}}}`
11
+ {{/setup}}
12
+ {{/hasSetup}}
13
+ {{#units}}
14
+
15
+ ## {{node}}
16
+ {{#groups}}
17
+
18
+ `{{importHeader}}`
19
+ {{#symbols}}
20
+ - `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}
21
+ {{/symbols}}
22
+ {{/groups}}
23
+ {{#example}}
24
+
25
+ Example:
26
+ ```ts
27
+ {{{example}}}
28
+ ```
29
+ {{/example}}
30
+ {{/units}}