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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts +12 -6
  3. package/dist/column-mapper.d.ts.map +1 -1
  4. package/dist/column-mapper.js +68 -28
  5. package/dist/column-mapper.js.map +1 -1
  6. package/dist/constants.d.ts +8 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +16 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/docs-paths.d.ts +58 -0
  11. package/dist/docs-paths.d.ts.map +1 -0
  12. package/dist/docs-paths.js +89 -0
  13. package/dist/docs-paths.js.map +1 -0
  14. package/dist/enum-import.d.ts +14 -0
  15. package/dist/enum-import.d.ts.map +1 -0
  16. package/dist/enum-import.js +35 -0
  17. package/dist/enum-import.js.map +1 -0
  18. package/dist/enum-shared.d.ts +32 -0
  19. package/dist/enum-shared.d.ts.map +1 -0
  20. package/dist/enum-shared.js +83 -0
  21. package/dist/enum-shared.js.map +1 -0
  22. package/dist/generator-registry.d.ts +22 -0
  23. package/dist/generator-registry.d.ts.map +1 -0
  24. package/dist/generator-registry.js +161 -0
  25. package/dist/generator-registry.js.map +1 -0
  26. package/dist/generator.d.ts +6 -0
  27. package/dist/generator.d.ts.map +1 -1
  28. package/dist/generator.js.map +1 -1
  29. package/dist/generators/api-doc-render.d.ts +17 -0
  30. package/dist/generators/api-doc-render.d.ts.map +1 -0
  31. package/dist/generators/api-doc-render.js +431 -0
  32. package/dist/generators/api-doc-render.js.map +1 -0
  33. package/dist/generators/api-docs-file.d.ts +21 -0
  34. package/dist/generators/api-docs-file.d.ts.map +1 -0
  35. package/dist/generators/api-docs-file.js +112 -0
  36. package/dist/generators/api-docs-file.js.map +1 -0
  37. package/dist/generators/api-field-shape.d.ts +39 -0
  38. package/dist/generators/api-field-shape.d.ts.map +1 -0
  39. package/dist/generators/api-field-shape.js +92 -0
  40. package/dist/generators/api-field-shape.js.map +1 -0
  41. package/dist/generators/api-label.d.ts +3 -0
  42. package/dist/generators/api-label.d.ts.map +1 -0
  43. package/dist/generators/api-label.js +8 -0
  44. package/dist/generators/api-label.js.map +1 -0
  45. package/dist/generators/api-model.d.ts +122 -0
  46. package/dist/generators/api-model.d.ts.map +1 -0
  47. package/dist/generators/api-model.js +809 -0
  48. package/dist/generators/api-model.js.map +1 -0
  49. package/dist/generators/docs-data-builder.d.ts +26 -4
  50. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  51. package/dist/generators/docs-data-builder.js +439 -167
  52. package/dist/generators/docs-data-builder.js.map +1 -1
  53. package/dist/generators/docs-data.d.ts +136 -27
  54. package/dist/generators/docs-data.d.ts.map +1 -1
  55. package/dist/generators/docs-data.js +1 -1
  56. package/dist/generators/docs-data.js.map +1 -1
  57. package/dist/generators/docs-file.d.ts +19 -0
  58. package/dist/generators/docs-file.d.ts.map +1 -1
  59. package/dist/generators/docs-file.js +154 -27
  60. package/dist/generators/docs-file.js.map +1 -1
  61. package/dist/generators/entity-file.d.ts.map +1 -1
  62. package/dist/generators/entity-file.js +29 -14
  63. package/dist/generators/entity-file.js.map +1 -1
  64. package/dist/generators/extractor-file.d.ts.map +1 -1
  65. package/dist/generators/extractor-file.js +2 -1
  66. package/dist/generators/extractor-file.js.map +1 -1
  67. package/dist/generators/field-anchor.d.ts +7 -0
  68. package/dist/generators/field-anchor.d.ts.map +1 -0
  69. package/dist/generators/field-anchor.js +23 -0
  70. package/dist/generators/field-anchor.js.map +1 -0
  71. package/dist/generators/index.d.ts +8 -1
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +6 -0
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/mermaid-er.d.ts +14 -0
  76. package/dist/generators/mermaid-er.d.ts.map +1 -1
  77. package/dist/generators/mermaid-er.js +14 -0
  78. package/dist/generators/mermaid-er.js.map +1 -1
  79. package/dist/generators/output-parser-file.d.ts.map +1 -1
  80. package/dist/generators/output-parser-file.js +3 -4
  81. package/dist/generators/output-parser-file.js.map +1 -1
  82. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  83. package/dist/generators/output-prompt-file.js +2 -2
  84. package/dist/generators/output-prompt-file.js.map +1 -1
  85. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  86. package/dist/generators/prompt-render-file.js +3 -4
  87. package/dist/generators/prompt-render-file.js.map +1 -1
  88. package/dist/generators/queries-file.d.ts.map +1 -1
  89. package/dist/generators/queries-file.js +8 -3
  90. package/dist/generators/queries-file.js.map +1 -1
  91. package/dist/generators/render-helper-file.d.ts.map +1 -1
  92. package/dist/generators/render-helper-file.js +2 -2
  93. package/dist/generators/render-helper-file.js.map +1 -1
  94. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  95. package/dist/generators/routes-file-hono.js +5 -1
  96. package/dist/generators/routes-file-hono.js.map +1 -1
  97. package/dist/generators/routes-file.d.ts +3 -0
  98. package/dist/generators/routes-file.d.ts.map +1 -1
  99. package/dist/generators/routes-file.js +6 -1
  100. package/dist/generators/routes-file.js.map +1 -1
  101. package/dist/generators/template-doc-builder.d.ts +19 -0
  102. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  103. package/dist/generators/template-doc-builder.js +220 -0
  104. package/dist/generators/template-doc-builder.js.map +1 -0
  105. package/dist/generators/template-doc-data.d.ts +62 -0
  106. package/dist/generators/template-doc-data.d.ts.map +1 -0
  107. package/dist/generators/template-doc-data.js +16 -0
  108. package/dist/generators/template-doc-data.js.map +1 -0
  109. package/dist/generators/template-payload-tree.d.ts +15 -0
  110. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  111. package/dist/generators/template-payload-tree.js +61 -0
  112. package/dist/generators/template-payload-tree.js.map +1 -0
  113. package/dist/generators/template-source-annotate.d.ts +74 -0
  114. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  115. package/dist/generators/template-source-annotate.js +184 -0
  116. package/dist/generators/template-source-annotate.js.map +1 -0
  117. package/dist/generators/template-source-render.d.ts +24 -0
  118. package/dist/generators/template-source-render.d.ts.map +1 -0
  119. package/dist/generators/template-source-render.js +175 -0
  120. package/dist/generators/template-source-render.js.map +1 -0
  121. package/dist/generators/trace-helper-file.d.ts +9 -0
  122. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  123. package/dist/generators/trace-helper-file.js +196 -0
  124. package/dist/generators/trace-helper-file.js.map +1 -0
  125. package/dist/import-path.d.ts +18 -0
  126. package/dist/import-path.d.ts.map +1 -1
  127. package/dist/import-path.js +21 -0
  128. package/dist/import-path.js.map +1 -1
  129. package/dist/index.d.ts +29 -4
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +28 -2
  132. package/dist/index.js.map +1 -1
  133. package/dist/metaobjects-config.d.ts +101 -2
  134. package/dist/metaobjects-config.d.ts.map +1 -1
  135. package/dist/metaobjects-config.js +46 -0
  136. package/dist/metaobjects-config.js.map +1 -1
  137. package/dist/naming.d.ts +39 -2
  138. package/dist/naming.d.ts.map +1 -1
  139. package/dist/naming.js +52 -3
  140. package/dist/naming.js.map +1 -1
  141. package/dist/payload-codegen.d.ts.map +1 -1
  142. package/dist/payload-codegen.js +14 -6
  143. package/dist/payload-codegen.js.map +1 -1
  144. package/dist/pk-resolver.d.ts.map +1 -1
  145. package/dist/pk-resolver.js +4 -2
  146. package/dist/pk-resolver.js.map +1 -1
  147. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  148. package/dist/projection/extract-view-spec.js +52 -26
  149. package/dist/projection/extract-view-spec.js.map +1 -1
  150. package/dist/relation-resolver.d.ts +16 -0
  151. package/dist/relation-resolver.d.ts.map +1 -1
  152. package/dist/relation-resolver.js +82 -1
  153. package/dist/relation-resolver.js.map +1 -1
  154. package/dist/render-context.d.ts +25 -2
  155. package/dist/render-context.d.ts.map +1 -1
  156. package/dist/render-context.js +7 -0
  157. package/dist/render-context.js.map +1 -1
  158. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  159. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  160. package/dist/render-engine/embedded-templates.generated.js +15 -0
  161. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  162. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  163. package/dist/render-engine/framework-provider.js +26 -13
  164. package/dist/render-engine/framework-provider.js.map +1 -1
  165. package/dist/runner.d.ts.map +1 -1
  166. package/dist/runner.js +20 -0
  167. package/dist/runner.js.map +1 -1
  168. package/dist/templates/docs-file.d.ts +2 -6
  169. package/dist/templates/docs-file.d.ts.map +1 -1
  170. package/dist/templates/docs-file.js +2 -5
  171. package/dist/templates/docs-file.js.map +1 -1
  172. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  173. package/dist/templates/drizzle-schema.js +72 -23
  174. package/dist/templates/drizzle-schema.js.map +1 -1
  175. package/dist/templates/entity-constants.d.ts +7 -0
  176. package/dist/templates/entity-constants.d.ts.map +1 -1
  177. package/dist/templates/entity-constants.js +3 -3
  178. package/dist/templates/entity-constants.js.map +1 -1
  179. package/dist/templates/entity-file.d.ts.map +1 -1
  180. package/dist/templates/entity-file.js +16 -5
  181. package/dist/templates/entity-file.js.map +1 -1
  182. package/dist/templates/enums-file.d.ts +11 -0
  183. package/dist/templates/enums-file.d.ts.map +1 -0
  184. package/dist/templates/enums-file.js +44 -0
  185. package/dist/templates/enums-file.js.map +1 -0
  186. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  187. package/dist/templates/extract-delegate-emitter.js +6 -8
  188. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  189. package/dist/templates/extractor.d.ts.map +1 -1
  190. package/dist/templates/extractor.js +58 -41
  191. package/dist/templates/extractor.js.map +1 -1
  192. package/dist/templates/field-meta.d.ts.map +1 -1
  193. package/dist/templates/field-meta.js +2 -6
  194. package/dist/templates/field-meta.js.map +1 -1
  195. package/dist/templates/filter-allowlist.d.ts +7 -2
  196. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  197. package/dist/templates/filter-allowlist.js +18 -10
  198. package/dist/templates/filter-allowlist.js.map +1 -1
  199. package/dist/templates/filter-shared.js +2 -2
  200. package/dist/templates/filter-shared.js.map +1 -1
  201. package/dist/templates/filter-type.d.ts +7 -1
  202. package/dist/templates/filter-type.d.ts.map +1 -1
  203. package/dist/templates/filter-type.js +10 -6
  204. package/dist/templates/filter-type.js.map +1 -1
  205. package/dist/templates/find-templates.d.ts +4 -0
  206. package/dist/templates/find-templates.d.ts.map +1 -0
  207. package/dist/templates/find-templates.js +15 -0
  208. package/dist/templates/find-templates.js.map +1 -0
  209. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  210. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  211. package/dist/templates/fr010-field-mapping.js +15 -11
  212. package/dist/templates/fr010-field-mapping.js.map +1 -1
  213. package/dist/templates/inferred-types.d.ts +44 -7
  214. package/dist/templates/inferred-types.d.ts.map +1 -1
  215. package/dist/templates/inferred-types.js +121 -19
  216. package/dist/templates/inferred-types.js.map +1 -1
  217. package/dist/templates/mermaid-er.d.ts +35 -2
  218. package/dist/templates/mermaid-er.d.ts.map +1 -1
  219. package/dist/templates/mermaid-er.js +174 -7
  220. package/dist/templates/mermaid-er.js.map +1 -1
  221. package/dist/templates/output-format-spec-emitter.js +1 -1
  222. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  223. package/dist/templates/output-parser.d.ts.map +1 -1
  224. package/dist/templates/output-parser.js +31 -80
  225. package/dist/templates/output-parser.js.map +1 -1
  226. package/dist/templates/output-prompt.d.ts.map +1 -1
  227. package/dist/templates/output-prompt.js +2 -2
  228. package/dist/templates/output-prompt.js.map +1 -1
  229. package/dist/templates/queries-file.d.ts.map +1 -1
  230. package/dist/templates/queries-file.js +113 -5
  231. package/dist/templates/queries-file.js.map +1 -1
  232. package/dist/templates/queries.d.ts +7 -2
  233. package/dist/templates/queries.d.ts.map +1 -1
  234. package/dist/templates/queries.js +15 -15
  235. package/dist/templates/queries.js.map +1 -1
  236. package/dist/templates/relations-block.d.ts.map +1 -1
  237. package/dist/templates/relations-block.js +12 -3
  238. package/dist/templates/relations-block.js.map +1 -1
  239. package/dist/templates/render-helper.d.ts.map +1 -1
  240. package/dist/templates/render-helper.js +5 -5
  241. package/dist/templates/render-helper.js.map +1 -1
  242. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  243. package/dist/templates/routes-file-hono.js +1 -2
  244. package/dist/templates/routes-file-hono.js.map +1 -1
  245. package/dist/templates/routes-file.d.ts.map +1 -1
  246. package/dist/templates/routes-file.js +184 -7
  247. package/dist/templates/routes-file.js.map +1 -1
  248. package/dist/templates/tph-discriminator.d.ts +56 -0
  249. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  250. package/dist/templates/tph-discriminator.js +180 -0
  251. package/dist/templates/tph-discriminator.js.map +1 -0
  252. package/dist/templates/value-object-file.d.ts +2 -1
  253. package/dist/templates/value-object-file.d.ts.map +1 -1
  254. package/dist/templates/value-object-file.js +33 -5
  255. package/dist/templates/value-object-file.js.map +1 -1
  256. package/dist/templates/zod-validators.d.ts +65 -2
  257. package/dist/templates/zod-validators.d.ts.map +1 -1
  258. package/dist/templates/zod-validators.js +202 -22
  259. package/dist/templates/zod-validators.js.map +1 -1
  260. package/package.json +103 -34
  261. package/src/column-mapper.ts +79 -32
  262. package/src/constants.ts +18 -0
  263. package/src/docs-paths.ts +128 -0
  264. package/src/enum-import.ts +43 -0
  265. package/src/enum-shared.ts +95 -0
  266. package/src/generator-registry.ts +204 -0
  267. package/src/generator.ts +6 -0
  268. package/src/generators/api-doc-render.ts +572 -0
  269. package/src/generators/api-docs-file.ts +146 -0
  270. package/src/generators/api-field-shape.ts +114 -0
  271. package/src/generators/api-label.ts +7 -0
  272. package/src/generators/api-model.ts +1067 -0
  273. package/src/generators/docs-data-builder.ts +483 -189
  274. package/src/generators/docs-data.ts +139 -28
  275. package/src/generators/docs-file.ts +205 -39
  276. package/src/generators/entity-file.ts +31 -15
  277. package/src/generators/extractor-file.ts +2 -1
  278. package/src/generators/field-anchor.ts +24 -0
  279. package/src/generators/index.ts +8 -1
  280. package/src/generators/mermaid-er.ts +14 -0
  281. package/src/generators/output-parser-file.ts +3 -4
  282. package/src/generators/output-prompt-file.ts +2 -1
  283. package/src/generators/prompt-render-file.ts +3 -4
  284. package/src/generators/queries-file.ts +9 -3
  285. package/src/generators/render-helper-file.ts +2 -1
  286. package/src/generators/routes-file-hono.ts +5 -1
  287. package/src/generators/routes-file.ts +7 -1
  288. package/src/generators/template-doc-builder.ts +306 -0
  289. package/src/generators/template-doc-data.ts +85 -0
  290. package/src/generators/template-payload-tree.ts +71 -0
  291. package/src/generators/template-source-annotate.ts +290 -0
  292. package/src/generators/template-source-render.ts +203 -0
  293. package/src/generators/trace-helper-file.ts +301 -0
  294. package/src/import-path.ts +28 -0
  295. package/src/index.ts +55 -4
  296. package/src/metaobjects-config.ts +146 -2
  297. package/src/naming.ts +73 -3
  298. package/src/payload-codegen.ts +16 -5
  299. package/src/pk-resolver.ts +4 -2
  300. package/src/projection/extract-view-spec.ts +50 -31
  301. package/src/relation-resolver.ts +103 -1
  302. package/src/render-context.ts +32 -2
  303. package/src/render-engine/embedded-templates.generated.ts +14 -0
  304. package/src/render-engine/framework-provider.ts +25 -11
  305. package/src/runner.ts +24 -0
  306. package/src/templates/docs-file.ts +2 -9
  307. package/src/templates/drizzle-schema.ts +80 -28
  308. package/src/templates/entity-constants.ts +3 -3
  309. package/src/templates/entity-file.ts +16 -5
  310. package/src/templates/enums-file.ts +50 -0
  311. package/src/templates/extract-delegate-emitter.ts +6 -7
  312. package/src/templates/extractor.ts +70 -40
  313. package/src/templates/field-meta.ts +1 -7
  314. package/src/templates/filter-allowlist.ts +18 -11
  315. package/src/templates/filter-shared.ts +2 -2
  316. package/src/templates/filter-type.ts +9 -7
  317. package/src/templates/find-templates.ts +15 -0
  318. package/src/templates/fr010-field-mapping.ts +15 -13
  319. package/src/templates/inferred-types.ts +122 -21
  320. package/src/templates/mermaid-er.ts +176 -8
  321. package/src/templates/output-format-spec-emitter.ts +1 -1
  322. package/src/templates/output-parser.ts +31 -80
  323. package/src/templates/output-prompt.ts +2 -1
  324. package/src/templates/queries-file.ts +133 -4
  325. package/src/templates/queries.ts +21 -15
  326. package/src/templates/relations-block.ts +19 -3
  327. package/src/templates/render-helper.ts +5 -4
  328. package/src/templates/routes-file-hono.ts +1 -2
  329. package/src/templates/routes-file.ts +234 -7
  330. package/src/templates/tph-discriminator.ts +232 -0
  331. package/src/templates/value-object-file.ts +39 -5
  332. package/src/templates/zod-validators.ts +225 -21
  333. package/templates/api/agent-api.md.mustache +30 -0
  334. package/templates/api/entity-api.md.mustache +69 -0
  335. package/templates/api/index.md.mustache +21 -0
  336. package/templates/docs/entity-page.md.mustache +33 -21
  337. package/templates/docs/template-page.md.mustache +56 -0
  338. package/dist/templates/extract-schema-emitter.d.ts +0 -8
  339. package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
  340. package/dist/templates/extract-schema-emitter.js +0 -81
  341. package/dist/templates/extract-schema-emitter.js.map +0 -1
  342. package/src/templates/extract-schema-emitter.ts +0 -111
package/src/naming.ts CHANGED
@@ -58,9 +58,79 @@ export function viewNameFromProjection(
58
58
  return "v" + sep + applyColumnNamingStrategy(projectionName, strategy);
59
59
  }
60
60
 
61
- /** PascalCase entity camelCase plural for the Drizzle table variable. */
62
- export function variableNameFromEntity(entityName: string): string {
63
- return pluralize(toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1)));
61
+ /** Codegen control over how an entity name lowers to its collection (table)
62
+ * variable name. Both knobs are project-level codegen config (ADR-0001 —
63
+ * naming is a per-port codegen concern, NOT a metadata attribute), so they
64
+ * carry no cross-port conformance cost. */
65
+ export interface CollectionNameOptions {
66
+ /** Auto-pluralize the camelCase entity name. Default `true` (e.g.
67
+ * `AgentConfig` → `agentConfigs`). Set `false` to keep it singular
68
+ * (`agentConfig`). */
69
+ pluralize?: boolean;
70
+ /** Per-entity exact var-name overrides, keyed by the bare entity name. Wins
71
+ * over `pluralize` — the escape hatch for the handful of tables a global
72
+ * rule gets wrong (e.g. `{ AuditLog: "auditLog", LlmTierConfig: "llmTierConfig" }`). */
73
+ overrides?: Record<string, string>;
74
+ }
75
+
76
+ /** PascalCase entity → camelCase Drizzle table variable. Auto-pluralizes by
77
+ * default; `opts` lets a project turn pluralization off globally and/or pin
78
+ * exact names per entity. With no `opts` the behavior is the historical
79
+ * always-pluralize (callers like the relation-resolver that only need the
80
+ * cosmetic query-API member name pass nothing). */
81
+ export function variableNameFromEntity(entityName: string, opts?: CollectionNameOptions): string {
82
+ const override = opts?.overrides?.[entityName];
83
+ if (override !== undefined && override.length > 0) return override;
84
+ const camel = toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1));
85
+ return opts?.pluralize === false ? camel : pluralize(camel);
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Generated CRUD-helper symbol names (single source of truth).
90
+ //
91
+ // The queries generator (templates/queries.ts) emits one exported async function
92
+ // per CRUD verb whose NAME is derived purely from the entity name. These helpers
93
+ // are the canonical spelling of those names so anything that needs to REFER to a
94
+ // generated symbol (e.g. the api-docs ApiModel builder) derives the exact same
95
+ // string the generator emits — no drift, no invented names. The queries template
96
+ // itself uses these so the two can never disagree.
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Generated read-by-PK helper name: `find<Entity>ById`. */
100
+ export function findByIdFnName(entityName: string): string {
101
+ return `find${entityName}ById`;
102
+ }
103
+
104
+ /** Generated list helper name: `list<Plural>` (PascalCase plural). */
105
+ export function listFnName(entityName: string): string {
106
+ return `list${pluralize(entityName)}`;
107
+ }
108
+
109
+ /** Generated create helper name: `create<Entity>`. */
110
+ export function createFnName(entityName: string): string {
111
+ return `create${entityName}`;
112
+ }
113
+
114
+ /** Generated update helper name: `update<Entity>`. */
115
+ export function updateFnName(entityName: string): string {
116
+ return `update${entityName}`;
117
+ }
118
+
119
+ /** Generated delete-by-PK helper name: `delete<Entity>ById`. */
120
+ export function deleteByIdFnName(entityName: string): string {
121
+ return `delete${entityName}ById`;
122
+ }
123
+
124
+ /**
125
+ * Generated Fastify route-registrar name: camelCase `<entity>Routes`. The routes
126
+ * generator (templates/routes-file.ts) emits a single exported
127
+ * `export async function <entity>Routes(fastify)` that mounts the entity's CRUD
128
+ * verb set — this is the symbol an adopter imports to wire the endpoints. Kept
129
+ * here as the single source of truth so the routes template and the api-docs
130
+ * ApiModel builder derive the exact same spelling (no drift).
131
+ */
132
+ export function routesHandlerName(entityName: string): string {
133
+ return `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Routes`;
64
134
  }
65
135
 
66
136
  // Re-exported here for callers that import from codegen-ts's naming module.
@@ -23,6 +23,8 @@ import {
23
23
  TEMPLATE_ATTR_PAYLOAD_REF,
24
24
  TEMPLATE_ATTR_TEXT_REF,
25
25
  TEMPLATE_ATTR_FORMAT,
26
+ refMatchesObject,
27
+ stripPackage,
26
28
  } from "@metaobjectsdev/metadata";
27
29
  import { enumValues } from "./enum-meta.js";
28
30
  import { enumUnionAliasName, enumUnionString } from "./templates/inferred-types.js";
@@ -45,7 +47,9 @@ const SCALAR_TS: Record<string, string> = {
45
47
  };
46
48
 
47
49
  function findObject(root: MetaData, name: string): MetaData | undefined {
48
- return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
50
+ // FR-032 @payloadRef/@responseRef are FQN after the desugar/sweep; match on
51
+ // the effective FQN resolution key (with bare back-compat).
52
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
49
53
  }
50
54
 
51
55
  /**
@@ -61,7 +65,7 @@ function fieldTsType(
61
65
  ownerName: string,
62
66
  ): { type: string; refVo?: string; enumAlias?: { name: string; decl: string } } {
63
67
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
64
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
68
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
65
69
  const refName = typeof ref === "string" ? ref : "unknown";
66
70
  // isArray is a structural property on MetaData, not an attr.
67
71
  const isArray = field.isArray;
@@ -92,7 +96,7 @@ function fieldTsType(
92
96
 
93
97
  /** True iff the field's @required is explicitly set to true. */
94
98
  function isFieldRequired(field: MetaData): boolean {
95
- return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
99
+ return field.attr(FIELD_ATTR_REQUIRED) === true;
96
100
  }
97
101
 
98
102
  function emitInterface(
@@ -168,14 +172,21 @@ export function generateRenderHandle(root: MetaData, templateName: string): stri
168
172
  const tmpl = root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === templateName);
169
173
  if (!tmpl) throw new Error(`template "${templateName}" not found`);
170
174
  const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
175
+ // FR-032 — @payloadRef is an FQN after the desugar/sweep; the generated TS
176
+ // TYPE NAME is the resolved value-object's bare name (an FQN like
177
+ // `acme::ai::Payload` is not a valid TS identifier). Fall back to the last
178
+ // `::`-segment when the VO is not in this root (defensive).
179
+ const payloadType =
180
+ (typeof payloadRef === "string" ? findObject(root, payloadRef)?.name : undefined) ??
181
+ (typeof payloadRef === "string" ? stripPackage(payloadRef) : String(payloadRef));
171
182
  const textRef = tmpl.ownAttr(TEMPLATE_ATTR_TEXT_REF);
172
183
  const format = (tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
173
184
  const fn = `render${pascal(templateName)}`;
174
185
  return [
175
186
  `import { render, type Provider } from "@metaobjectsdev/render";`,
176
- `import type { ${payloadRef} } from "./payloads.js";`,
187
+ `import type { ${payloadType} } from "./payloads.js";`,
177
188
  ``,
178
- `export function ${fn}(payload: ${payloadRef}, provider: Provider): string {`,
189
+ `export function ${fn}(payload: ${payloadType}, provider: Provider): string {`,
179
190
  ` return render({ ref: ${JSON.stringify(textRef)}, payload, format: ${JSON.stringify(format)}, provider });`,
180
191
  `}`,
181
192
  ``,
@@ -30,7 +30,9 @@ export function buildPkMap(root: MetaRoot): Map<string, PkInfo> {
30
30
  // primaryIdentity() resolves the primary identity across the super-chain.
31
31
  const primary = obj.primaryIdentity();
32
32
  if (!primary) continue;
33
- const fields = primary.ownAttr(IDENTITY_ATTR_FIELDS);
33
+ // attr() (effective) not ownAttr() — @fields/@generation can be inherited when the
34
+ // identity node-level `extends` a base identity without restating them (#56).
35
+ const fields = primary.attr(IDENTITY_ATTR_FIELDS);
34
36
  if (!Array.isArray(fields) && typeof fields !== "string") continue;
35
37
  const fieldsList = Array.isArray(fields) ? fields : [fields];
36
38
  if (fieldsList.length === 0) continue;
@@ -38,7 +40,7 @@ export function buildPkMap(root: MetaRoot): Map<string, PkInfo> {
38
40
  // findField() resolves the field across the super-chain (handles extends:).
39
41
  const pkField = obj.findField(pkFieldName);
40
42
  const fieldSubType = pkField?.subType ?? FIELD_SUBTYPE_LONG; // sane default
41
- const generation = primary.ownAttr(IDENTITY_ATTR_GENERATION);
43
+ const generation = primary.attr(IDENTITY_ATTR_GENERATION);
42
44
  const info: PkInfo = { fieldName: pkFieldName, fieldSubType };
43
45
  if (typeof generation === "string") info.generation = generation;
44
46
  result.set(obj.name, info);
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  TYPE_FIELD,
3
+ TYPE_IDENTITY,
3
4
  TYPE_ORIGIN,
4
5
  TYPE_RELATIONSHIP,
5
6
  MetaSource,
@@ -79,33 +80,62 @@ export function projectionViewName(
79
80
  return viewName(projection, { columnNamingStrategy });
80
81
  }
81
82
 
83
+ /**
84
+ * FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
85
+ * owner part of `<owner>.<child>...` resolved as an object. Mirrors the
86
+ * loader's `_refNamedOwner`: the ref names the anchor, never the physical
87
+ * declaring ancestor of an inherited child.
88
+ */
89
+ function refNamedOwner(node: MetaData, root: MetaRoot): MetaObject | undefined {
90
+ const ref = (node as { superRef?: string }).superRef;
91
+ if (ref === undefined) return undefined;
92
+ const lastSep = ref.lastIndexOf("::");
93
+ const tail = lastSep === -1 ? ref : ref.slice(lastSep + 2);
94
+ const dot = tail.indexOf(".");
95
+ if (dot <= 0) return undefined;
96
+ return root.findObject(tail.slice(0, dot)) ?? undefined;
97
+ }
98
+
82
99
  function baseEntityFor(
83
100
  projection: MetaObject,
84
101
  root: MetaRoot,
85
102
  ): MetaObject {
86
- // v1: base entity is the resolved super (set via `extends:` in metadata).
87
- const superModel = projection.superResolved;
88
- const superName = superModel?.name ?? projection.superRef;
89
- if (!superName) {
90
- throw new Error(
91
- `Projection ${projection.name}: missing extends projections must extend a writable entity in v1.`,
92
- );
103
+ // FR-024 base-anchor rules (mirror the loader's _deriveBaseEntity):
104
+ // 1) the extends-bound identity anchors the base entity;
105
+ // 2) else the single distinct entity targeted by extends-bound fields.
106
+ // The pre-FR-024 object-level `extends:` firehose is removed (B4b cutover).
107
+ //
108
+ // COUPLING NOTE: this intentionally derives the anchor ONLY from the ref's
109
+ // named owner (refNamedOwner), NOT the loader's `superResolved.parent`
110
+ // fallback for a non-dotted identity extends. That fallback is unreachable
111
+ // here because the loader gate (validate-identity-passthrough →
112
+ // ERR_PROJECTION_IDENTITY_NOT_EXTENDED) rejects any projection whose identity
113
+ // is not dotted-extends-bound before codegen runs. If that loader gate is
114
+ // ever loosened, this function must grow the same fallback.
115
+ for (const identity of projection
116
+ .ownChildren()
117
+ .filter((c) => c.type === TYPE_IDENTITY)) {
118
+ const named = refNamedOwner(identity, root);
119
+ if (named !== undefined) return named;
93
120
  }
94
- const base =
95
- superModel instanceof MetaObject ? superModel : root.findObject(superName);
96
- if (!base) {
97
- throw new Error(
98
- `Projection ${projection.name}: extends "${superName}" does not resolve to any entity.`,
99
- );
121
+ const targets = new Set<MetaObject>();
122
+ for (const f of projection.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
123
+ const named = refNamedOwner(f, root);
124
+ if (named !== undefined && named !== projection) targets.add(named);
100
125
  }
101
- return base;
126
+ if (targets.size === 1) return [...targets][0]!;
127
+ throw new Error(
128
+ `Projection ${projection.name}: cannot derive the base entity — declare an ` +
129
+ `extends-bound identity (identity.primary { name, extends: "<Entity>.<identity>" }) ` +
130
+ `to anchor the base (FR-024).`,
131
+ );
102
132
  }
103
133
 
104
134
  function sourceColumnNameFor(
105
135
  entityField: MetaData,
106
136
  ctx: ExtractContext,
107
137
  ): string {
108
- const col = entityField.ownAttr(FIELD_ATTR_COLUMN);
138
+ const col = entityField.attr(FIELD_ATTR_COLUMN);
109
139
  if (typeof col === "string" && col !== "") return col;
110
140
  return columnNameFromField(entityField.name, ctx.columnNamingStrategy);
111
141
  }
@@ -270,22 +300,11 @@ function buildSelectSpec(
270
300
  ): SelectSpec {
271
301
  const columns: SelectColumn[] = [];
272
302
 
273
- // Inherited fields from extends parent emit as passthrough on baseAlias.
274
- // Skip fields that the projection has overridden with an explicit origin.
275
- // fields() is effective-by-default, so multi-level inheritance (base → BaseEntity) works.
276
- for (const baseField of base.fields()) {
277
- const overridden = projection.ownChildren().find(
278
- (c) => c.type === TYPE_FIELD && c.name === baseField.name,
279
- );
280
- if (overridden) continue;
281
- columns.push({
282
- kind: "passthrough",
283
- fieldName: baseField.name,
284
- dbColAlias: sourceColumnNameFor(baseField, ctx),
285
- sourceAlias: joinTree.baseAlias,
286
- sourceColumn: sourceColumnNameFor(baseField, ctx),
287
- });
288
- }
303
+ // FR-024 (ADR-0028): the projection's DECLARED field set IS the exposure
304
+ // the inclusive list, fail-closed by construction. The pre-FR-024 loop that
305
+ // emitted every base-entity field as an implicit passthrough (the firehose)
306
+ // is removed with the B4b cutover: base columns are declared explicitly as
307
+ // extends-bound fields (`{ field.int: { name: id, extends: "Program.id" } }`).
289
308
 
290
309
  // Fields explicitly declared on the projection.
291
310
  for (const field of projection.ownChildren()) {
@@ -5,11 +5,14 @@
5
5
  //
6
6
  // Reads identity.reference declarations to determine the physical reference side.
7
7
 
8
- import type { MetaRoot } from "@metaobjectsdev/metadata";
8
+ import type { MetaRoot, MetaObject, MetaRelationship } from "@metaobjectsdev/metadata";
9
9
  import {
10
10
  RELATIONSHIP_ATTR_CARDINALITY,
11
11
  RELATIONSHIP_ATTR_OBJECT_REF,
12
+ RELATIONSHIP_ATTR_THROUGH,
12
13
  CARDINALITY_ONE,
14
+ CARDINALITY_MANY,
15
+ deriveM2MFields,
13
16
  stripPackage,
14
17
  } from "@metaobjectsdev/metadata";
15
18
  import { variableNameFromEntity } from "./naming.js";
@@ -26,6 +29,22 @@ export interface RelationEntry {
26
29
  fkField?: string;
27
30
  /** For cardinality 'one': the target entity's PK field (e.g., "id") */
28
31
  targetPkField?: string;
32
+ /**
33
+ * FR-018 M:N navigation fields. Present only for a many-to-many navigation (a
34
+ * `@cardinality: "many"` relationship that declares `@through`). The Drizzle
35
+ * relations() block emits `many(<junction>)` for these; the routes file emits
36
+ * a `mountM2mRoute(...)` traversal. The junction FK fields are DERIVED from the
37
+ * junction entity's two `identity.reference` children (the SSOT), never
38
+ * restated on the relationship.
39
+ */
40
+ /** The junction/through entity name (e.g., "PostTag"). M:N entries only. */
41
+ junctionEntity?: string;
42
+ /** Junction FK field holding the source-side key (logical field name, e.g. "postId"). M:N only. */
43
+ sourceJoinField?: string;
44
+ /** Junction FK field holding the target-side key (logical field name, e.g. "tagId"). M:N only. */
45
+ targetJoinField?: string;
46
+ /** Undirected self-join: union both junction FK columns at read time. M:N only. */
47
+ symmetric?: boolean;
29
48
  }
30
49
 
31
50
  /** Map from entity name → list of relations for that entity's relations() block */
@@ -51,6 +70,16 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
51
70
 
52
71
  for (const child of obj.relationships()) {
53
72
  const cardinality = child.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
73
+
74
+ // FR-018 M:N: `@cardinality: "many"` + `@through` — derive the junction FK
75
+ // columns from the junction's identity.reference children and register a
76
+ // many(junction) navigation on the source.
77
+ if (cardinality === CARDINALITY_MANY && child.ownAttr(RELATIONSHIP_ATTR_THROUGH) !== undefined) {
78
+ const m2m = buildM2mEntry(obj, child as MetaRelationship, root);
79
+ if (m2m) ensure(obj.name).push(m2m);
80
+ continue;
81
+ }
82
+
54
83
  if (cardinality !== CARDINALITY_ONE) continue;
55
84
 
56
85
  const targetEntityRaw = child.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
@@ -83,5 +112,78 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
83
112
  }
84
113
  }
85
114
 
115
+ // FR-018: junction entities reached via @through need their two belongs-to
116
+ // one() sides so the through-table is navigable in the Drizzle relational
117
+ // query API (db.query.posts.findMany({ with: { tags: { with: { tag: true }}}})).
118
+ // A junction is any entity named by some M:N relationship's @through.
119
+ for (const junctionName of collectJunctionNames(root)) {
120
+ const junction = root.findObject(junctionName);
121
+ if (!junction) continue;
122
+ const entries = ensure(junctionName);
123
+ for (const ref of junction.referenceIdentities()) {
124
+ const targetRaw = ref.targetEntity;
125
+ const fkField = ref.fields[0];
126
+ if (!targetRaw || !fkField) continue;
127
+ const targetEntity = stripPackage(targetRaw);
128
+ // The relation member is named after the target entity (camel singular);
129
+ // multiple references to the same entity (self-join junction) are
130
+ // disambiguated by the FK field name.
131
+ const refName = ref.name && ref.name.length > 0
132
+ ? ref.name
133
+ : variableNameFromEntity(targetEntity);
134
+ entries.push({
135
+ name: refName,
136
+ cardinality: "one",
137
+ targetEntity,
138
+ fkField,
139
+ targetPkField: "id",
140
+ });
141
+ }
142
+ }
143
+
86
144
  return result;
87
145
  }
146
+
147
+ /** Names of all entities that are the `@through` junction of some M:N relationship. */
148
+ function collectJunctionNames(root: MetaRoot): Set<string> {
149
+ const names = new Set<string>();
150
+ for (const obj of root.objects()) {
151
+ for (const rel of obj.relationships()) {
152
+ if (rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
153
+ const through = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
154
+ if (through) names.add(stripPackage(through));
155
+ }
156
+ }
157
+ return names;
158
+ }
159
+
160
+ /**
161
+ * Build the source-side M:N navigation entry: derive the junction FK fields from
162
+ * the junction's two identity.reference children (the SSOT), handling hetero /
163
+ * directed-self-join / symmetric. Returns null (skips the entry) if derivation
164
+ * fails — the loader validation pass surfaces the actionable error separately.
165
+ */
166
+ function buildM2mEntry(
167
+ source: MetaObject,
168
+ rel: MetaRelationship,
169
+ root: MetaRoot,
170
+ ): RelationEntry | null {
171
+ const targetRaw = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
172
+ const throughRaw = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
173
+ if (!targetRaw || !throughRaw) return null;
174
+ let fields;
175
+ try {
176
+ fields = deriveM2MFields(rel, source, root);
177
+ } catch {
178
+ return null;
179
+ }
180
+ return {
181
+ name: rel.name,
182
+ cardinality: "many",
183
+ targetEntity: stripPackage(targetRaw),
184
+ junctionEntity: stripPackage(throughRaw),
185
+ sourceJoinField: fields.sourceField,
186
+ targetJoinField: fields.targetField,
187
+ symmetric: rel.symmetric,
188
+ };
189
+ }
@@ -6,6 +6,7 @@ import type { PkInfo } from "./pk-resolver.js";
6
6
  import type { RelationMap } from "./relation-resolver.js";
7
7
  import type { ColumnNamingStrategy } from "./metaobjects-config.js";
8
8
  import type { OutputLayout, ResolvedTarget } from "./import-path.js";
9
+ import { variableNameFromEntity } from "./naming.js";
9
10
 
10
11
  /**
11
12
  * How to format cross-entity import specifiers in generated files.
@@ -37,12 +38,26 @@ export interface RenderContext {
37
38
  extStyle: ExtStyle;
38
39
  /** Column naming strategy: how field names map to DB column names. Defaults to "snake_case". */
39
40
  columnNamingStrategy: ColumnNamingStrategy;
41
+ /**
42
+ * Drizzle timestamp column mode. "string" (default) types timestamp columns as
43
+ * ISO-8601 strings (matching the generated Zod + cross-port wire contract);
44
+ * "date" uses drizzle's native JS-Date mode (for consumers whose hand-written
45
+ * code works with `Date`). Opt in via `codegen.timestampMode`.
46
+ */
47
+ timestampMode: "date" | "string";
40
48
  /** Path prefix applied to generated route registrations + hook fetch URLs. Defaults to "". */
41
49
  apiPrefix: string;
42
50
  /** Whether abstract entities emit their shape artifact (type-only interface / value-object file). Defaults to true. Instance/write artifacts are never emitted for abstract entities regardless. */
43
51
  emitAbstractShapes: boolean;
44
52
  /** Output layout mode: "flat" (default) — all files in outDir; "package" — sub-paths from entity metadata package. */
45
53
  outputLayout: OutputLayout;
54
+ /**
55
+ * Resolve an entity name to its Drizzle collection (table) variable name,
56
+ * applying the project's pluralization config + per-entity overrides. Every
57
+ * template that emits or references a table var goes through this so the
58
+ * declaration and all references agree. Defaults to always-pluralize.
59
+ */
60
+ collectionName: (entityName: string) => string;
46
61
  /** The target THIS generator emits to (drives path layout + same-target imports). */
47
62
  selfTarget: ResolvedTarget;
48
63
  /** Where entity files live (drives cross-target entity imports). */
@@ -52,19 +67,28 @@ export interface RenderContext {
52
67
  relationMap: RelationMap;
53
68
  /** Entity name → its metadata package (undefined if the entity has no package). Built once per run. */
54
69
  packageOf: Map<string, string | undefined>;
70
+ /** FR-019: module specifier to import externally-PROVIDED shared enums from
71
+ * (`@provided: true` declarations). Undefined when unset — referencing a
72
+ * provided enum without it is a codegen-time error. */
73
+ providedEnumModule?: string;
55
74
  }
56
75
 
57
- /** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). */
58
- export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget"> & {
76
+ /** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). `collectionName` is built from `pluralizeCollections` + `collectionNameOverrides` (both default to always-pluralize). */
77
+ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
59
78
  extStyle?: ExtStyle;
60
79
  omImport?: string;
61
80
  columnNamingStrategy?: ColumnNamingStrategy;
81
+ timestampMode?: "date" | "string";
62
82
  apiPrefix?: string;
63
83
  emitAbstractShapes?: boolean;
64
84
  outputLayout?: OutputLayout;
65
85
  packageOf?: Map<string, string | undefined>;
66
86
  selfTarget?: ResolvedTarget;
67
87
  entityModuleTarget?: ResolvedTarget;
88
+ /** Auto-pluralize collection (table) variable names. Default true. */
89
+ pluralizeCollections?: boolean;
90
+ /** Per-entity exact collection-var-name overrides, keyed by bare entity name. */
91
+ collectionNameOverrides?: Record<string, string>;
68
92
  };
69
93
 
70
94
  /** Append the configured extension to a cross-entity module specifier. */
@@ -82,16 +106,22 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
82
106
  outputLayout,
83
107
  dbImport: opts.dbImport,
84
108
  };
109
+ const collectionNameOpts = {
110
+ pluralize: opts.pluralizeCollections ?? true,
111
+ overrides: opts.collectionNameOverrides ?? {},
112
+ };
85
113
  return {
86
114
  ...opts,
87
115
  extStyle: opts.extStyle ?? "none",
88
116
  omImport: opts.omImport ?? "../index",
89
117
  columnNamingStrategy: opts.columnNamingStrategy ?? "snake_case",
118
+ timestampMode: opts.timestampMode ?? "string",
90
119
  apiPrefix: opts.apiPrefix ?? "",
91
120
  emitAbstractShapes: opts.emitAbstractShapes ?? true,
92
121
  outputLayout,
93
122
  packageOf: opts.packageOf ?? new Map(),
94
123
  selfTarget: defaultTarget,
95
124
  entityModuleTarget: opts.entityModuleTarget ?? defaultTarget,
125
+ collectionName: (entityName: string) => variableNameFromEntity(entityName, collectionNameOpts),
96
126
  };
97
127
  }
@@ -0,0 +1,14 @@
1
+ // @generated from templates/*/*.mustache — DO NOT EDIT.
2
+ // Regenerate: bun run scripts/generate-embedded-templates.ts (or scripts/sync-doc-templates.sh).
3
+ //
4
+ // Embedded framework doc templates so they resolve inside the
5
+ // `bun build --compile` standalone `meta` binary, where the on-disk
6
+ // `templates/` directory is unavailable. Keys are provider resolve refs
7
+ // (path under templates/ minus the .mustache suffix).
8
+ export const EMBEDDED_FRAMEWORK_TEMPLATES: Record<string, string> = {
9
+ "api/agent-api.md": "{{{generatedMarker}}}\n\n# {{title}}\n\nGenerated API reference for {{project}}; call these exactly as written. {{importNote}}\n{{#hasSetup}}\n\n## Setup\n{{#setup}}\n- `{{handle}}` — {{{note}}} `{{{snippetInline}}}`\n{{/setup}}\n{{/hasSetup}}\n{{#units}}\n\n## {{node}}\n{{#groups}}\n\n`{{importHeader}}`\n{{#symbols}}\n- `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}\n{{/symbols}}\n{{/groups}}\n{{#example}}\n\nExample:\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/units}}\n",
10
+ "api/entity-api.md": "{{{generatedMarker}}}\n\n# {{node}} API\n{{#modelPageHref}}\n\n**Model / metadata:** [{{node}}]({{modelPageHref}})\n{{/modelPageHref}}\n\n> Import paths are relative to your generated-output directory.\n{{#hasSetup}}\n\n## Setup\n\nObtain the runtime handles the calls below need:\n{{#setup}}\n\n- `{{handle}}` — {{{note}}}\n\n```ts\n{{{snippet}}}\n```\n{{/setup}}\n{{/hasSetup}}\n{{#unitExample}}\n\n## Example\n\n```ts\n{{{unitExample}}}\n```\n{{/unitExample}}\n{{#sections}}\n\n## {{heading}}\n{{#symbols}}\n\n### `{{signature}}`\n\n{{usage}}\n\n```ts\n{{importLine}}\n```\n{{#hasFields}}\n\n{{fieldsCaption}}:\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n{{#fieldRows}}\n| `{{field}}` | `{{{type}}}` | {{required}} | {{notes}} |\n{{/fieldRows}}\n{{/hasFields}}\n{{#mountNote}}\n\nMount: {{{mountNote}}}\n{{/mountNote}}\n{{#throws}}\n\nThrows: {{throws}}\n{{/throws}}\n{{#example}}\n\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/symbols}}\n{{/sections}}\n",
11
+ "api/index.md": "{{{generatedMarker}}}\n\n# {{title}}\n\n{{intro}}\n{{#hasEntities}}\n\n## Entities\n\n{{#entities}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/entities}}\n{{/hasEntities}}\n{{#hasTemplates}}\n\n## Templates\n\n{{#templates}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/templates}}\n{{/hasTemplates}}\n",
12
+ "docs/entity-page.md": "{{{generatedMarker}}}\n\n# {{entity.name}}\n{{#summaryLead}}\n\n{{{.}}}\n{{/summaryLead}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n{{#apiRefs.0}}\n\n**API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}\n{{/apiRefs.0}}\n\n{{{preambleHeader}}}\n{{#hasIdentities}}\n\n## Identity\n\n{{#identities}}\n- {{{bullet}}}\n{{/identities}}\n{{/hasIdentities}}\n{{#hasNeighborhoodEr}}\n\n## In context\n\n{{{neighborhoodErBlock}}}\n{{/hasNeighborhoodEr}}\n{{#fields.hasFields}}\n\n## Fields\n\n| Field | Type | Required | Column | Rules |\n|---|---|---|---|---|\n{{#fields.rows}}\n| {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |\n{{/fields.rows}}\n{{/fields.hasFields}}\n{{#fieldDetails.hasDetails}}\n\n## Field details\n\n{{#fieldDetails.rows}}\n{{{block}}}\n\n{{/fieldDetails.rows}}\n{{/fieldDetails.hasDetails}}\n{{#hasRelationships}}\n\n## Relationships\n\n{{#relationships}}\n- {{{bullet}}}\n{{/relationships}}\n{{/hasRelationships}}\n{{#hasUsedBy}}\n\n## Used by\n\n{{#usedBy}}\n- {{{bullet}}}\n{{/usedBy}}\n{{/hasUsedBy}}\n",
13
+ "docs/template-page.md": "{{{generatedMarker}}}\n\n# {{name}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n\n**Kind:** {{kind}}\n\n## Output\n{{^isEmail}}\n\n- Format: `{{format}}`\n{{/isEmail}}\n{{#isEmail}}\n\nMultipart email — rendered as the following parts:\n\n| Part | Source | Format | Escaping |\n|---|---|---|---|\n{{#parts}}\n| {{label}} | `{{ref}}` | `{{format}}` | {{#escaped}}escaped{{/escaped}}{{^escaped}}raw{{/escaped}} |\n{{/parts}}\n{{/isEmail}}\n\n## Input\n\n- Payload: [`{{payload.name}}`]({{payload.link}})\n{{#hasRequiredTags}}\n- Required fields:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Render contract\n\n- Every field referenced by the template is validated against the payload at generation time; an unknown field fails generation.\n{{#maxChars}}\n- Maximum length: {{.}} characters (rendering longer output fails).\n{{/maxChars}}\n{{#hasRequiredTags}}\n- Required tags must be present:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Source\n\n{{#sourceRefs}}\n- `{{.}}`\n{{/sourceRefs}}\n{{#templateSourceSection}}\n\n{{{.}}}\n{{/templateSourceSection}}\n\n## Capability\n\n{{capability}}\n",
14
+ };
@@ -16,6 +16,7 @@ import type { Provider } from "@metaobjectsdev/render";
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import { join, resolve, dirname } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
+ import { EMBEDDED_FRAMEWORK_TEMPLATES } from "./embedded-templates.generated.js";
19
20
 
20
21
  /** Canonical shipped template — used to verify a candidate framework
21
22
  * templates directory actually contains our defaults. Without this check a
@@ -31,8 +32,11 @@ const CANONICAL_TEMPLATE_REL = "docs/entity-page.md.mustache";
31
32
  *
32
33
  * Returns `undefined` when no on-disk templates dir can be found — e.g. inside
33
34
  * the `bun build --compile` standalone binary, whose `import.meta.url` is a
34
- * `/$bunfs/root` virtual path with no real `package.json` alongside it. The
35
- * embedded-template fallback (see `FileSystemProvider`) covers that case. */
35
+ * `/$bunfs/root` virtual path with no real `package.json` alongside it. In that
36
+ * case `FrameworkTemplatesProvider.resolve` falls back to the bundled
37
+ * `EMBEDDED_FRAMEWORK_TEMPLATES` (embedded-templates.generated.ts), a plain
38
+ * string module generated from the canonical templates/docs/*.mustache and
39
+ * compiled into the binary. */
36
40
  function findFrameworkTemplatesDir(start: string): string | undefined {
37
41
  let dir = start;
38
42
  while (true) {
@@ -81,23 +85,33 @@ export class FileSystemProvider implements Provider {
81
85
  }
82
86
 
83
87
  /** The framework defaults provider — resolves refs against codegen-ts's own
84
- * on-disk `templates/` directory.
88
+ * on-disk `templates/` directory, falling back to the bundled
89
+ * `EMBEDDED_FRAMEWORK_TEMPLATES` when no on-disk dir exists.
85
90
  *
86
91
  * Resolution is lazy: the directory is located on first `resolve()`, not at
87
92
  * module import. This keeps merely importing this module side-effect-free,
88
93
  * which matters for the `bun build --compile` standalone `meta` binary — its
89
94
  * `import.meta.url` is a `/$bunfs/root` virtual path with no on-disk
90
- * `templates/` dir, so eager resolution at import time used to throw before
91
- * any command (even `--help` or the schema ops `migrate`/`verify --db`) could
92
- * run. Now non-codegen commands import cleanly; only the codegen doc path
93
- * (which the standalone binary doesn't target) needs the on-disk dir. */
95
+ * `templates/` dir.
96
+ *
97
+ * ON-DISK FIRST, then embedded: the source/install layout (and adopter
98
+ * overrides chained ahead of this provider via `projectProvider`) always wins,
99
+ * so local edits to the shipped package `templates/` still take effect. Only
100
+ * when the on-disk dir is unresolved (the compiled binary) — or a ref the dir
101
+ * doesn't contain — do we consult the embedded map, which is generated from
102
+ * the same canonical templates and compiled into the binary. Unknown refs are
103
+ * `undefined` in the map, which is the correct miss. */
94
104
  class FrameworkTemplatesProvider implements Provider {
95
105
  resolve(ref: string): string | undefined {
106
+ // On-disk first: dev/install layout, plus shipped-package edits.
96
107
  const dir = frameworkTemplatesDir();
97
- if (dir === undefined) return undefined;
98
- const path = join(dir, `${ref}.mustache`);
99
- if (!existsSync(path)) return undefined;
100
- return readFileSync(path, "utf-8");
108
+ if (dir !== undefined) {
109
+ const path = join(dir, `${ref}.mustache`);
110
+ if (existsSync(path)) return readFileSync(path, "utf-8");
111
+ }
112
+ // Embedded fallback: the binary case (no on-disk dir) or a ref the on-disk
113
+ // dir doesn't carry. `undefined` for unknown refs is the correct miss.
114
+ return EMBEDDED_FRAMEWORK_TEMPLATES[ref];
101
115
  }
102
116
  }
103
117
 
package/src/runner.ts CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  * from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
21
21
  const VALID_ENTITY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
22
 
23
+ /** ADR-0025: doc generators whose single door is `meta docs`. If a `meta gen`
24
+ * config still lists one (by its stable `name`), the runner warns + skips it. */
25
+ const DEPRECATED_DOC_GENERATORS = new Set(["docs-file", "api-docs"]);
26
+
23
27
  export interface RunGenOpts {
24
28
  config: MetaobjectsGenConfig;
25
29
  metadata: MetaData;
@@ -143,9 +147,24 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
143
147
  root.objects().map((o) => [o.name, o.package]),
144
148
  );
145
149
 
150
+ // Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
151
+ // surface it on every generator's ctx.config so api-docs documents the Hono
152
+ // CRUD surface it actually emits (rather than silently omitting it).
153
+ const includeHonoRoutes = config.generators.some((g) => g.emitsHonoRoutes === true);
154
+
146
155
  // 4. Run each generator with a per-target render context; collect with full path.
147
156
  const emitted: { fullPath: string; content: string; generatedBy: string }[] = [];
148
157
  for (const generator of config.generators) {
158
+ // ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
159
+ // still lists a deprecated doc generator is warned + skipped, not run — the
160
+ // generator stays as `meta docs`'s internal engine.
161
+ if (DEPRECATED_DOC_GENERATORS.has(generator.name)) {
162
+ warnings.push(
163
+ `[${generator.name}] docs are produced by 'meta docs' (ADR-0025); ` +
164
+ `remove ${generator.name === "api-docs" ? "apiDocsFile()" : "docsFile()"} from generators. Skipped.`,
165
+ );
166
+ continue;
167
+ }
149
168
  const selfTarget = targetOf(generator);
150
169
  const renderContext = makeRenderContext({
151
170
  dialect: config.dialect,
@@ -154,6 +173,9 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
154
173
  dbImport: selfTarget.dbImport,
155
174
  extStyle: config.extStyle,
156
175
  columnNamingStrategy: config.columnNamingStrategy,
176
+ pluralizeCollections: config.pluralizeCollections,
177
+ collectionNameOverrides: config.collectionNameOverrides,
178
+ timestampMode: config.timestampMode,
157
179
  apiPrefix: config.apiPrefix,
158
180
  emitAbstractShapes: config.emitAbstractShapes,
159
181
  outputLayout: selfTarget.outputLayout,
@@ -162,6 +184,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
162
184
  packageOf,
163
185
  selfTarget,
164
186
  entityModuleTarget,
187
+ ...(config.providedEnumModule !== undefined && { providedEnumModule: config.providedEnumModule }),
165
188
  });
166
189
  const ctx: GenContext = {
167
190
  entities: safeEntities,
@@ -173,6 +196,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
173
196
  dbImport: selfTarget.dbImport,
174
197
  dialect: config.dialect,
175
198
  outputLayout: selfTarget.outputLayout,
199
+ includeHonoRoutes,
176
200
  },
177
201
  renderContext,
178
202
  ...(projectRoot !== undefined && { projectRoot }),