@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
@@ -0,0 +1,809 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/api-model.ts
2
+ //
3
+ // ApiModel — an intermediate representation (IR) of the PUBLIC API surface an
4
+ // adopter's codegen produces from their metadata. The whole point is to be
5
+ // ACCURATE BY CONSTRUCTION: every symbol NAME here is derived by REUSING the
6
+ // real generators' own naming/signature logic (the same helpers the generators
7
+ // call when they emit code), never invented. The Task-4 accuracy gate runs the
8
+ // real generators and asserts each ApiModel symbol name actually appears in the
9
+ // generated output — so this builder must agree with them by construction.
10
+ //
11
+ // What it documents, per node:
12
+ // • ENTITY (object.entity, queryable):
13
+ // - model : the entity type/const (entity-file emits `<Name>`)
14
+ // - data-access : findById / list / create / update / deleteById helpers
15
+ // (templates/queries.ts — exact spellings via naming.ts)
16
+ // - rest : the 5 CRUD endpoints the routes generator mounts at the
17
+ // entity's $path (read-only set for projections)
18
+ // - validation : <Name>InsertSchema / <Name>UpdateSchema (zod-validators)
19
+ // • template.output:
20
+ // - extractor : extract<Name> / extractLenient<Name> (templates/extractor.ts)
21
+ // — ONLY when @format is json/xml (extractor generator gate)
22
+ // - render : render<Name> (templates/render-helper.ts) — document →
23
+ // string, email → EmailDocument (@kind gate)
24
+ // • ENTITY (additional, T5 — relationships / callable / Hono):
25
+ // - relation : the `<var>Relations` drizzle relations() export the
26
+ // entity file composes (relations-block.ts), one per entity
27
+ // that has relations; the per-navigation accessors (1:N
28
+ // one() / M:N many(junction)) ride in its field shape, named
29
+ // + cardinality-tagged + target-tagged. ONLY when the
30
+ // relation-resolver derives a relations() block for it.
31
+ // - callable : call<Entity> (templates/callable-file.ts) — ONLY when the
32
+ // entity is backed by a stored-proc / table-function source
33
+ // (isCallableEntity); the typed proc wrapper.
34
+ // - rest-hono : the Hono CRUD registrar register<Entity>Routes
35
+ // (templates/routes-file-hono.ts) — the OPT-IN Hono variant
36
+ // of the Fastify REST surface. Documented ONLY when the
37
+ // adopter wires routesFileHono() (ctx.includeHonoRoutes), and
38
+ // gated by the SAME @emitRoutes:false filter.
39
+ // • template.prompt (T5):
40
+ // - prompt : render<Name> (payload, provider): string — the prompt
41
+ // render handle promptRender() emits into a single
42
+ // aggregated `prompts.ts` (payload-codegen generateRenderHandle).
43
+ // ONLY for TOP-LEVEL template.prompt nodes (matching
44
+ // prompt-render-file.ts's `ctx.loadedRoot.ownChildren()`).
45
+ //
46
+ // DEFERRALS (tracked follow-ups — NOT documented by this builder yet, stated here
47
+ // so the gap is known + intentional):
48
+ // • TanStack / React generator surface — formFile / tanstackQuery / grid + hooks
49
+ // are framework ADD-ONS (a separate front-end-codegen effort); their emitted
50
+ // symbols are out of scope for the back-end public-API IR this builder models.
51
+ // • TPH BASE per-subtype write helpers (create<Sub> / update<Sub>ById /
52
+ // delete<Sub>ById scoped to the shared table) + the subtype REST subpaths —
53
+ // the prior deliberate deferral (see the TPH skip note below). Under-documented
54
+ // (allowed), never invented.
55
+ //
56
+ // SKIP rules honored (matching the real generators' filters):
57
+ // • object.value records have no primary identity → the queries generator skips
58
+ // them entirely (queries-file.ts `skipNonQueryable` = subType !== "value" &&
59
+ // !isTphSubtype), and they get no CRUD/routes/validation. So value objects
60
+ // contribute ONLY a model symbol here.
61
+ // • TPH subtypes (a @discriminatorValue under a @discriminator base) are ALSO
62
+ // skipped by the queries + routes generators (isTphSubtype, from
63
+ // templates/zod-validators.ts) — their query/route/validation surface lives
64
+ // in the discriminator BASE's polymorphic file, NOT their own. So a TPH
65
+ // subtype likewise contributes ONLY a model symbol here.
66
+ // The discriminator BASE itself stays queryable, but its data-access surface
67
+ // is REDUCED: the queries generator emits only the polymorphic reads
68
+ // find<Base>ById + list<Base>s on the base — create/update/delete are emitted
69
+ // PER CONCRETE SUBTYPE (create<Sub> …), since a base row can't be inserted
70
+ // without choosing a subtype. So the builder documents only those two reads
71
+ // (plus the base's validation schemas + base-path REST, which ARE emitted);
72
+ // documenting create<Base>/update<Base>/delete<Base>ById would be
73
+ // over-documentation (the Task-4 accuracy gate catches exactly that).
74
+ // DEFERRAL: the TPH BASE's per-subtype polymorphic write helpers (create<Sub>
75
+ // / update<Sub>ById / delete<Sub>ById scoped to the shared table) and the
76
+ // subtype REST subpaths are NOT YET documented by this builder — that fuller
77
+ // TPH modeling is a tracked follow-up (under-documentation, allowed).
78
+ // • @emitRoutes:false entities → the routes generator filters them out
79
+ // (routes-file.ts: ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false), so they get
80
+ // NO REST symbols here. The queries + validator generators do NOT honor
81
+ // @emitRoutes, so data-access + validation symbols still apply.
82
+ import { OBJECT_SUBTYPE_VALUE, TYPE_TEMPLATE, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_SUBTYPE_PROMPT, TEMPLATE_ATTR_PAYLOAD_REF, TEMPLATE_ATTR_FORMAT, TEMPLATE_ATTR_KIND, TEMPLATE_KIND_EMAIL, TEMPLATE_KIND_DEFAULT, TYPE_SOURCE, SOURCE_ATTR_PARAMETER_REF, refMatchesObject, } from "@metaobjectsdev/metadata";
83
+ import { findByIdFnName, listFnName, createFnName, updateFnName, deleteByIdFnName, routesHandlerName, variableNameFromEntity, } from "../naming.js";
84
+ import { getPkInfo } from "../templates/queries.js";
85
+ import { isTphSubtype } from "../templates/zod-validators.js";
86
+ import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
87
+ import { isCallableEntity } from "../templates/callable-file.js";
88
+ import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
89
+ import { resourcePath } from "../templates/entity-constants.js";
90
+ import { isProjection } from "../projection/projection-detector.js";
91
+ import { buildPkMap } from "../pk-resolver.js";
92
+ import { buildRelationMap } from "../relation-resolver.js";
93
+ import { effectivePackage } from "../docs-paths.js";
94
+ import { entityOutputPath } from "../import-path.js";
95
+ import { modelFieldShapes, createFieldShapes, updateFieldShapes, payloadFieldShapes, } from "./api-field-shape.js";
96
+ // ---------------------------------------------------------------------------
97
+ // Builder.
98
+ // ---------------------------------------------------------------------------
99
+ export function buildApiModel(root, ctx) {
100
+ const pkMap = ctx.pkMap ?? buildPkMap(root);
101
+ // getPkInfo wants a RenderContext; it only reads `.pkMap`, so a structural
102
+ // shim is sufficient (and avoids forcing callers to build a full context).
103
+ const pkCtx = { pkMap };
104
+ const layout = ctx.outputLayout ?? "flat";
105
+ const relationMap = ctx.relationMap ?? buildRelationMap(root);
106
+ const includeHono = ctx.includeHonoRoutes ?? false;
107
+ const units = [];
108
+ for (const obj of root.objects()) {
109
+ units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono));
110
+ }
111
+ for (const tmpl of templateOutputs(root)) {
112
+ units.push(buildTemplateUnit(tmpl, root, layout));
113
+ }
114
+ for (const tmpl of templatePrompts(root)) {
115
+ units.push(buildPromptUnit(tmpl, root));
116
+ }
117
+ return { units };
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // importPath derivation — the SINGLE place a documented symbol's import module
121
+ // is computed. It mirrors the EMITTING generator's own path logic exactly so a
122
+ // documented import can never drift from where the code actually lands:
123
+ //
124
+ // • entity / queries / routes files use
125
+ // entityOutputPath(layout, entity.package, "<Name>.<suffix>.ts")
126
+ // (queries-file.ts / entity-file.ts / routes-file.ts) — note they key off
127
+ // the entity's OWN bare `.package` (often undefined for objects, FR5d), so
128
+ // in package layout they only fold when the object actually carries a
129
+ // package. We pass the SAME `obj.package` here, not effectivePackage.
130
+ // • extractor / render-helper files emit a FLAT `<Name>.extractor.ts` /
131
+ // `<Name>.render.ts` regardless of layout (extractor-file.ts /
132
+ // render-helper-file.ts: `${t.name}.<suffix>.ts`, no package folding).
133
+ //
134
+ // The importPath is the emitted path WITHOUT the trailing `.ts`.
135
+ // ---------------------------------------------------------------------------
136
+ /** Extension-less module specifier for an entity-derived file
137
+ * (`<Name>` / `<Name>.queries` / `<Name>.routes`), folded by the SAME
138
+ * entityOutputPath logic the emitting generator uses. */
139
+ function entityModulePath(layout, obj, basename) {
140
+ return stripTs(entityOutputPath(layout, obj.package, `${basename}.ts`));
141
+ }
142
+ /** Extension-less module specifier for a template-derived file
143
+ * (`<Name>.extractor` / `<Name>.render`) — always flat (the generators do not
144
+ * fold these by package). */
145
+ function templateModulePath(basename) {
146
+ return basename;
147
+ }
148
+ function stripTs(path) {
149
+ return path.endsWith(".ts") ? path.slice(0, -3) : path;
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Entities.
153
+ // ---------------------------------------------------------------------------
154
+ /** Mirror of the queries generator's filter (queries-file.ts `skipNonQueryable`
155
+ * = `subType !== OBJECT_SUBTYPE_VALUE && !isTphSubtype`). A queryable entity is
156
+ * any non-value, non-TPH-subtype object:
157
+ * • Value objects have no primary identity → the queries/routes/validation
158
+ * generators emit no CRUD for them.
159
+ * • TPH subtypes (@discriminatorValue under a @discriminator base) emit no
160
+ * standalone queries/routes file — their surface lives in the discriminator
161
+ * BASE's polymorphic file (routes-file.ts:27 + queries-file.ts:21-22).
162
+ * Either way the object contributes only a model symbol here. (The TPH base's
163
+ * per-subtype polymorphic helpers + subpaths are a documented deferral — see
164
+ * the module header.) */
165
+ function isQueryable(obj) {
166
+ return obj.subType !== OBJECT_SUBTYPE_VALUE && !isTphSubtype(obj);
167
+ }
168
+ /** Whether the routes generator emits REST routes for this entity. It filters
169
+ * out @emitRoutes:false (routes-file.ts:27), unlike the queries + validator
170
+ * generators which always emit. So REST symbols are gated separately from the
171
+ * other queryable kinds. */
172
+ function emitsRoutes(obj) {
173
+ return obj.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
174
+ }
175
+ function buildEntityUnit(obj, ctx, root, layout, relationMap, includeHono) {
176
+ const name = obj.name;
177
+ const symbols = [];
178
+ // The entity MODEL + its zod schemas are emitted into `<Name>.ts` (entity-file
179
+ // composes drizzle-schema + inferred-types + zod-validators), so model AND
180
+ // validation share the entity module's importPath.
181
+ const entityMod = entityModulePath(layout, obj, name);
182
+ // --- model: the entity type/const the entity-file generator emits (bare name). ---
183
+ symbols.push({
184
+ name,
185
+ kind: "model",
186
+ importPath: entityMod,
187
+ signature: `interface ${name}`,
188
+ returns: name,
189
+ usage: `The typed shape of a ${name} row, generated from its metadata.`,
190
+ fields: modelFieldShapes(obj),
191
+ });
192
+ if (isQueryable(obj)) {
193
+ symbols.push(...dataAccessSymbols(obj, ctx, root, layout));
194
+ symbols.push(...validationSymbols(obj, entityMod));
195
+ // REST is additionally gated: @emitRoutes:false suppresses routes only.
196
+ if (emitsRoutes(obj)) {
197
+ symbols.push(...restSymbols(obj, layout));
198
+ // The OPT-IN Hono variant mounts the SAME CRUD verbs under the SAME
199
+ // @emitRoutes filter — documented only when the adopter wired it.
200
+ if (includeHono)
201
+ symbols.push(...restHonoSymbols(obj, layout));
202
+ }
203
+ }
204
+ // --- relation: the drizzle relations() export, when the resolver derives a
205
+ // relations() block for this entity (1:N belongs-to + inverse many, M:N
206
+ // @through). Independent of isQueryable — a relations() block is emitted by
207
+ // the entity file regardless. ---
208
+ const relationSym = relationSymbol(obj, entityMod, relationMap);
209
+ if (relationSym !== undefined)
210
+ symbols.push(relationSym);
211
+ // --- callable: call<Entity>, only when the entity is backed by a stored-proc /
212
+ // table-function source (matching the callable generator's isCallableEntity
213
+ // filter). ---
214
+ const callableSym = callableSymbol(obj, root, layout);
215
+ if (callableSym !== undefined)
216
+ symbols.push(callableSym);
217
+ const unit = {
218
+ node: name,
219
+ package: effectivePackage(obj),
220
+ nodeKind: "entity",
221
+ symbols,
222
+ };
223
+ // The worked example reads the row back by its REAL primary key (e.g.
224
+ // `created.code`), not a hard-coded `id` — reuse the same getPkInfo the
225
+ // data-access symbols were named from so the example is accurate-by-
226
+ // construction for any PK field name.
227
+ const pkName = getPkInfo(obj, ctx).fieldName;
228
+ const example = entityExample(name, pkName, symbols);
229
+ if (example !== undefined)
230
+ unit.example = example;
231
+ return unit;
232
+ }
233
+ /** The CRUD helpers templates/queries.ts emits, named via the SHARED naming
234
+ * helpers the template itself uses (so the names cannot drift). The PK field +
235
+ * TS type come from the real getPkInfo.
236
+ *
237
+ * TPH discriminator BASE divergence: when `obj` is a discriminator base, the
238
+ * queries generator does NOT emit standalone create<Base>/update<Base>/
239
+ * delete<Base>ById — a base row can't be inserted without choosing a concrete
240
+ * subtype, so write helpers are emitted PER CONCRETE SUBTYPE (create<Sub> …),
241
+ * not on the base. The base file emits only the polymorphic reads find<Base>ById
242
+ * + list<Base>s. Documenting create<Base>/update<Base>/delete<Base>ById would be
243
+ * OVER-documentation (the api-docs accuracy gate catches exactly this). The
244
+ * per-subtype write helpers themselves are a tracked deferral (module header),
245
+ * so we under-document (allowed) rather than invent names. */
246
+ function dataAccessSymbols(obj, ctx, root, layout) {
247
+ const name = obj.name;
248
+ const { fieldName: pk, tsType: pkType } = getPkInfo(obj, ctx);
249
+ // All CRUD helpers are emitted into `<Name>.queries.ts` (queries-file.ts).
250
+ const mod = entityModulePath(layout, obj, `${name}.queries`);
251
+ const find = findByIdFnName(name);
252
+ const list = listFnName(name);
253
+ const create = createFnName(name);
254
+ const update = updateFnName(name);
255
+ const del = deleteByIdFnName(name);
256
+ const reads = [
257
+ {
258
+ name: find,
259
+ kind: "data-access",
260
+ importPath: mod,
261
+ signature: `${find}(db: Db, ${pk}: ${pkType}): Promise<${name} | null>`,
262
+ params: [`db: Db`, `${pk}: ${pkType}`],
263
+ returns: `Promise<${name} | null>`,
264
+ usage: `Fetch a single ${name} by its primary key; null when not found.`,
265
+ },
266
+ {
267
+ name: list,
268
+ kind: "data-access",
269
+ importPath: mod,
270
+ signature: `${list}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${name}[]>`,
271
+ params: [`db: Db`, `opts?: { limit?: number; offset?: number }`],
272
+ returns: `Promise<${name}[]>`,
273
+ usage: `List ${name} rows with optional limit/offset paging.`,
274
+ },
275
+ ];
276
+ // A TPH discriminator base emits ONLY the polymorphic reads — the write
277
+ // helpers are per concrete subtype (create<Sub> …), not on the base.
278
+ if (isTphDiscriminatorBase(obj, root)) {
279
+ return reads;
280
+ }
281
+ // Create/update payload shapes — the EXACT InsertSchema/UpdateSchema field sets
282
+ // (api-field-shape reuses the zod emitter's own walk), so `data: unknown`'s
283
+ // real shape is documented + gate-verified against the emitted schema.
284
+ const createShape = createFieldShapes(obj);
285
+ const updateShape = updateFieldShapes(obj);
286
+ return [
287
+ ...reads,
288
+ {
289
+ name: create,
290
+ kind: "data-access",
291
+ importPath: mod,
292
+ signature: `${create}(db: Db, data: unknown): Promise<${name}>`,
293
+ params: [`db: Db`, `data: unknown`],
294
+ returns: `Promise<${name}>`,
295
+ throws: `ZodError when data fails ${name}InsertSchema validation.`,
296
+ usage: `Validate (via ${name}InsertSchema) and insert a new ${name}.`,
297
+ fields: createShape,
298
+ },
299
+ {
300
+ name: update,
301
+ kind: "data-access",
302
+ importPath: mod,
303
+ signature: `${update}(db: Db, ${pk}: ${pkType}, data: unknown): Promise<${name} | null>`,
304
+ params: [`db: Db`, `${pk}: ${pkType}`, `data: unknown`],
305
+ returns: `Promise<${name} | null>`,
306
+ throws: `ZodError when data fails the partial ${name}InsertSchema validation.`,
307
+ usage: `Partially update an existing ${name} by primary key; null when not found.`,
308
+ fields: updateShape,
309
+ },
310
+ {
311
+ name: del,
312
+ kind: "data-access",
313
+ importPath: mod,
314
+ signature: `${del}(db: Db, ${pk}: ${pkType}): Promise<boolean>`,
315
+ params: [`db: Db`, `${pk}: ${pkType}`],
316
+ returns: `Promise<boolean>`,
317
+ usage: `Delete a ${name} by primary key; true when a row was removed.`,
318
+ },
319
+ ];
320
+ }
321
+ /** The two zod schemas the validator generator emits per entity. The route +
322
+ * queries generators import these exact names (<Name>InsertSchema /
323
+ * <Name>UpdateSchema), so the spelling is verified against their usage. */
324
+ function validationSymbols(obj, entityMod) {
325
+ const name = obj.name;
326
+ // The zod schemas are composed INTO the entity file (entity-file.ts calls
327
+ // renderZodValidators), so they import from the same `<Name>` module. The
328
+ // documented field shapes ARE those schemas' accepted shapes.
329
+ return [
330
+ {
331
+ name: `${name}InsertSchema`,
332
+ kind: "validation",
333
+ importPath: entityMod,
334
+ signature: `${name}InsertSchema: ZodType`,
335
+ returns: `ZodType`,
336
+ usage: `Zod schema validating the body of a create<${name}> / POST request (auto-generated PKs excluded).`,
337
+ fields: createFieldShapes(obj),
338
+ },
339
+ {
340
+ name: `${name}UpdateSchema`,
341
+ kind: "validation",
342
+ importPath: entityMod,
343
+ signature: `${name}UpdateSchema: ZodType`,
344
+ returns: `ZodType`,
345
+ usage: `Zod schema validating the body of an update / PATCH request (all fields optional).`,
346
+ fields: updateFieldShapes(obj),
347
+ },
348
+ ];
349
+ }
350
+ /**
351
+ * The REST endpoints the routes generator mounts for an entity. The routes
352
+ * generator does NOT emit one function per verb — it emits a single
353
+ * `<name>Routes(fastify)` handler that mounts the standard CRUD verb set at the
354
+ * entity's $path via mountCrudRoutes (or the read-only subset via
355
+ * mountReadOnlyCrudRoutes for a projection). We reuse resourcePath() — the same
356
+ * function entity-constants.ts uses to compute $path — so the documented paths
357
+ * match the generated routes exactly. The verb→path mapping mirrors the runtime
358
+ * mountCrudRoutes contract referenced in routes-file.ts's comments.
359
+ */
360
+ function restSymbols(obj, layout) {
361
+ const name = obj.name;
362
+ const path = resourcePath(obj);
363
+ const readOnly = isProjection(obj);
364
+ // REST endpoints are not importable functions — to WIRE them an adopter
365
+ // imports the entity's route registrar (`<entity>Routes`) from the routes
366
+ // module the routes generator emits (`<Name>.routes.ts`) and mounts it:
367
+ // import { <registrar> } from "<routesMod>"; await <registrar>(fastify);
368
+ // Every endpoint of one entity shares that single registrar import.
369
+ const routesMod = entityModulePath(layout, obj, `${name}.routes`);
370
+ const registrar = routesHandlerName(name);
371
+ // The REST bodies/responses ARE the same gate-verified shapes: a GET returns
372
+ // the model shape, POST takes the create shape, PATCH takes the update shape.
373
+ const modelShape = modelFieldShapes(obj);
374
+ const createShape = createFieldShapes(obj);
375
+ const updateShape = updateFieldShapes(obj);
376
+ const ep = restEndpointFactory("rest", routesMod, registrar);
377
+ const symbols = [
378
+ ep("GET", path, `List ${name} (supports filter/sort/paging query params).`, modelShape),
379
+ ep("GET", `${path}/:id`, `Fetch a single ${name} by id (404 when not found).`, modelShape),
380
+ ];
381
+ if (!readOnly) {
382
+ symbols.push(ep("POST", path, `Create a ${name} (body validated by ${name}InsertSchema).`, createShape), ep("PATCH", `${path}/:id`, `Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape), ep("DELETE", `${path}/:id`, `Delete a ${name} by id.`));
383
+ }
384
+ return symbols;
385
+ }
386
+ /** Build a REST endpoint symbol factory bound to one route surface (kind +
387
+ * route module + registrar). The Fastify and Hono REST builders share this so a
388
+ * `METHOD /path` endpoint is shaped one way; only the surface-level kind/module/
389
+ * registrar and the per-endpoint description differ between them. */
390
+ function restEndpointFactory(kind, routesMod, registrar) {
391
+ return (method, p, desc, fields) => {
392
+ const sym = {
393
+ name: `${method} ${p}`,
394
+ kind,
395
+ importPath: routesMod,
396
+ registrar,
397
+ signature: `${method} ${p}`,
398
+ usage: desc,
399
+ };
400
+ if (fields !== undefined)
401
+ sym.fields = fields;
402
+ return sym;
403
+ };
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ // T5: relations / callable / Hono (entity-level), then prompt (template.prompt).
407
+ // ---------------------------------------------------------------------------
408
+ /**
409
+ * The drizzle relations() export the entity file composes for an entity that has
410
+ * relations. The relations-block generator emits
411
+ * `export const <var>Relations = relations(<var>, ({ one, many }) => ({ … }))`
412
+ * where `<var> = variableNameFromEntity(name)` (so `Post` → `postRelations`) and
413
+ * the body is one accessor per RelationEntry the resolver derived — `author:
414
+ * one(User, …)` for a 1:N belongs-to, `tags: many(PostTag)` for an M:N @through,
415
+ * and the inverse `posts: many(…)` registered on the target. We document the
416
+ * EXPORT (the importable symbol) and ride each navigation in the field shape
417
+ * (name → cardinality-tagged target), all derived from the SAME RelationMap the
418
+ * generator emits from — never invented. Returns undefined when the resolver
419
+ * derived no relations() block for this entity (the entity file emits none).
420
+ */
421
+ function relationSymbol(obj, entityMod, relationMap) {
422
+ const entries = relationMap.get(obj.name);
423
+ if (entries === undefined || entries.length === 0)
424
+ return undefined;
425
+ const varName = variableNameFromEntity(obj.name);
426
+ const relationsExport = `${varName}Relations`;
427
+ // One field-shape row per navigation: name is the relation accessor key the
428
+ // block emits; type carries the cardinality + the entity you traverse to; note
429
+ // explains how to query it via the relational API.
430
+ const navFields = entries.map((e) => relationNavField(e));
431
+ return {
432
+ name: relationsExport,
433
+ kind: "relation",
434
+ importPath: entityMod,
435
+ signature: `const ${relationsExport}: Relations<"${tableName(varName)}", …>`,
436
+ returns: relationsExport,
437
+ usage: `Drizzle relations() for ${obj.name} — register it with your schema, then ` +
438
+ `traverse via the relational query API (db.query.${varName}.findMany({ with: { … } })).`,
439
+ fields: navFields,
440
+ };
441
+ }
442
+ /** Drizzle's relations() first arg is the table var; we only need a stable label
443
+ * here for the signature, so reuse the entity's table var name. */
444
+ function tableName(varName) {
445
+ return varName;
446
+ }
447
+ /** A field-shape row describing ONE relation navigation: accessor name + a
448
+ * cardinality-tagged target "type" + a how-to-traverse note. Mirrors the
449
+ * RelationEntry the resolver produced (1:N one() / M:N many(junction) / inverse
450
+ * many()), never restated. */
451
+ function relationNavField(e) {
452
+ if (e.cardinality === "one") {
453
+ return {
454
+ name: e.name,
455
+ type: `${e.targetEntity} (1:1 / N:1)`,
456
+ optional: true,
457
+ note: `belongs-to → ${e.targetEntity}${e.fkField ? ` via ${e.fkField}` : ""}`,
458
+ };
459
+ }
460
+ // many — either a M:N through a junction or a 1:N inverse.
461
+ if (e.junctionEntity !== undefined) {
462
+ return {
463
+ name: e.name,
464
+ type: `${e.targetEntity}[] (M:N via ${e.junctionEntity})`,
465
+ optional: true,
466
+ note: `many-to-many → ${e.targetEntity} through ${e.junctionEntity}`,
467
+ };
468
+ }
469
+ return {
470
+ name: e.name,
471
+ type: `${e.targetEntity}[] (1:N)`,
472
+ optional: true,
473
+ note: `has-many → ${e.targetEntity}`,
474
+ };
475
+ }
476
+ /**
477
+ * The callable wrapper `call<Entity>` the callable generator emits for an entity
478
+ * backed by a stored-proc / table-function source (isCallableEntity — the SAME
479
+ * filter the generator factory uses). The wrapper takes `(db, args: <argsVO>)`
480
+ * (or just `(db)` for a zero-arg proc) and returns `Promise<<Entity>[]>`, emitted
481
+ * into a FLAT-OR-PACKAGE-FOLDED `<Entity>.callable.ts` (entityOutputPath, same as
482
+ * the generator). Returns undefined for a non-callable entity (no file emitted).
483
+ */
484
+ function callableSymbol(obj, root, layout) {
485
+ if (!isCallableEntity(obj))
486
+ return undefined;
487
+ const name = obj.name;
488
+ const fn = `call${name}`;
489
+ const mod = entityModulePath(layout, obj, `${name}.callable`);
490
+ // Resolve the @parameterRef args value-object name (same resolution the
491
+ // callable template uses) to type the `args` param — undefined ⇒ zero-arg proc.
492
+ const argsRef = callableArgsRef(obj, root);
493
+ const signature = argsRef
494
+ ? `${fn}(db: NodePgDatabase, args: ${argsRef}): Promise<${name}[]>`
495
+ : `${fn}(db: NodePgDatabase): Promise<${name}[]>`;
496
+ const params = argsRef
497
+ ? [`db: NodePgDatabase`, `args: ${argsRef}`]
498
+ : [`db: NodePgDatabase`];
499
+ return {
500
+ name: fn,
501
+ kind: "callable",
502
+ importPath: mod,
503
+ signature,
504
+ params,
505
+ returns: `${name}[]`,
506
+ usage: `Call the ${name} stored procedure / table function and parse each row into a typed ${name}.`,
507
+ };
508
+ }
509
+ /** The @parameterRef value-object name for a callable entity's source, or
510
+ * undefined for a zero-arg proc. Mirrors the callable template's resolution
511
+ * (the source child's SOURCE_ATTR_PARAMETER_REF). */
512
+ function callableArgsRef(obj, root) {
513
+ for (const child of obj.ownChildren()) {
514
+ if (child.type !== TYPE_SOURCE)
515
+ continue;
516
+ const ref = child.ownAttr(SOURCE_ATTR_PARAMETER_REF);
517
+ if (typeof ref === "string" && ref !== "") {
518
+ // Only count it when it resolves to a value object (the template's guard).
519
+ const vo = root
520
+ .ownChildren()
521
+ .find((c) => c.subType === OBJECT_SUBTYPE_VALUE && refMatchesObject(c, ref));
522
+ if (vo !== undefined)
523
+ return ref;
524
+ }
525
+ }
526
+ return undefined;
527
+ }
528
+ /**
529
+ * The OPT-IN Hono CRUD registrar `register<Entity>Routes(app, deps)` the
530
+ * routesFileHono generator emits into `<Entity>.routes.hono.ts`. Parallels the
531
+ * Fastify restSymbols (same verb set, same resourcePath, read-only for
532
+ * projections) but carries the Hono registrar name + import module. Documented
533
+ * only when the adopter opts into the Hono variant (includeHonoRoutes).
534
+ */
535
+ function restHonoSymbols(obj, layout) {
536
+ const name = obj.name;
537
+ const path = resourcePath(obj);
538
+ const readOnly = isProjection(obj);
539
+ const honoMod = entityModulePath(layout, obj, `${name}.routes.hono`);
540
+ const registrar = `register${name}Routes`;
541
+ const modelShape = modelFieldShapes(obj);
542
+ const createShape = createFieldShapes(obj);
543
+ const updateShape = updateFieldShapes(obj);
544
+ const ep = restEndpointFactory("rest-hono", honoMod, registrar);
545
+ const symbols = [
546
+ ep("GET", path, `[Hono] List ${name} (filter/sort/paging query params).`, modelShape),
547
+ ep("GET", `${path}/:id`, `[Hono] Fetch a single ${name} by id (404 when not found).`, modelShape),
548
+ ];
549
+ if (!readOnly) {
550
+ symbols.push(ep("POST", path, `[Hono] Create a ${name} (body validated by ${name}InsertSchema).`, createShape), ep("PATCH", `${path}/:id`, `[Hono] Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape), ep("DELETE", `${path}/:id`, `[Hono] Delete a ${name} by id.`));
551
+ }
552
+ return symbols;
553
+ }
554
+ // ---------------------------------------------------------------------------
555
+ // template.output nodes.
556
+ // ---------------------------------------------------------------------------
557
+ function templateOutputs(root) {
558
+ return root
559
+ .ownChildren()
560
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
561
+ }
562
+ function buildTemplateUnit(tmpl, root, _layout) {
563
+ const name = tmpl.name;
564
+ const symbols = [];
565
+ // extractor + render-helper generators emit FLAT `<Name>.extractor.ts` /
566
+ // `<Name>.render.ts` (no package folding), so importPath ignores layout.
567
+ const extractorMod = templateModulePath(`${name}.extractor`);
568
+ const renderMod = templateModulePath(`${name}.render`);
569
+ const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
570
+ const payload = typeof payloadRef === "string" ? payloadRef : undefined;
571
+ const format = (tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) ?? "text").toLowerCase();
572
+ const kind = (tmpl.ownAttr(TEMPLATE_ATTR_KIND) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
573
+ // --- extractor: only json/xml output-parsers expose the extract API (matches
574
+ // extractor-file.ts's `if (format !== "json" && format !== "xml") continue`). ---
575
+ if (payload && (format === "json" || format === "xml")) {
576
+ const extract = `extract${name}`;
577
+ const extractLenient = `extractLenient${name}`;
578
+ // The extractor's strict return IS the @payloadRef value-object's interface —
579
+ // document its field shape (same VO field walk the payload interface emitter
580
+ // uses) so an agent sees what `extract<Name>` yields, not just the type name.
581
+ const payloadShape = payloadFieldShapes(root, payload);
582
+ const extractSym = {
583
+ name: extract,
584
+ kind: "extractor",
585
+ importPath: extractorMod,
586
+ signature: `${extract}(root: MetaRoot, text: string): ${payload}`,
587
+ params: [`root: MetaRoot`, `text: string`],
588
+ returns: payload,
589
+ throws: `Error when a @required field is lost (the strict opt-in gate).`,
590
+ usage: `Parse dirty LLM ${format} text into a strict, fully-typed ${payload} graph.`,
591
+ };
592
+ if (payloadShape !== undefined)
593
+ extractSym.fields = payloadShape;
594
+ symbols.push(extractSym, {
595
+ name: extractLenient,
596
+ kind: "extractor",
597
+ importPath: extractorMod,
598
+ signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
599
+ params: [`root: MetaRoot`, `text: string`],
600
+ returns: `ExtractionResult<${name}Extracted>`,
601
+ usage: `Never-throwing extract; inspect report for lost/defaulted fields.`,
602
+ });
603
+ }
604
+ // --- render: render<Name>; document → string, email → EmailDocument
605
+ // (matches render-helper.ts's @kind branch). Render is emitted for any
606
+ // @format (the helper wraps render() regardless), so it is NOT format-gated. ---
607
+ if (payload) {
608
+ const render = `render${name}`;
609
+ const isEmail = kind === TEMPLATE_KIND_EMAIL;
610
+ const returns = isEmail ? "EmailDocument" : "string";
611
+ symbols.push({
612
+ name: render,
613
+ kind: "render",
614
+ importPath: renderMod,
615
+ signature: `${render}(payload: ${payload}, provider: Provider): ${returns}`,
616
+ params: [`payload: ${payload}`, `provider: Provider`],
617
+ returns,
618
+ usage: isEmail
619
+ ? `Render the ${name} email (subject + bodies) from a typed ${payload} payload.`
620
+ : `Render the ${name} document from a typed ${payload} payload.`,
621
+ });
622
+ }
623
+ const unit = {
624
+ node: name,
625
+ package: effectivePackage(tmpl),
626
+ nodeKind: "template",
627
+ symbols,
628
+ };
629
+ const example = templateExample(name, symbols);
630
+ if (example !== undefined)
631
+ unit.example = example;
632
+ return unit;
633
+ }
634
+ // ---------------------------------------------------------------------------
635
+ // template.prompt nodes — the prompt-render handle.
636
+ // ---------------------------------------------------------------------------
637
+ /** TOP-LEVEL template.prompt nodes — matching the promptRender generator's own
638
+ * collection (`ctx.loadedRoot.ownChildren()` filtered to TYPE_TEMPLATE +
639
+ * TEMPLATE_SUBTYPE_PROMPT). A prompt nested INSIDE an entity is not collected by
640
+ * the generator, so the builder must not document it either (no over-doc). */
641
+ function templatePrompts(root) {
642
+ return root
643
+ .ownChildren()
644
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
645
+ }
646
+ /**
647
+ * The render handle promptRender() emits per template.prompt — generateRenderHandle
648
+ * (payload-codegen.ts) produces
649
+ * `export function render<Name>(payload: <payloadRef>, provider: Provider): string`
650
+ * and promptRender aggregates every handle into a SINGLE file (default outFile
651
+ * "prompts.ts"), so the import module is the bare `prompts` (no package folding;
652
+ * the generator writes the outFile verbatim). The payload field shape is the
653
+ * @payloadRef VO interface (same walk the payload-interface emitter uses), so an
654
+ * agent sees what to pass.
655
+ */
656
+ function buildPromptUnit(tmpl, root) {
657
+ const name = tmpl.name;
658
+ const symbols = [];
659
+ // promptRender writes the aggregated handles to `outFile` (default "prompts.ts").
660
+ const promptsMod = templateModulePath("prompts");
661
+ const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
662
+ const payload = typeof payloadRef === "string" ? payloadRef : undefined;
663
+ if (payload) {
664
+ const render = `render${name}`;
665
+ const sym = {
666
+ name: render,
667
+ kind: "prompt",
668
+ importPath: promptsMod,
669
+ signature: `${render}(payload: ${payload}, provider: Provider): string`,
670
+ params: [`payload: ${payload}`, `provider: Provider`],
671
+ returns: "string",
672
+ usage: `Render the ${name} prompt text from a typed ${payload} payload (ready to send to an LLM).`,
673
+ };
674
+ const payloadShape = payloadFieldShapes(root, payload);
675
+ if (payloadShape !== undefined)
676
+ sym.fields = payloadShape;
677
+ symbols.push(sym);
678
+ }
679
+ return {
680
+ node: name,
681
+ package: effectivePackage(tmpl),
682
+ nodeKind: "template",
683
+ symbols,
684
+ };
685
+ }
686
+ // ---------------------------------------------------------------------------
687
+ // Worked examples — composed from the symbols already documented above (their
688
+ // real names + importPaths) and the field SHAPES (T2) attached to the payload
689
+ // symbols, with VALUES derived from each field's TS type. Nothing is invented:
690
+ // a symbol that isn't in `symbols` is never called, and an import never names a
691
+ // module a documented symbol doesn't already point at.
692
+ // ---------------------------------------------------------------------------
693
+ /** A sample literal for a documented field, derived from its TS type STRING
694
+ * (T2's `FieldShape.type`) — never from the entity. Enum unions yield a real
695
+ * member; arrays an empty list; scalars a type-appropriate placeholder. */
696
+ function sampleValueForType(type) {
697
+ const t = type.trim();
698
+ // Enum / string-literal union (`"active" | "archived"`): use the first member
699
+ // verbatim so the example is a REAL accepted value.
700
+ const firstLiteral = t.match(/^"([^"]*)"/);
701
+ if (firstLiteral)
702
+ return `"${firstLiteral[1]}"`;
703
+ if (t.endsWith("[]"))
704
+ return "[]";
705
+ if (t === "number")
706
+ return "1";
707
+ if (t === "bigint")
708
+ return "1n";
709
+ if (t === "boolean")
710
+ return "true";
711
+ if (t === "Date")
712
+ return "new Date()";
713
+ if (t === "string")
714
+ return `"…"`;
715
+ // Unknown / object / nested type: a typed-object placeholder keeps the call
716
+ // shape intact without inventing a fake member set.
717
+ return "{}";
718
+ }
719
+ /** Build a `{ field: value; … }` object literal from a payload field shape,
720
+ * using only the REQUIRED fields plus the first optional (so an agent sees a
721
+ * minimal-but-real body) — values derived from each field's TS type. */
722
+ function objectLiteralFromFields(fields) {
723
+ if (fields === undefined || fields.length === 0)
724
+ return "{}";
725
+ const required = fields.filter((f) => !f.optional);
726
+ // If nothing is strictly required, show the first field so the body isn't `{}`.
727
+ const chosen = required.length > 0 ? required : fields.slice(0, 1);
728
+ const parts = chosen.map((f) => `${f.name}: ${sampleValueForType(f.type)}`);
729
+ return `{ ${parts.join(", ")} }`;
730
+ }
731
+ /** One `import { … } from "<mod>"` line per module, deduped, reusing each
732
+ * symbol's OWN importPath (so the example import can't drift from the docs). */
733
+ function importLines(picks) {
734
+ const order = [];
735
+ const byMod = new Map();
736
+ for (const p of picks) {
737
+ let names = byMod.get(p.importPath);
738
+ if (names === undefined) {
739
+ names = [];
740
+ byMod.set(p.importPath, names);
741
+ order.push(p.importPath);
742
+ }
743
+ if (!names.includes(p.name))
744
+ names.push(p.name);
745
+ }
746
+ return order.map((mod) => `import { ${byMod.get(mod).join(", ")} } from "${mod}";`);
747
+ }
748
+ /** A worked create→find→update→delete flow over an entity's documented CRUD
749
+ * helpers. Only emitted when the entity actually carries those data-access
750
+ * symbols (a value object / TPH subtype has only a model → no example).
751
+ *
752
+ * `pkName` is the entity's REAL primary-key field (from getPkInfo) — the
753
+ * find/update/delete calls read it back as `created.<pkName>`, so the example
754
+ * stays accurate-by-construction for an entity whose PK is not named `id`. */
755
+ function entityExample(name, pkName, symbols) {
756
+ const da = (fn) => symbols.find((s) => s.kind === "data-access" && s.name === fn);
757
+ const create = da(createFnName(name));
758
+ const find = da(findByIdFnName(name));
759
+ const update = da(updateFnName(name));
760
+ const del = da(deleteByIdFnName(name));
761
+ // Need at least create+find to have a meaningful worked flow.
762
+ if (create === undefined || find === undefined)
763
+ return undefined;
764
+ const createBody = objectLiteralFromFields(create.fields);
765
+ // The handle returned by create<Name> exposes the row's real PK accessor.
766
+ const createdPk = `created.${pkName}`;
767
+ const picks = [
768
+ { name: create.name, importPath: create.importPath },
769
+ { name: find.name, importPath: find.importPath },
770
+ ];
771
+ const body = [
772
+ `const created = await ${create.name}(db, ${createBody});`,
773
+ `const found = await ${find.name}(db, ${createdPk});`,
774
+ ];
775
+ if (update !== undefined) {
776
+ picks.push({ name: update.name, importPath: update.importPath });
777
+ body.push(`const updated = await ${update.name}(db, ${createdPk}, ${objectLiteralFromFields(update.fields)});`);
778
+ }
779
+ if (del !== undefined) {
780
+ picks.push({ name: del.name, importPath: del.importPath });
781
+ body.push(`const removed = await ${del.name}(db, ${createdPk});`);
782
+ }
783
+ return { imports: importLines(picks), body };
784
+ }
785
+ /** A worked extract / render example for a template unit, over whichever of the
786
+ * two surfaces the template actually exposes (extract is json/xml-gated). */
787
+ function templateExample(name, symbols) {
788
+ const extract = symbols.find((s) => s.kind === "extractor" && s.name === `extract${name}`);
789
+ const renderSym = symbols.find((s) => s.kind === "render" && s.name === `render${name}`);
790
+ if (extract === undefined && renderSym === undefined)
791
+ return undefined;
792
+ const picks = [];
793
+ const body = [];
794
+ if (extract !== undefined) {
795
+ picks.push({ name: extract.name, importPath: extract.importPath });
796
+ body.push(`const extracted = ${extract.name}(root, llmText);`);
797
+ }
798
+ if (renderSym !== undefined) {
799
+ picks.push({ name: renderSym.name, importPath: renderSym.importPath });
800
+ // Render's payload object literal comes from the @payloadRef VO shape the
801
+ // render symbol returns/consumes; reuse the extractor's documented payload
802
+ // fields when present so the example body is real.
803
+ const payloadFields = extract?.fields;
804
+ const payloadLit = objectLiteralFromFields(payloadFields);
805
+ body.push(`const output = ${renderSym.name}(${payloadLit}, provider);`);
806
+ }
807
+ return { imports: importLines(picks), body };
808
+ }
809
+ //# sourceMappingURL=api-model.js.map