@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
@@ -33,6 +33,7 @@ import {
33
33
  TEMPLATE_ATTR_PAYLOAD_REF,
34
34
  TEMPLATE_ATTR_FORMAT,
35
35
  PACKAGE_SEPARATOR,
36
+ refMatchesObject,
36
37
  } from "@metaobjectsdev/metadata";
37
38
  import { fields, isArray } from "./fr010-field-mapping.js";
38
39
  import { mirrorName } from "./extract-delegate-emitter.js";
@@ -40,7 +41,7 @@ import { enumUnionAliasName } from "./inferred-types.js";
40
41
  import { enumValues } from "../enum-meta.js";
41
42
 
42
43
  function findObject(root: MetaData, name: string): MetaData | undefined {
43
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
44
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
44
45
  }
45
46
 
46
47
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
@@ -64,9 +65,9 @@ function isObjectField(field: MetaData): boolean {
64
65
 
65
66
  /**
66
67
  * The union-alias type name for a `field.enum` with effective `@values`, or undefined when the
67
- * field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the payload
68
- * emitter (`payload-codegen.ts`) types the field as — so the cast target resolves to the exact
69
- * alias exported from `payloads.ts`. `ownerName` is the owning value-object's interface name.
68
+ * field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the entity
69
+ * inferred-types emitter types the field as — so the cast target resolves to the exact alias
70
+ * exported from the owning VO's entity module. `ownerName` is the owning value-object's interface name.
70
71
  */
71
72
  function enumAlias(field: MetaData, ownerName: string): string | undefined {
72
73
  if (field.subType !== FIELD_SUBTYPE_ENUM) return undefined;
@@ -76,11 +77,12 @@ function enumAlias(field: MetaData, ownerName: string): string | undefined {
76
77
  }
77
78
 
78
79
  /**
79
- * True iff the field is required IN THE STRICT PAYLOAD TYPE. This MUST match
80
- * payload-codegen.ts's `isFieldRequired` predicate EXACTLY (boolean `true` only) the payload
81
- * interface decides `T` vs `T | null` by that predicate, and the mapper's optionality assumption
82
- * (`m.f!` vs `m.f ?? null`) has to agree with the type it is constructing. A `@required:"true"`
83
- * string field is therefore `T | null` in the payload AND optional here (no skew).
80
+ * True iff the field is required IN THE STRICT PAYLOAD TYPE. The strict payload IS the VO's own
81
+ * generated entity-module interface (`renderValueObjectInterface`), which types a required field
82
+ * `f: T` and an optional one `f?: T` (i.e. `T | undefined` NOT `T | null`). So the mapper's
83
+ * optionality assumption (`m.f!` vs `m.f ?? undefined`) has to agree with THAT interface, and an
84
+ * absent optional maps to `undefined`, never `null`. This predicate matches the interface's
85
+ * required test (boolean `true` only) so the two never skew.
84
86
  */
85
87
  function isFieldRequired(field: MetaData): boolean {
86
88
  return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
@@ -93,8 +95,10 @@ function mapperName(vo: MetaData): string {
93
95
 
94
96
  /**
95
97
  * The mapper-body initializer expression for one field, reading mirror member `m.<name>` and
96
- * mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? null`).
97
- * Nested single/array objects recurse into their toStrict<Type> mapper, guarding null when optional.
98
+ * mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? undefined`).
99
+ * The strict payload is the VO's generated entity-module interface, whose optional fields are
100
+ * `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`.
101
+ * Nested single/array objects recurse into their toStrict<Type> mapper, guarding when optional.
98
102
  */
99
103
  function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
100
104
  const name = field.name;
@@ -104,25 +108,25 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
104
108
  const target = refVo(field, root);
105
109
  if (target === undefined) {
106
110
  // Unresolved @objectRef — the payload type would be `unknown`; pass through as-is.
107
- return required ? `m.${name}!` : `m.${name} ?? null`;
111
+ return required ? `m.${name}!` : `m.${name} ?? undefined`;
108
112
  }
109
113
  const fn = mapperName(target);
110
114
  if (isArray(field)) {
111
115
  // Required array-of-objects: each element mapped; element nulls dropped at the type level
112
116
  // via the non-null assertion (extract never yields null elements for a present array).
113
117
  if (required) return `m.${name}!.map((e) => ${fn}(e!))`;
114
- return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : null`;
118
+ return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : undefined`;
115
119
  }
116
120
  // Single nested object.
117
121
  if (required) return `${fn}(m.${name}!)`;
118
- return `m.${name} ? ${fn}(m.${name}) : null`;
122
+ return `m.${name} ? ${fn}(m.${name}) : undefined`;
119
123
  }
120
124
 
121
125
  // Scalar ARRAY (e.g. `field.string` with isArray): the mirror types it `(T | null)[] | null`
122
- // but the strict payload types it `T[]` (required) / `T[] | null` (optional). A bare `m.f!`
126
+ // but the strict payload types it `T[]` (required) / `T[]?` (optional). A bare `m.f!`
123
127
  // would leave the element type `T | null`, a `tsc --strict` TS2322 error. Filter out null
124
128
  // elements so the element type narrows to non-null (consistent with the lost-element DROP policy
125
- // already used for required arrays-of-objects above).
129
+ // already used for required arrays-of-objects above). An absent optional array maps to `undefined`.
126
130
  //
127
131
  // ENUM arrays: the mirror element is a plain `string`, but the strict payload types it as the
128
132
  // closed `<Alias>[]` union. The null-filter alone narrows to `string[]`, not `<Alias>[]` — a
@@ -136,23 +140,23 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
136
140
  return alias !== undefined ? `(${filtered}) as ${alias}[]` : filtered;
137
141
  }
138
142
  const filtered = `m.${name}.filter((x): x is NonNullable<typeof x> => x != null)`;
139
- const guarded = `m.${name} == null ? null : ${filtered}`;
143
+ const guarded = `m.${name} == null ? undefined : ${filtered}`;
140
144
  return alias !== undefined
141
- ? `m.${name} == null ? null : (${filtered}) as ${alias}[]`
145
+ ? `m.${name} == null ? undefined : (${filtered}) as ${alias}[]`
142
146
  : guarded;
143
147
  }
144
148
 
145
149
  // Scalar / enum (single): the strict payload's optionality decides the shape.
146
- // Required → non-null assertion; optional → `?? null` (matches the payload's `f?: T | null`).
150
+ // Required → non-null assertion; optional → `?? undefined` (matches the entity-module `f?: T`).
147
151
  //
148
152
  // ENUM scalar: the mirror member is a plain `string`, but the strict payload types it as the
149
153
  // closed `<Alias>` union — assigning `string` into `<Alias>` is a `tsc --strict` TS2322 error.
150
154
  // So the value is CAST to `<Alias>`. Sound for the same reason as enum arrays above: the engine
151
155
  // already validated membership (or extract throws on a lost required field).
152
156
  if (alias !== undefined) {
153
- return required ? `m.${name}! as ${alias}` : `(m.${name} ?? null) as ${alias} | null`;
157
+ return required ? `m.${name}! as ${alias}` : `(m.${name} ?? undefined) as ${alias} | undefined`;
154
158
  }
155
- return required ? `m.${name}!` : `m.${name} ?? null`;
159
+ return required ? `m.${name}!` : `m.${name} ?? undefined`;
156
160
  }
157
161
 
158
162
  /**
@@ -202,24 +206,44 @@ function emitMapper(
202
206
  }
203
207
 
204
208
  /**
205
- * Collect the strict payload-interface names reachable from `vo` (for the type-only import),
206
- * PLUS every enum union-alias reachable from those VOs. Both are exported from `payloads.ts`
207
- * (the alias is hoisted above the interface there), so the extractor's `as <Alias>` casts need
208
- * the alias names imported alongside the interface names. Deduped, in discovery order.
209
+ * One payload-type import group: the strict types (VO interface + its own enum
210
+ * union-aliases) imported from a single VO entity module.
209
211
  */
210
- function reachablePayloadTypes(vo: MetaData, root: MetaData): string[] {
211
- const order: string[] = [];
212
- const seen = new Set<string>();
212
+ interface PayloadImportGroup {
213
+ /** The VO whose entity module exports these types (`./<module>.js`). */
214
+ module: string;
215
+ /** The strict type names exported by that module, in discovery order. */
216
+ types: string[];
217
+ }
218
+
219
+ /**
220
+ * Group the strict payload types reachable from `vo` BY THE ENTITY MODULE THAT EXPORTS THEM.
221
+ *
222
+ * Each value-object gets its own generated entity module (`entityFile()` emits `<VO>.ts` exporting
223
+ * `export interface <VO>`), and `renderEnumTypeAliases` hoists every `field.enum` union-alias INTO
224
+ * the OWNING VO's module (`export type <Owner><Field> = ...` co-located with the interface). So a
225
+ * VO's interface AND the aliases for its own enum fields are imported from `./<VO>.js` — NOT from a
226
+ * single `payloads.ts` (which no generator emits). Deduped, in discovery order, one group per VO.
227
+ */
228
+ function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGroup[] {
229
+ const groups: PayloadImportGroup[] = [];
230
+ const seenVo = new Set<string>();
231
+ const seenAlias = new Set<string>();
213
232
  const visit = (cur: MetaData) => {
214
- if (seen.has(cur.name)) return;
215
- seen.add(cur.name);
216
- order.push(cur.name);
233
+ if (seenVo.has(cur.name)) return;
234
+ seenVo.add(cur.name);
235
+ // The VO interface + its OWN enum aliases share the VO's entity module.
236
+ const types: string[] = [cur.name];
217
237
  for (const f of fields(cur)) {
218
238
  const alias = enumAlias(f, cur.name);
219
- if (alias !== undefined && !seen.has(alias)) {
220
- seen.add(alias);
221
- order.push(alias);
239
+ if (alias !== undefined && !seenAlias.has(alias)) {
240
+ seenAlias.add(alias);
241
+ types.push(alias);
222
242
  }
243
+ }
244
+ groups.push({ module: cur.name, types });
245
+ // Recurse into nested object refs (their interfaces live in their own modules).
246
+ for (const f of fields(cur)) {
223
247
  if (isObjectField(f)) {
224
248
  const target = refVo(f, root);
225
249
  if (target !== undefined) visit(target);
@@ -227,7 +251,7 @@ function reachablePayloadTypes(vo: MetaData, root: MetaData): string[] {
227
251
  }
228
252
  };
229
253
  visit(vo);
230
- return order;
254
+ return groups;
231
255
  }
232
256
 
233
257
  /** Collect the mirror-interface names reachable from `vo` (root mirror + nested VO mirrors). */
@@ -286,10 +310,16 @@ export function renderExtractor(root: MetaData, templateName: string): string {
286
310
  const extractName = `extract${templateName}`;
287
311
  const rootMapper = mapperName(vo);
288
312
 
289
- const payloadTypes = reachablePayloadTypes(vo, root);
313
+ const payloadGroups = reachablePayloadGroups(vo, root);
290
314
  const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror);
291
315
  const mappers = emitMappers(vo, root, rootMirror);
292
316
 
317
+ // One type-only import per VO entity module (the VO interface + its own enum
318
+ // union-aliases co-located there). NOT a single non-existent `./payloads.js`.
319
+ const payloadImports = payloadGroups
320
+ .map((g) => `import type { ${g.types.join(", ")} } from "./${g.module}.js";`)
321
+ .join("\n");
322
+
293
323
  const lostMsg =
294
324
  `${extractName}: lost required field(s): `;
295
325
 
@@ -301,13 +331,13 @@ export function renderExtractor(root: MetaData, templateName: string): string {
301
331
  `// mapping the all-nullable mirror onto the strict payload. No registry / binding / factory.\n` +
302
332
  `\n` +
303
333
  `import {\n ${extractLenientWithName},\n type ${mirrorTypes.join(",\n type ")},\n} from "./${templateName}.output.js";\n` +
304
- `import type { ${payloadTypes.join(", ")} } from "./payloads.js";\n` +
334
+ `${payloadImports}\n` +
305
335
  `import type { MetaRoot } from "@metaobjectsdev/metadata";\n` +
306
336
  `import type { ExtractionResult } from "@metaobjectsdev/render";\n` +
307
337
  `\n` +
308
338
  `/**\n` +
309
339
  ` * Extract a fully-typed \`${strictType}\` from dirty \`text\` using the loaded \`root\` (which must\n` +
310
- ` * declare the "${payloadRef}" payload value-object). Runs the tolerant extract, then maps the\n` +
340
+ ` * declare the "${strictType}" payload value-object). Runs the tolerant extract, then maps the\n` +
311
341
  ` * extracted mirror onto the strict payload.\n` +
312
342
  ` *\n` +
313
343
  ` * @throws Error iff a \`@required\` field was lost (the strict opt-in gate).\n` +
@@ -8,8 +8,6 @@ import {
8
8
  FIELD_SUBTYPE_STRING,
9
9
  FIELD_SUBTYPE_INT,
10
10
  FIELD_SUBTYPE_LONG,
11
- FIELD_SUBTYPE_SHORT,
12
- FIELD_SUBTYPE_BYTE,
13
11
  FIELD_SUBTYPE_DOUBLE,
14
12
  FIELD_SUBTYPE_FLOAT,
15
13
  FIELD_SUBTYPE_DECIMAL,
@@ -54,8 +52,6 @@ function defaultViewForSubType(subType: string): string {
54
52
  return VIEW_SUBTYPE_CHECKBOX;
55
53
  case FIELD_SUBTYPE_INT:
56
54
  case FIELD_SUBTYPE_LONG:
57
- case FIELD_SUBTYPE_SHORT:
58
- case FIELD_SUBTYPE_BYTE:
59
55
  case FIELD_SUBTYPE_DOUBLE:
60
56
  case FIELD_SUBTYPE_FLOAT:
61
57
  case FIELD_SUBTYPE_DECIMAL:
@@ -92,8 +88,6 @@ export function zodTypeFor(field: MetaField): string {
92
88
  return "z.string()";
93
89
  case FIELD_SUBTYPE_INT:
94
90
  case FIELD_SUBTYPE_LONG:
95
- case FIELD_SUBTYPE_SHORT:
96
- case FIELD_SUBTYPE_BYTE:
97
91
  case FIELD_SUBTYPE_CURRENCY:
98
92
  return "z.number().int()";
99
93
  case FIELD_SUBTYPE_DOUBLE:
@@ -5,8 +5,6 @@ import {
5
5
  FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
6
6
  FIELD_SUBTYPE_BOOLEAN,
7
7
  FIELD_SUBTYPE_INT,
8
- FIELD_SUBTYPE_SHORT,
9
- FIELD_SUBTYPE_BYTE,
10
8
  FIELD_SUBTYPE_LONG,
11
9
  FIELD_SUBTYPE_DOUBLE,
12
10
  FIELD_SUBTYPE_FLOAT,
@@ -14,18 +12,19 @@ import {
14
12
  FIELD_SUBTYPE_DATE,
15
13
  FIELD_SUBTYPE_TIME,
16
14
  FIELD_SUBTYPE_TIMESTAMP,
15
+ FIELD_SUBTYPE_CURRENCY,
17
16
  opsForSubType,
18
17
  } from "@metaobjectsdev/metadata";
19
18
  import { sortableFields } from "./filter-shared.js";
20
19
 
21
20
  const NUMBER_SUBTYPES = new Set<string>([
22
21
  FIELD_SUBTYPE_INT,
23
- FIELD_SUBTYPE_SHORT,
24
- FIELD_SUBTYPE_BYTE,
25
22
  FIELD_SUBTYPE_LONG,
26
23
  FIELD_SUBTYPE_DOUBLE,
27
24
  FIELD_SUBTYPE_FLOAT,
28
25
  FIELD_SUBTYPE_DECIMAL,
26
+ // currency is integer minor units — coerces as a number on the wire.
27
+ FIELD_SUBTYPE_CURRENCY,
29
28
  ]);
30
29
 
31
30
  const DATETIME_SUBTYPES = new Set<string>([
@@ -42,13 +41,20 @@ function filterSubTypeFor(fieldSubType: string): "string" | "number" | "boolean"
42
41
  return "string";
43
42
  }
44
43
 
45
- function filterableFields(entity: MetaObject): MetaField[] {
44
+ function filterableFields(entity: MetaObject, exclude?: string): MetaField[] {
46
45
  // fields() returns effective fields, so inherited fields (from extends:/super:) are included in allowlists.
47
- return entity.fields().filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true);
46
+ return entity
47
+ .fields()
48
+ .filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true && c.name !== exclude);
48
49
  }
49
50
 
50
- export function renderFilterAllowlist(entity: MetaObject): Code {
51
- const fields = filterableFields(entity);
51
+ /**
52
+ * `exclude` (FR-017): drop a field from the allowlist. Used by per-subtype TPH
53
+ * allowlists to omit the discriminator — it's pinned by the per-subtype route
54
+ * path, so a client must not filter on it.
55
+ */
56
+ export function renderFilterAllowlist(entity: MetaObject, exclude?: string): Code {
57
+ const fields = filterableFields(entity, exclude);
52
58
  if (fields.length === 0) {
53
59
  return code`
54
60
  import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
@@ -72,11 +78,12 @@ ${rows}
72
78
  `;
73
79
  }
74
80
 
75
- export function renderSortAllowlist(entity: MetaObject): Code {
81
+ export function renderSortAllowlist(entity: MetaObject, exclude?: string): Code {
76
82
  // Sortable = explicit @sortable === true, OR (no @sortable AND @filterable === true).
77
83
  // @sortable: false explicitly opts out.
78
84
  // Uses shared isSortableField predicate — must stay in sync with renderFilterType.
79
- const sortable = sortableFields(entity);
85
+ // `exclude` (FR-017): per-subtype TPH allowlists omit the discriminator.
86
+ const sortable = sortableFields(entity).filter((f) => f.name !== exclude);
80
87
  if (sortable.length === 0) {
81
88
  return code`
82
89
  import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
@@ -8,8 +8,6 @@ import {
8
8
  FIELD_ATTR_FILTERABLE,
9
9
  FIELD_SUBTYPE_BOOLEAN,
10
10
  FIELD_SUBTYPE_INT,
11
- FIELD_SUBTYPE_SHORT,
12
- FIELD_SUBTYPE_BYTE,
13
11
  FIELD_SUBTYPE_LONG,
14
12
  FIELD_SUBTYPE_DOUBLE,
15
13
  FIELD_SUBTYPE_FLOAT,
@@ -23,8 +21,6 @@ import { isSortableField } from "./filter-shared.js";
23
21
  // matching the entity field representation (exact decimal string, not lossy number).
24
22
  const NUMBER_VALUE_SUBTYPES = new Set<string>([
25
23
  FIELD_SUBTYPE_INT,
26
- FIELD_SUBTYPE_SHORT,
27
- FIELD_SUBTYPE_BYTE,
28
24
  FIELD_SUBTYPE_LONG,
29
25
  FIELD_SUBTYPE_DOUBLE,
30
26
  FIELD_SUBTYPE_FLOAT,
@@ -49,9 +45,15 @@ function renderFieldUnion(field: MetaField): string {
49
45
  return `${tsName} | { ${opEntries.join("; ")} }`;
50
46
  }
51
47
 
52
- export function renderFilterType(entity: MetaObject): Code {
48
+ /**
49
+ * `exclude` (FR-017): drop a field from the client filter type. Used by
50
+ * per-subtype TPH filter types to omit the discriminator (it's pinned by the
51
+ * per-subtype route path), keeping the client `<Sub>Filter` type aligned with
52
+ * the server's per-subtype allowlist.
53
+ */
54
+ export function renderFilterType(entity: MetaObject, exclude?: string): Code {
53
55
  // fields() returns effective fields, so inherited fields (from extends:/super:) are included in filter types.
54
- const allFields = entity.fields();
56
+ const allFields = entity.fields().filter((c) => c.name !== exclude);
55
57
  const filterableFieldsList = allFields.filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true);
56
58
  // Sort union uses isSortableField — same predicate as renderSortAllowlist to prevent
57
59
  // client/server mismatches (@filterable: true + @sortable: false must be excluded from both).
@@ -0,0 +1,15 @@
1
+ import type { MetaData } from "@metaobjectsdev/metadata";
2
+ import { TYPE_TEMPLATE } from "@metaobjectsdev/metadata";
3
+
4
+ /** All template nodes of `subType` anywhere in the tree (top-level OR nested in entities). */
5
+ export function findTemplates(root: MetaData, subType: string): MetaData[] {
6
+ const out: MetaData[] = [];
7
+ const visit = (node: MetaData) => {
8
+ for (const child of node.ownChildren()) {
9
+ if (child.type === TYPE_TEMPLATE && child.subType === subType) out.push(child);
10
+ visit(child);
11
+ }
12
+ };
13
+ visit(root);
14
+ return out;
15
+ }
@@ -1,7 +1,7 @@
1
1
  // server/typescript/packages/codegen-ts/src/templates/fr010-field-mapping.ts
2
2
  //
3
- // Shared field-kind mapping for the FR-010 codegen emitters (extract-schema-emitter +
4
- // output-format-spec-emitter). Maps a metadata field subtype onto the render engine's
3
+ // Shared field-kind mapping for the FR-010 codegen emitters (the output-format-spec
4
+ // emitter et al.). Maps a metadata field subtype onto the render engine's
5
5
  // FieldKind member, the idiomatic nullable TS type used by the extract mirror interface,
6
6
  // and the ExtractMap accessor that reads it from the forgiving outcome map.
7
7
  //
@@ -13,14 +13,11 @@ import {
13
13
  type MetaData,
14
14
  TYPE_FIELD,
15
15
  FIELD_SUBTYPE_STRING,
16
- FIELD_SUBTYPE_CLASS,
17
16
  FIELD_SUBTYPE_UUID,
18
17
  FIELD_SUBTYPE_DATE,
19
18
  FIELD_SUBTYPE_TIME,
20
19
  FIELD_SUBTYPE_TIMESTAMP,
21
20
  FIELD_SUBTYPE_INT,
22
- FIELD_SUBTYPE_SHORT,
23
- FIELD_SUBTYPE_BYTE,
24
21
  FIELD_SUBTYPE_LONG,
25
22
  FIELD_SUBTYPE_CURRENCY,
26
23
  FIELD_SUBTYPE_DOUBLE,
@@ -34,6 +31,7 @@ import {
34
31
  FIELD_ATTR_COERCE_DEFAULT,
35
32
  FIELD_ATTR_DEFAULT,
36
33
  FIELD_ATTR_NORMALIZE,
34
+ FIELD_ATTR_XML_TEXT,
37
35
  NORMALIZE_DEFAULT,
38
36
  type NormalizeMode,
39
37
  } from "@metaobjectsdev/metadata";
@@ -42,7 +40,6 @@ import {
42
40
  export function scalarKind(subType: string): string | null {
43
41
  switch (subType) {
44
42
  case FIELD_SUBTYPE_STRING:
45
- case FIELD_SUBTYPE_CLASS:
46
43
  case FIELD_SUBTYPE_UUID:
47
44
  case FIELD_SUBTYPE_DATE:
48
45
  case FIELD_SUBTYPE_TIME:
@@ -53,8 +50,6 @@ export function scalarKind(subType: string): string | null {
53
50
  case FIELD_SUBTYPE_DECIMAL:
54
51
  return "STRING";
55
52
  case FIELD_SUBTYPE_INT:
56
- case FIELD_SUBTYPE_SHORT:
57
- case FIELD_SUBTYPE_BYTE:
58
53
  return "INT";
59
54
  case FIELD_SUBTYPE_LONG:
60
55
  case FIELD_SUBTYPE_CURRENCY:
@@ -86,6 +81,13 @@ export function isRequired(field: MetaData): boolean {
86
81
  return typeof v === "string" && v.toLowerCase() === "true";
87
82
  }
88
83
 
84
+ /** True iff the field's @xmlText is explicitly true (the XML text-content extract marker). */
85
+ export function xmlText(field: MetaData): boolean {
86
+ const v = field.ownAttr(FIELD_ATTR_XML_TEXT);
87
+ if (v === true) return true;
88
+ return typeof v === "string" && v.toLowerCase() === "true";
89
+ }
90
+
89
91
  /** The string members of an enum field's @values attr (empty when absent). */
90
92
  export function enumValues(field: MetaData): string[] {
91
93
  const v = field.ownAttr(FIELD_ATTR_VALUES);
@@ -14,8 +14,6 @@ import {
14
14
  FIELD_SUBTYPE_OBJECT,
15
15
  FIELD_SUBTYPE_STRING,
16
16
  FIELD_SUBTYPE_INT,
17
- FIELD_SUBTYPE_SHORT,
18
- FIELD_SUBTYPE_BYTE,
19
17
  FIELD_SUBTYPE_LONG,
20
18
  FIELD_SUBTYPE_DOUBLE,
21
19
  FIELD_SUBTYPE_FLOAT,
@@ -25,23 +23,36 @@ import {
25
23
  FIELD_SUBTYPE_DATE,
26
24
  FIELD_SUBTYPE_TIME,
27
25
  FIELD_SUBTYPE_TIMESTAMP,
28
- FIELD_SUBTYPE_CLASS,
29
26
  FIELD_SUBTYPE_UUID,
30
27
  FIELD_ATTR_REQUIRED,
31
28
  FIELD_ATTR_OBJECT_REF,
32
29
  } from "@metaobjectsdev/metadata";
33
30
  import { variableNameFromEntity, toPascalCase } from "../naming.js";
31
+ import { stripPackage } from "@metaobjectsdev/metadata";
34
32
  import { enumValues } from "../enum-meta.js";
35
33
  import { renderDocsFor } from "./jsdoc.js";
34
+ import { sharedEnumForField } from "../enum-shared.js";
35
+ import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
36
+ import type { RenderContext } from "../render-context.js";
36
37
 
37
- export function renderInferredTypes(entity: MetaObject): Code {
38
+ /**
39
+ * Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
40
+ *
41
+ * `tphBase` (FR-017): when this entity is a TPH discriminator base, the
42
+ * discriminated-union type (emitted by the tph-discriminator template) owns the
43
+ * bare `<Base>` name, so the raw single-table row type is emitted as `<Base>Row`
44
+ * to avoid a duplicate `export type <Base>`. Insert/Update keep their names
45
+ * (no collision); they describe the physical TPH table row shape.
46
+ */
47
+ export function renderInferredTypes(entity: MetaObject, tphBase = false): Code {
38
48
  const varName = variableNameFromEntity(entity.name);
39
49
  const selectSym = imp("InferSelectModel@drizzle-orm");
40
50
  const insertSym = imp("InferInsertModel@drizzle-orm");
41
51
  const docs = renderDocsFor(entity);
42
52
  const docsPrefix = docs ? `${docs}\n` : "";
53
+ const rowName = tphBase ? `${entity.name}Row` : entity.name;
43
54
  return code`
44
- ${docsPrefix}export type ${entity.name} = ${selectSym}<typeof ${varName}>;
55
+ ${docsPrefix}export type ${rowName} = ${selectSym}<typeof ${varName}>;
45
56
  export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
46
57
  export type ${entity.name}Update = Partial<${entity.name}Insert>;
47
58
  `;
@@ -71,12 +82,23 @@ export function enumUnionString(values: string[]): string {
71
82
  }
72
83
 
73
84
  /**
74
- * Emit one `export type <Name> = "A" | "B";` line per field.enum field on the entity.
75
- * - If the field extends an abstract field.enum (super), use the super field's PascalCase name.
76
- * - Otherwise use `<Entity><FieldPascal>` for inline enums.
77
- * Returns null if the entity has no enum fields.
85
+ * Emit the enum type-alias section for an entity file. Three cases per field:
86
+ *
87
+ * inline enum (members declared directly on the field; no root-abstract super)
88
+ * `export type <Entity><Field> = "A" | "B";` UNCHANGED (byte-identical).
89
+ * • shared materialized enum (extends a NON-@provided root-level abstract
90
+ * field.enum) → re-export the materialized type from the shared `./enums`
91
+ * module (`export { type E } from "./enums"`) instead of redeclaring it. The
92
+ * type is materialized ONCE in enums.ts (FR-019).
93
+ * • provided enum (extends a @provided root-level abstract field.enum) →
94
+ * re-export the type from the configured external module
95
+ * (`export { type E } from "<providedEnumModule>"`); metaobjects emits no
96
+ * declaration for it. A missing config is a codegen-time error.
97
+ *
98
+ * `ctx` is required to compute the shared/provided import specifiers. Returns
99
+ * null when the entity has no enum-alias lines to emit.
78
100
  */
79
- export function renderEnumTypeAliases(entity: MetaObject): Code | null {
101
+ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext): Code | null {
80
102
  // De-duplicate by type-alias name — multiple fields can extend the same abstract enum.
81
103
  const seen = new Set<string>();
82
104
  const lines: string[] = [];
@@ -91,7 +113,21 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
91
113
  if (seen.has(typeName)) continue;
92
114
  seen.add(typeName);
93
115
 
94
- lines.push(`export type ${typeName} = ${enumUnionString(values)};`);
116
+ // Without a RenderContext (bare unit-test calls) the shared/provided import
117
+ // specifiers can't be computed — fall back to inline emission. Real runs
118
+ // always pass ctx (entity-file template), so shared materialization applies.
119
+ const shared = ctx !== undefined ? sharedEnumForField(field) : undefined;
120
+ if (shared === undefined) {
121
+ // Inline enum — emit the literal union exactly as before.
122
+ lines.push(`export type ${typeName} = ${enumUnionString(values)};`);
123
+ continue;
124
+ }
125
+ // Shared / provided enum — re-export from the materialized module or the
126
+ // configured external module; never redeclare the union here.
127
+ const spec = shared.provided
128
+ ? providedEnumImportSpecifier(ctx!, shared.name)
129
+ : sharedEnumImportSpecifier(ctx!, entity.package);
130
+ lines.push(`export { type ${shared.name} } from ${JSON.stringify(spec)};`);
95
131
  }
96
132
 
97
133
  return lines.length > 0 ? code`${lines.join("\n")}` : null;
@@ -103,11 +139,8 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
103
139
 
104
140
  const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
105
141
  [FIELD_SUBTYPE_STRING]: "string",
106
- [FIELD_SUBTYPE_CLASS]: "string",
107
142
  [FIELD_SUBTYPE_UUID]: "string",
108
143
  [FIELD_SUBTYPE_INT]: "number",
109
- [FIELD_SUBTYPE_SHORT]: "number",
110
- [FIELD_SUBTYPE_BYTE]: "number",
111
144
  [FIELD_SUBTYPE_LONG]: "number",
112
145
  [FIELD_SUBTYPE_DOUBLE]: "number",
113
146
  [FIELD_SUBTYPE_FLOAT]: "number",
@@ -122,19 +155,62 @@ const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
122
155
  [FIELD_SUBTYPE_TIMESTAMP]: "string",
123
156
  };
124
157
 
158
+ /**
159
+ * The PLAIN-STRING TS type expression for a field — the SINGLE source of truth
160
+ * for "what TS type does the codegen give this field". `valueObjectFieldType`
161
+ * (which returns a `Code` so cross-module `field.object` refs hoist via
162
+ * `imp(...)`) makes the SAME per-branch decisions; this string form exists for
163
+ * consumers (the api-docs field-shape builder) that need the type name as text,
164
+ * not a hoisting `Code`. The branch logic MUST stay in lock-step with
165
+ * `valueObjectFieldType` below.
166
+ *
167
+ * • field.object → the referenced object's bare (package-stripped) name, `[]`
168
+ * when an array; `unknown` / `unknown[]` when the @objectRef is missing.
169
+ * • field.enum → the same enum-union alias `enumUnionAliasName` emits
170
+ * (`<Owner><Field>` or the abstract super's PascalCase), `string` fallback.
171
+ * • scalar → SCALAR_TS_BY_SUBTYPE (else `unknown`), `[]` when an array.
172
+ */
173
+ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
174
+ if (field.subType === FIELD_SUBTYPE_OBJECT) {
175
+ const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
176
+ if (typeof ref === "string" && ref.length > 0) {
177
+ const base = stripPackage(ref);
178
+ return field.isArray ? `${base}[]` : base;
179
+ }
180
+ return field.isArray ? "unknown[]" : "unknown";
181
+ }
182
+ if (field.subType === FIELD_SUBTYPE_ENUM) {
183
+ const values = enumValues(field);
184
+ if (values !== undefined) {
185
+ // The emitted TS type is an enum-union ALIAS (`<Owner><Field>`), but its
186
+ // definition IS this literal union — inline it so the documented shape is
187
+ // self-contained (an agent sees the exact allowed values, not an opaque
188
+ // alias name). Array enums wrap the parenthesized union: `(A | B)[]`.
189
+ const union = enumUnionString(values);
190
+ return field.isArray ? `(${union})[]` : union;
191
+ }
192
+ return field.isArray ? "string[]" : "string";
193
+ }
194
+ const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
195
+ return field.isArray ? `${scalar}[]` : scalar;
196
+ }
197
+
125
198
  /**
126
199
  * One-line TS type expression for a field on a value-only object.
127
200
  * Returns a `Code` so cross-module `field.object` refs can be hoisted via
128
201
  * ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
129
202
  */
130
- function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
203
+ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: RenderContext): Code {
131
204
  // field.object: import the referenced TS interface from its sibling module
132
205
  // so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
133
206
  // import strategy, just for the type alias instead of the schema constant.
134
207
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
135
208
  const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
136
209
  if (typeof ref === "string" && ref.length > 0) {
137
- const refImp = imp(`${ref}@./${ref}.js`);
210
+ // @objectRef may be authored fully-qualified (acme::sales::Brief) or bare; the
211
+ // referenced interface + its sibling module are named by the BARE short name.
212
+ const base = stripPackage(ref);
213
+ const refImp = imp(`${base}@./${base}.js`);
138
214
  return field.isArray ? code`${refImp}[]` : code`${refImp}`;
139
215
  }
140
216
  return field.isArray ? code`unknown[]` : code`unknown`;
@@ -145,6 +221,20 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
145
221
  const values = enumValues(field);
146
222
  if (values !== undefined) {
147
223
  const alias = enumUnionAliasName(entity.name, field);
224
+ // FR-019: a shared/provided enum's type lives in another module (./enums or
225
+ // the provided module). Use imp() so ts-poet hoists `import { type E }` —
226
+ // the local interface can then reference E. Inline enums reference the
227
+ // locally-declared `<Entity><Field>` alias as before.
228
+ if (ctx !== undefined) {
229
+ const shared = sharedEnumForField(field);
230
+ if (shared !== undefined) {
231
+ const spec = shared.provided
232
+ ? providedEnumImportSpecifier(ctx, shared.name)
233
+ : sharedEnumImportSpecifier(ctx, entity.package);
234
+ const sym = imp(`${shared.name}@${spec}`);
235
+ return field.isArray ? code`${sym}[]` : code`${sym}`;
236
+ }
237
+ }
148
238
  return field.isArray ? code`${alias}[]` : code`${alias}`;
149
239
  }
150
240
  return field.isArray ? code`string[]` : code`string`;
@@ -162,7 +252,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
162
252
  * trip through Drizzle nullable columns, so the null-bridge is unnecessary
163
253
  * here — and forces consumers into a residual cast at the call site.
164
254
  */
165
- export function renderValueObjectInterface(entity: MetaObject): Code {
255
+ export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderContext): Code {
166
256
  const docs = renderDocsFor(entity);
167
257
  const docsPrefix = docs ? `${docs}\n` : "";
168
258
 
@@ -170,7 +260,7 @@ export function renderValueObjectInterface(entity: MetaObject): Code {
170
260
  for (const field of entity.fields()) {
171
261
  const required = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
172
262
  const optional = required ? "" : "?";
173
- const tsType = valueObjectFieldType(entity, field);
263
+ const tsType = valueObjectFieldType(entity, field, ctx);
174
264
  lines.push(code` ${field.name}${optional}: ${tsType};`);
175
265
  }
176
266