@metaobjectsdev/codegen-ts 0.24.4 → 0.25.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 (265) hide show
  1. package/README.md +37 -12
  2. package/dist/api-surface.d.ts +29 -0
  3. package/dist/api-surface.d.ts.map +1 -1
  4. package/dist/api-surface.js +47 -0
  5. package/dist/api-surface.js.map +1 -1
  6. package/dist/client-directive.d.ts +18 -0
  7. package/dist/client-directive.d.ts.map +1 -0
  8. package/dist/client-directive.js +37 -0
  9. package/dist/client-directive.js.map +1 -0
  10. package/dist/column-mapper.d.ts +12 -0
  11. package/dist/column-mapper.d.ts.map +1 -1
  12. package/dist/column-mapper.js +1 -0
  13. package/dist/column-mapper.js.map +1 -1
  14. package/dist/constants.d.ts +24 -11
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +54 -19
  17. package/dist/constants.js.map +1 -1
  18. package/dist/db-emitting.d.ts +6 -0
  19. package/dist/db-emitting.d.ts.map +1 -0
  20. package/dist/db-emitting.js +26 -0
  21. package/dist/db-emitting.js.map +1 -0
  22. package/dist/generator-registry.d.ts.map +1 -1
  23. package/dist/generator-registry.js +7 -1
  24. package/dist/generator-registry.js.map +1 -1
  25. package/dist/generator.d.ts +5 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/agent-docs-file.d.ts +19 -0
  29. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  30. package/dist/generators/agent-docs-file.js +257 -0
  31. package/dist/generators/agent-docs-file.js.map +1 -0
  32. package/dist/generators/agent-requirements-page.d.ts +22 -0
  33. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  34. package/dist/generators/agent-requirements-page.js +175 -0
  35. package/dist/generators/agent-requirements-page.js.map +1 -0
  36. package/dist/generators/agent-schema-input.d.ts +69 -0
  37. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-input.js +23 -0
  39. package/dist/generators/agent-schema-input.js.map +1 -0
  40. package/dist/generators/agent-schema-page.d.ts +21 -0
  41. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  42. package/dist/generators/agent-schema-page.js +241 -0
  43. package/dist/generators/agent-schema-page.js.map +1 -0
  44. package/dist/generators/agent-ui-page.d.ts +22 -0
  45. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  46. package/dist/generators/agent-ui-page.js +253 -0
  47. package/dist/generators/agent-ui-page.js.map +1 -0
  48. package/dist/generators/api-docs-file.d.ts.map +1 -1
  49. package/dist/generators/api-docs-file.js +13 -6
  50. package/dist/generators/api-docs-file.js.map +1 -1
  51. package/dist/generators/api-model.d.ts +6 -0
  52. package/dist/generators/api-model.d.ts.map +1 -1
  53. package/dist/generators/api-model.js +54 -35
  54. package/dist/generators/api-model.js.map +1 -1
  55. package/dist/generators/callable-file.js +1 -1
  56. package/dist/generators/callable-file.js.map +1 -1
  57. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  58. package/dist/generators/docs-data-builder.js +47 -2
  59. package/dist/generators/docs-data-builder.js.map +1 -1
  60. package/dist/generators/docs-data.d.ts +6 -2
  61. package/dist/generators/docs-data.d.ts.map +1 -1
  62. package/dist/generators/index.d.ts +3 -0
  63. package/dist/generators/index.d.ts.map +1 -1
  64. package/dist/generators/index.js +4 -0
  65. package/dist/generators/index.js.map +1 -1
  66. package/dist/generators/names-file.d.ts +17 -0
  67. package/dist/generators/names-file.d.ts.map +1 -0
  68. package/dist/generators/names-file.js +95 -0
  69. package/dist/generators/names-file.js.map +1 -0
  70. package/dist/generators/requirements-markdown.d.ts +18 -1
  71. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  72. package/dist/generators/requirements-markdown.js +11 -9
  73. package/dist/generators/requirements-markdown.js.map +1 -1
  74. package/dist/generators/routes-file-hono.d.ts +17 -2
  75. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  76. package/dist/generators/routes-file-hono.js +15 -12
  77. package/dist/generators/routes-file-hono.js.map +1 -1
  78. package/dist/generators/routes-file.d.ts +17 -2
  79. package/dist/generators/routes-file.d.ts.map +1 -1
  80. package/dist/generators/routes-file.js +10 -8
  81. package/dist/generators/routes-file.js.map +1 -1
  82. package/dist/generators/template-generator.d.ts.map +1 -1
  83. package/dist/generators/template-generator.js +20 -6
  84. package/dist/generators/template-generator.js.map +1 -1
  85. package/dist/index.d.ts +21 -5
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +40 -4
  88. package/dist/index.js.map +1 -1
  89. package/dist/metaobjects-config.d.ts +67 -1
  90. package/dist/metaobjects-config.d.ts.map +1 -1
  91. package/dist/metaobjects-config.js +10 -1
  92. package/dist/metaobjects-config.js.map +1 -1
  93. package/dist/names.d.ts +275 -0
  94. package/dist/names.d.ts.map +1 -0
  95. package/dist/names.js +434 -0
  96. package/dist/names.js.map +1 -0
  97. package/dist/naming.d.ts +17 -0
  98. package/dist/naming.d.ts.map +1 -1
  99. package/dist/naming.js +21 -0
  100. package/dist/naming.js.map +1 -1
  101. package/dist/overwrite-policy.d.ts.map +1 -1
  102. package/dist/overwrite-policy.js +15 -3
  103. package/dist/overwrite-policy.js.map +1 -1
  104. package/dist/payload-codegen.d.ts.map +1 -1
  105. package/dist/payload-codegen.js +4 -3
  106. package/dist/payload-codegen.js.map +1 -1
  107. package/dist/projection/extract-view-spec.d.ts +25 -1
  108. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  109. package/dist/projection/extract-view-spec.js +79 -6
  110. package/dist/projection/extract-view-spec.js.map +1 -1
  111. package/dist/prompt-generator-gate.d.ts +27 -0
  112. package/dist/prompt-generator-gate.d.ts.map +1 -0
  113. package/dist/prompt-generator-gate.js +62 -0
  114. package/dist/prompt-generator-gate.js.map +1 -0
  115. package/dist/reference-templates.d.ts +22 -5
  116. package/dist/reference-templates.d.ts.map +1 -1
  117. package/dist/reference-templates.js +55 -21
  118. package/dist/reference-templates.js.map +1 -1
  119. package/dist/render-context.d.ts +20 -1
  120. package/dist/render-context.d.ts.map +1 -1
  121. package/dist/render-context.js +2 -0
  122. package/dist/render-context.js.map +1 -1
  123. package/dist/retired-codegen-attrs.d.ts +32 -0
  124. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  125. package/dist/retired-codegen-attrs.js +47 -0
  126. package/dist/retired-codegen-attrs.js.map +1 -0
  127. package/dist/routes-expose.d.ts +63 -0
  128. package/dist/routes-expose.d.ts.map +1 -0
  129. package/dist/routes-expose.js +57 -0
  130. package/dist/routes-expose.js.map +1 -0
  131. package/dist/runner.d.ts +18 -0
  132. package/dist/runner.d.ts.map +1 -1
  133. package/dist/runner.js +115 -7
  134. package/dist/runner.js.map +1 -1
  135. package/dist/templates/callable-file.d.ts +7 -2
  136. package/dist/templates/callable-file.d.ts.map +1 -1
  137. package/dist/templates/callable-file.js +54 -9
  138. package/dist/templates/callable-file.js.map +1 -1
  139. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  140. package/dist/templates/drizzle-schema.js +236 -65
  141. package/dist/templates/drizzle-schema.js.map +1 -1
  142. package/dist/templates/entity-constants.d.ts +15 -6
  143. package/dist/templates/entity-constants.d.ts.map +1 -1
  144. package/dist/templates/entity-constants.js +85 -161
  145. package/dist/templates/entity-constants.js.map +1 -1
  146. package/dist/templates/entity-file.d.ts.map +1 -1
  147. package/dist/templates/entity-file.js +38 -4
  148. package/dist/templates/entity-file.js.map +1 -1
  149. package/dist/templates/entity-meta-file.d.ts +15 -1
  150. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  151. package/dist/templates/entity-meta-file.js +12 -2
  152. package/dist/templates/entity-meta-file.js.map +1 -1
  153. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  154. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  155. package/dist/templates/entity-ui-descriptor.js +238 -0
  156. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  157. package/dist/templates/field-meta.d.ts +46 -9
  158. package/dist/templates/field-meta.d.ts.map +1 -1
  159. package/dist/templates/field-meta.js +95 -21
  160. package/dist/templates/field-meta.js.map +1 -1
  161. package/dist/templates/names-decl.d.ts +38 -0
  162. package/dist/templates/names-decl.d.ts.map +1 -0
  163. package/dist/templates/names-decl.js +104 -0
  164. package/dist/templates/names-decl.js.map +1 -0
  165. package/dist/templates/output-parser.d.ts.map +1 -1
  166. package/dist/templates/output-parser.js +54 -8
  167. package/dist/templates/output-parser.js.map +1 -1
  168. package/dist/templates/projection-decl.d.ts +21 -0
  169. package/dist/templates/projection-decl.d.ts.map +1 -1
  170. package/dist/templates/projection-decl.js +53 -29
  171. package/dist/templates/projection-decl.js.map +1 -1
  172. package/dist/templates/queries-file.d.ts.map +1 -1
  173. package/dist/templates/queries-file.js +17 -2
  174. package/dist/templates/queries-file.js.map +1 -1
  175. package/dist/templates/render-helper.d.ts.map +1 -1
  176. package/dist/templates/render-helper.js +3 -1
  177. package/dist/templates/render-helper.js.map +1 -1
  178. package/dist/templates/requirement-test.d.ts.map +1 -1
  179. package/dist/templates/requirement-test.js +48 -21
  180. package/dist/templates/requirement-test.js.map +1 -1
  181. package/dist/templates/routes-file-hono.d.ts +2 -1
  182. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  183. package/dist/templates/routes-file-hono.js +11 -2
  184. package/dist/templates/routes-file-hono.js.map +1 -1
  185. package/dist/templates/routes-file.d.ts +2 -1
  186. package/dist/templates/routes-file.d.ts.map +1 -1
  187. package/dist/templates/routes-file.js +54 -21
  188. package/dist/templates/routes-file.js.map +1 -1
  189. package/dist/templates/value-object-file.d.ts.map +1 -1
  190. package/dist/templates/value-object-file.js +15 -1
  191. package/dist/templates/value-object-file.js.map +1 -1
  192. package/dist/templates/view-decl.d.ts +37 -1
  193. package/dist/templates/view-decl.d.ts.map +1 -1
  194. package/dist/templates/view-decl.js +27 -3
  195. package/dist/templates/view-decl.js.map +1 -1
  196. package/dist/templates/zod-validators.d.ts +21 -0
  197. package/dist/templates/zod-validators.d.ts.map +1 -1
  198. package/dist/templates/zod-validators.js +40 -5
  199. package/dist/templates/zod-validators.js.map +1 -1
  200. package/dist/view-context.d.ts +24 -0
  201. package/dist/view-context.d.ts.map +1 -0
  202. package/dist/view-context.js +81 -0
  203. package/dist/view-context.js.map +1 -0
  204. package/package.json +6 -6
  205. package/src/api-surface.ts +53 -0
  206. package/src/client-directive.ts +36 -0
  207. package/src/column-mapper.ts +13 -0
  208. package/src/constants.ts +91 -16
  209. package/src/db-emitting.ts +31 -0
  210. package/src/generator-registry.ts +7 -0
  211. package/src/generator.ts +5 -0
  212. package/src/generators/agent-docs-file.ts +297 -0
  213. package/src/generators/agent-requirements-page.ts +199 -0
  214. package/src/generators/agent-schema-input.ts +88 -0
  215. package/src/generators/agent-schema-page.ts +297 -0
  216. package/src/generators/agent-ui-page.ts +290 -0
  217. package/src/generators/api-docs-file.ts +13 -6
  218. package/src/generators/api-model.ts +63 -34
  219. package/src/generators/callable-file.ts +1 -1
  220. package/src/generators/docs-data-builder.ts +46 -2
  221. package/src/generators/docs-data.ts +6 -2
  222. package/src/generators/index.ts +10 -0
  223. package/src/generators/names-file.ts +98 -0
  224. package/src/generators/requirements-markdown.ts +36 -2
  225. package/src/generators/routes-file-hono.ts +26 -15
  226. package/src/generators/routes-file.ts +20 -7
  227. package/src/generators/template-generator.ts +20 -6
  228. package/src/index.ts +63 -5
  229. package/src/metaobjects-config.ts +78 -2
  230. package/src/names.ts +630 -0
  231. package/src/naming.ts +23 -0
  232. package/src/overwrite-policy.ts +15 -3
  233. package/src/payload-codegen.ts +4 -3
  234. package/src/projection/extract-view-spec.ts +85 -6
  235. package/src/prompt-generator-gate.ts +73 -0
  236. package/src/reference/barrel.ts +3 -0
  237. package/src/reference/entity.ts +28 -2
  238. package/src/reference/names.ts +125 -0
  239. package/src/reference/queries.ts +14 -2
  240. package/src/reference/routes-hono.ts +113 -0
  241. package/src/reference/routes.ts +30 -9
  242. package/src/reference-templates.ts +61 -21
  243. package/src/render-context.ts +22 -1
  244. package/src/retired-codegen-attrs.ts +52 -0
  245. package/src/routes-expose.ts +86 -0
  246. package/src/runner.ts +129 -11
  247. package/src/templates/callable-file.ts +60 -8
  248. package/src/templates/drizzle-schema.ts +248 -60
  249. package/src/templates/entity-constants.ts +95 -184
  250. package/src/templates/entity-file.ts +46 -4
  251. package/src/templates/entity-meta-file.ts +16 -3
  252. package/src/templates/entity-ui-descriptor.ts +305 -0
  253. package/src/templates/field-meta.ts +100 -19
  254. package/src/templates/names-decl.ts +165 -0
  255. package/src/templates/output-parser.ts +54 -7
  256. package/src/templates/projection-decl.ts +72 -31
  257. package/src/templates/queries-file.ts +17 -2
  258. package/src/templates/render-helper.ts +3 -1
  259. package/src/templates/requirement-test.ts +49 -18
  260. package/src/templates/routes-file-hono.ts +14 -2
  261. package/src/templates/routes-file.ts +69 -24
  262. package/src/templates/value-object-file.ts +15 -1
  263. package/src/templates/view-decl.ts +53 -3
  264. package/src/templates/zod-validators.ts +41 -5
  265. package/src/view-context.ts +88 -0
@@ -610,10 +610,22 @@ export function decideAndWrite(
610
610
  return {
611
611
  path,
612
612
  status: "refused",
613
+ // The hint states WHAT HAPPENED, per file, and points at the recovery rather
614
+ // than restating it. The two remedies it used to name were both WRONG for the
615
+ // generator with the most at stake: `requirementTests()` emits a stub whose
616
+ // entire value is the body you write over it, and whose own header says "do not
617
+ // rename the test — the name is the link", so "move your edits into a
618
+ // non-generated file" asks for something the artifact forbids, and
619
+ // "--baseline=fresh to discard them" discards the only content the file has.
620
+ // The sequence that actually keeps an edit is three commands long and identical
621
+ // for every file in the run, so `runner.ts` prints it once; a doc pointer is
622
+ // what a direct `decideAndWrite` caller (a public export — no runner involved)
623
+ // gets instead, and it is never wrong.
613
624
  conflictHint: kase.hasRecord
614
- ? "this file has been edited since it was generated it was NOT " +
615
- "overwritten. Move your edits into a non-generated file, or re-run " +
616
- "with --baseline=fresh to discard them and adopt fresh output."
625
+ ? "this file has been edited since it was generated, and there is no " +
626
+ ".gen-state snapshot body on this machine to merge against it was NOT " +
627
+ "overwritten, and your version is intact on disk. Recovery: " +
628
+ "docs/features/own-your-codegen.md."
617
629
  : "no record of generating this file, and its content differs from fresh " +
618
630
  "output — it was NOT overwritten. Move it aside, or re-run with " +
619
631
  "--baseline=fresh to overwrite it and adopt fresh output as the baseline.",
@@ -41,6 +41,7 @@ import {
41
41
  stripPackage,
42
42
  } from "@metaobjectsdev/metadata";
43
43
  import { enumValues } from "./enum-meta.js";
44
+ import { templateSymbolBase } from "./naming.js";
44
45
  import { enumUnionAliasName, enumUnionString } from "./templates/inferred-types.js";
45
46
  import { assignEmittedNames } from "./naming/collision-names.js";
46
47
 
@@ -258,9 +259,9 @@ export function generatePayloadInterfacesBatch(
258
259
  return out.length === 0 ? "" : out.join("\n\n") + "\n";
259
260
  }
260
261
 
261
- function pascal(s: string): string {
262
- return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
263
- }
262
+ // Delegates to the shared authority so this emitter cannot drift from the parser and
263
+ // render-helper again (naming.ts / templateSymbolBase).
264
+ const pascal = templateSymbolBase;
264
265
 
265
266
  /** Emit a typed render handle binding a template's @textRef + @format and typing its payload. */
266
267
  export function generateRenderHandle(root: MetaData, templateName: string): string {
@@ -6,6 +6,8 @@ import {
6
6
  TYPE_RELATIONSHIP,
7
7
  isMetaObject,
8
8
  isReadOnlySource,
9
+ SOURCE_ROLE_PRIMARY,
10
+ SOURCE_ATTR_SCHEMA,
9
11
  ORIGIN_SUBTYPE_PASSTHROUGH,
10
12
  ORIGIN_SUBTYPE_AGGREGATE,
11
13
  ORIGIN_SUBTYPE_COMPUTED,
@@ -49,7 +51,7 @@ import {
49
51
  resolveObjectRef,
50
52
  type AggregateFunction,
51
53
  } from "@metaobjectsdev/metadata";
52
- import { type MetaData, type MetaField, type MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
54
+ import { type MetaData, type MetaField, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
53
55
  import { intValueMapOf } from "../enum-meta.js";
54
56
  import {
55
57
  columnNameFromField,
@@ -331,16 +333,58 @@ function resolveHop(
331
333
  function viewName(projection: MetaObject, ctx: ExtractContext): string {
332
334
  // The read-only source carries the physical view name. FR-016: physicalName
333
335
  // implements the four-step rule (kind-matching alias → legacy @table →
334
- // source.name → entity-name fallback), so the call below correctly resolves
335
- // @view / @materializedView / legacy @table for projection sources.
336
+ // source.name → entity-name fallback), so `viewSource.physicalName` below
337
+ // correctly resolves @view / @materializedView / legacy @table for projection
338
+ // sources.
336
339
  // ADR-0039: own — projection source classification (mirrors C# projection
337
- // OwnSources / IsReadOnlyProjection): the view name comes from the projection's
338
- // OWN read-only source, not one inherited via extends.
339
- const viewSource = projection.ownChildren().find(isReadOnlySource);
340
+ // OwnSources / IsReadOnlyProjection): the view name comes from the object's
341
+ // OWN read-only source, not one inherited via extends. `ERR_PROJECTION_
342
+ // INHERITED_SOURCE` (subtype-rules.ts) already refuses a concrete projection
343
+ // that would otherwise inherit a source, so own-only never drops a legally
344
+ // reachable source here.
345
+ //
346
+ // This function serves TWO shapes — a plain projection, and a write-through
347
+ // ENTITY's replica view (extractViewSpec's `writeThrough` branch, #213/#214;
348
+ // the parameter name is historical) — which need DIFFERENT selection rules,
349
+ // hence "prefer role:primary, else the first read-only source" rather than
350
+ // "require role:primary":
351
+ // - A plain projection: EVERY own source must be read-only-kind
352
+ // (ERR_PROJECTION_SOURCE_WRITABLE), so if it has any sources at all,
353
+ // exactly one is role:"primary" (ERR_SOURCE_NO_PRIMARY /
354
+ // ERR_SOURCE_MULTIPLE_PRIMARY) and that one IS read-only — the primary
355
+ // branch always fires when there is a source.
356
+ // - A write-through entity: its role:"primary" source is the WRITABLE
357
+ // table (never read-only), so the primary branch never fires; its
358
+ // read-only companion (the replica view) is intentionally NOT primary.
359
+ // The fallback (first read-only own source) is what names it.
360
+ //
361
+ // Selecting the role:"primary" read-only source PREFERENTIALLY over
362
+ // declaration order (rather than "first read-only source in file order") is
363
+ // what makes this agree with `resolveTableName()` (the resolver every §A6
364
+ // site defers to via `resolveObjectNames`): a plain projection declaring a
365
+ // role:"replica" source before its role:"primary" one would otherwise bind a
366
+ // different physical view depending on whether the names generator happened
367
+ // to be active in the run.
368
+ const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
369
+ const viewSource =
370
+ readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
340
371
  const explicit = viewSource?.physicalName;
341
372
  // physicalName always returns a string; empty string means the source had
342
373
  // neither alias nor a name and the owning entity name was empty (impossible
343
374
  // for a real projection). Fall through to the helper anyway for safety.
375
+ //
376
+ // This fallback fires only when the object has NO own read-only source at
377
+ // all. It differs from `resolveTableName()`'s own no-source fallback
378
+ // (`v_<snake(name)>` here vs `pluralize(toSnakeCase(name))` there), but that
379
+ // difference is UNOBSERVABLE: whenever a sourceless object reaches this
380
+ // fallback, `resolveObjectNames()` also finds no primary source and returns
381
+ // `undefined` before ever calling `resolveTableName()` — so no §A6 site has
382
+ // a names constant to reference either way, and both the ON and OFF arms
383
+ // land on THIS function's literal on the identical no-source condition. A
384
+ // sourceless projection also fails `isProjection()` itself
385
+ // (projection-detector.ts requires a read-only-kind source), so codegen's
386
+ // dispatch never reaches `renderProjectionDecl`/this function for it in the
387
+ // first place — doubly unreachable.
344
388
  return explicit !== undefined && explicit !== ""
345
389
  ? explicit
346
390
  : viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
@@ -363,6 +407,41 @@ export function projectionViewName(
363
407
  return viewName(projection, { columnNamingStrategy });
364
408
  }
365
409
 
410
+ /**
411
+ * The `@schema` of the SAME source node {@link projectionViewName} names, or undefined.
412
+ *
413
+ * It re-uses that function's selection rule ("prefer role:primary read-only, else the first
414
+ * read-only own source") deliberately, rather than asking the object for its schema: for a
415
+ * write-through ENTITY the object's primary source is the WRITABLE table, so
416
+ * `resolveTableSchema` would hand back the table's schema and qualify the replica view with
417
+ * it. A view and the table it replicates need not live in the same schema, and a name paired
418
+ * with someone else's schema is worse than no schema at all — it is confidently wrong.
419
+ */
420
+ export function projectionViewSchema(projection: MetaObject): string | undefined {
421
+ const schema = projectionViewSource(projection)?.attr(SOURCE_ATTR_SCHEMA);
422
+ return typeof schema === "string" && schema !== "" ? schema : undefined;
423
+ }
424
+
425
+ /**
426
+ * The source node {@link projectionViewName} names — the ONE selection, so the name, the
427
+ * schema and the role cannot be picked by three functions that agree until they do not.
428
+ *
429
+ * Hoisted out of `projectionViewSchema`, which used to be the only place the rule was
430
+ * written twice-removed from `viewName`. The third caller is what forced it: the names
431
+ * artifact keys its sources by ROLE, so a consumer reaching for a write-through entity's
432
+ * replica view has to know which role that view plays, and guessing `"replica"` would be
433
+ * exactly the second derivation this file exists to avoid.
434
+ */
435
+ export function projectionViewSource(projection: MetaObject): MetaSource | undefined {
436
+ const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
437
+ return readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
438
+ }
439
+
440
+ /** The ROLE of that source — the key its entry has in `<Entity>Names.sources`. */
441
+ export function projectionViewRole(projection: MetaObject): string | undefined {
442
+ return projectionViewSource(projection)?.role;
443
+ }
444
+
366
445
  /**
367
446
  * FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
368
447
  * owner part of `<owner>.<child>...` resolved as an object. Mirrors the
@@ -0,0 +1,73 @@
1
+ import type { MetaData } from "@metaobjectsdev/metadata";
2
+ import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, TEMPLATE_ATTR_RESPONSE_REF } from "@metaobjectsdev/metadata";
3
+ import type { Generator } from "./generator.js";
4
+
5
+ /**
6
+ * Generators that turn a declared `template.*` into runnable prompt code. Names, not
7
+ * identities, because an adopter may wrap or re-export them (ADR-0034 scaffold-and-own
8
+ * makes owning a copy the encouraged path) and a wrapper keeps the name.
9
+ */
10
+ const PROMPT_GENERATOR_NAMES: ReadonlySet<string> = new Set([
11
+ "prompt-render",
12
+ "output-parser",
13
+ "render-helper",
14
+ "output-prompt",
15
+ ]);
16
+
17
+ /**
18
+ * A declared prompt with no prompt generator wired produced NOTHING and said NOTHING.
19
+ *
20
+ * `meta gen` emitted the payload value objects (they are `object.value` nodes, which the
21
+ * entity generator picks up) and stopped there: no `render<Name>()`, no parser, no
22
+ * response-format fragment. `meta verify` then reported "1 template(s) clean", which reads
23
+ * as confirmation that the prompt is fine. So the fourth pillar produced two type files and
24
+ * a green gate, and the adopter had neither a send side nor a receive side.
25
+ *
26
+ * The wiring IS documented — but only in the prompts skill's per-language reference
27
+ * fragment, which SKILL.md points at in its final line, while the skill body walks the
28
+ * entire declaration without once saying a generator is required. Found by declaring a
29
+ * `template.prompt` in a from-scratch app exactly as that skill teaches.
30
+ *
31
+ * This follows the `layout.dataGrid` precedent (#287, data-grid-gate.ts): tell the adopter
32
+ * at `meta gen` time rather than adding a doc line that gets missed the same way. It is a
33
+ * WARNING — the exit code is untouched — and it is **self-extinguishing**: wire any one
34
+ * prompt generator and it goes quiet forever, so a project that has made this choice
35
+ * deliberately is never nagged.
36
+ *
37
+ * It must live in the runner rather than in a generator, because the whole condition is
38
+ * that the generator which would speak up is not running.
39
+ */
40
+ export function warnMissingPromptGenerators(
41
+ root: MetaData,
42
+ generators: readonly Generator[],
43
+ warn: (msg: string) => void,
44
+ ): void {
45
+ if (generators.some((g) => PROMPT_GENERATOR_NAMES.has(g.name))) return;
46
+
47
+ // ADR-0039: resolving children — a template may arrive through an overlay or extends.
48
+ const templates = root
49
+ .children()
50
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
51
+ if (templates.length === 0) return;
52
+
53
+ const names = templates.map((t) => t.name).join(", ");
54
+ // A @responseRef is what asks for the inbound tier (ADR-0052), so a responding prompt
55
+ // is missing strictly more than an outbound-only one. Name that, rather than making the
56
+ // adopter infer which half is absent.
57
+ const responding = templates.filter((t) => typeof t.attr(TEMPLATE_ATTR_RESPONSE_REF) === "string");
58
+ const receiveHalf =
59
+ responding.length > 0
60
+ ? ` ${responding.length === templates.length ? "All" : `${responding.length} of them`} ` +
61
+ `declare a @responseRef, so the parser and response-format fragment are missing too — ` +
62
+ `add outputParser().`
63
+ : "";
64
+
65
+ warn(
66
+ `${templates.length} declared template.prompt (${names}) generated no prompt code: ` +
67
+ `no prompt generator is wired, so there is no render function to send one and nothing ` +
68
+ `read a reply. Add promptRender() to \`generators\` in metaobjects.config.ts ` +
69
+ `(import it from "@metaobjectsdev/codegen-ts/generators").${receiveHalf} ` +
70
+ `The payload value objects were emitted regardless — those are object.value nodes, ` +
71
+ `which is why this looked like it had worked.`,
72
+ );
73
+ }
@@ -6,6 +6,9 @@
6
6
  // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
7
  // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
8
  // `Bun is not defined`. Use `node:` builtins instead.
9
+ // targets: nothing framework-specific — it re-exports whatever the other generators
10
+ // emitted. `extStyle` decides whether the re-export specifiers carry a
11
+ // `.js` extension.
9
12
  // use-when: you want a single `index.ts` re-exporting every generated entity module.
10
13
  // emits: <target>/index.ts with one `export * from "./<Entity>"` per entity, alphabetical.
11
14
  // customize: the export form (star vs named), ordering, grouping by package, what to include/exclude.
@@ -6,6 +6,13 @@
6
6
  // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
7
  // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
8
  // `Bun is not defined`. Use `node:` builtins instead.
9
+ // targets: Drizzle ORM + Zod. The emitted module is a Drizzle table plus Zod
10
+ // insert/update schemas; the column mapping follows `dialect`. On the
11
+ // default (vanilla) path, replace the `renderDrizzleSchema` /
12
+ // `renderZodValidators` calls to target a different ORM or validator — the
13
+ // metadata walk that feeds them is ORM-neutral. (The `isWriteThrough`
14
+ // branch calls `renderEntityFile` instead — a narrow #214 read-view case,
15
+ // not the default.)
9
16
  // use-when: ALWAYS — this is the entity-module generator. It owns the shape of each
10
17
  // generated <Entity>.ts (the Drizzle table, Zod schemas, inferred types,
11
18
  // constants, filter allowlists). Start here and adapt the assembly.
@@ -54,11 +61,14 @@ import {
54
61
  isWriteThrough,
55
62
  isAbstract,
56
63
  hasWritableRdbSource,
64
+ isTphSubtype,
57
65
  // engine composer — used for the delegated write-through variant:
58
66
  renderEntityFile,
59
67
  // engine plumbing:
60
68
  formatTs,
61
69
  entityOutputPath,
70
+ namesRef,
71
+ namesConstArg,
62
72
  GENERATED_HEADER,
63
73
  } from "@metaobjectsdev/codegen-ts";
64
74
 
@@ -77,6 +87,10 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
77
87
  }
78
88
  // Projection → read-only view declaration + read schema.
79
89
  if (isProjection(entity)) {
90
+ // §A6/§B2 — same `namesRef` pair renderDrizzleSchema builds, so the projection's view
91
+ // name + per-field dbCol reference the exact constant the names artifact exports
92
+ // (undefined when the artifact is not in this run). `namesRef`'s `{ resolved, symbol }`
93
+ // return is exactly the shape `ProjectionDeclOpts.names` wants.
80
94
  return renderProjectionDecl(entity, ctx.loadedRoot, {
81
95
  columnNamingStrategy: ctx.columnNamingStrategy,
82
96
  dialect: ctx.dialect,
@@ -85,10 +99,18 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
85
99
  allowlists,
86
100
  ctx,
87
101
  includeViewDecl: runtime,
102
+ names: namesRef(entity, ctx),
88
103
  });
89
104
  }
90
105
  // Value-only / contract target → interface + Zod, no Drizzle table.
91
- if (!runtime || !hasWritableRdbSource(entity)) {
106
+ //
107
+ // A TPH subtype (FR-017) routes here too. It INHERITS the discriminator base's writable
108
+ // source.rdb through `extends`, so `hasWritableRdbSource` is true for it under the
109
+ // ADR-0039 resolving read — but the base owns the single shared table. Without the
110
+ // isTphSubtype clause this template fell through to the vanilla path and emitted a
111
+ // SECOND Drizzle table bound to the base's physical name, carrying only the subtype's
112
+ // own columns.
113
+ if (!runtime || !hasWritableRdbSource(entity) || isTphSubtype(entity)) {
92
114
  return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
93
115
  }
94
116
  // #214 — a write-through entity read-view (writable table + a read-only replica view +
@@ -103,12 +125,16 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
103
125
  const enumAliases = renderEnumTypeAliases(entity, ctx);
104
126
  const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
105
127
  const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
128
+ // §A6/§B2 — same `namesRef` pair renderDrizzleSchema (and the projection branch above)
129
+ // build, so the descriptor's $table references the exact constant the names artifact
130
+ // exports (undefined when the artifact is not in this run).
131
+ const constantsNames = namesRef(entity, ctx);
106
132
  const sections: Code[] = [
107
133
  renderDrizzleSchema(entity, ctx),
108
134
  renderInferredTypes(entity, tphBase, ctx),
109
135
  ...(enumAliases !== null ? [enumAliases] : []),
110
136
  renderZodValidators(entity, ctx),
111
- renderEntityConstants(entity, ctx.apiPrefix),
137
+ renderEntityConstants(entity, ctx.apiPrefix, namesConstArg(constantsNames)),
112
138
  ...(allowlists ? [renderFilterAllowlist(entity, undefined, ctx), renderSortAllowlist(entity)] : []),
113
139
  renderFilterType(entity),
114
140
  ...(tphBlock !== null ? [tphBlock] : []),
@@ -0,0 +1,125 @@
1
+ // REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/names.ts) and own it.
2
+ // Then import it LOCALLY in metaobjects.config.ts:
3
+ // import { namesFile } from "./codegen/generators/names.js";
4
+ //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`. Use `node:` builtins instead.
9
+ // targets: the emit step. Replace `renderNamesDecl` to change the artifact's SHAPE
10
+ // (e.g. flat `SUBSCRIBER_TABLE` constants instead of a nested object); keep
11
+ // `resolveObjectNames` so the names still come from the same resolver as the DDL.
12
+ // use-when: you want the physical database names (table/view + column names) for each
13
+ // object available as importable constants, so a hand-written consumer never
14
+ // has to embed a name as a string literal a second time.
15
+ // emits: <target>/<Entity>.names.ts per concrete object with a primary source
16
+ // (under outputLayout: "package", <target>/<pkg>/<Entity>.names.ts — beside
17
+ // the entity module it describes). An object with no primary source (#248)
18
+ // gets no names artifact — EXCEPT an abstract base that a sourced object
19
+ // extends, which gets a fragment (columns, no physical name) so its
20
+ // children extend it instead of restating every inherited column.
21
+ // customize: swap `renderNamesDecl` for your own shape; keep `resolveObjectNames` as the
22
+ // one resolver so the constant and the DDL it describes cannot disagree.
23
+ // composes-with: entity.ts, routes.ts — both reference these constants instead of
24
+ // embedding the names a second time. queries.ts never embeds a physical
25
+ // name of its own: it reads/writes through the Drizzle table object
26
+ // entity.ts builds (columns keyed by FIELD name), so there is nothing in
27
+ // it to wire.
28
+ //
29
+ // The composition here is deliberately a SEPARATE generator, never a boolean on the entity
30
+ // generator — a new artifact is a MINOR under docs/compatibility-policy.md and adds zero
31
+ // bytes to existing files, where a flag would move every $table-carrying golden for the
32
+ // same functionality.
33
+ import {
34
+ crossEntitySpecifier,
35
+ entityOutputPath,
36
+ namesArtifactSuperOf,
37
+ renderNamesDecl,
38
+ resolveObjectNames,
39
+ type EmittedFile,
40
+ type GenContext,
41
+ type Generator,
42
+ } from "@metaobjectsdev/codegen-ts";
43
+ import type { MetaObject } from "@metaobjectsdev/metadata";
44
+
45
+ export function namesFile(): Generator {
46
+ return {
47
+ name: "names",
48
+ // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
49
+ // entity generator can tell whether this artifact will exist. Exactly the mechanism
50
+ // routesFileHono already uses via emitsHonoRoutes/includeHonoRoutes.
51
+ emitsNames: true,
52
+ generate: (ctx: GenContext): EmittedFile[] => {
53
+ const layout = ctx.config.outputLayout ?? "flat";
54
+ const extStyle = ctx.config.extStyle ?? "js";
55
+ // The strategy lives on the RENDER CONTEXT, not on ResolvedGenConfig — `ctx.config`
56
+ // carries outDir/extStyle/dbImport/dialect and nothing about naming.
57
+ const strategy = ctx.renderContext?.columnNamingStrategy;
58
+
59
+ const pathOf = (obj: MetaObject): string =>
60
+ // entityOutputPath, not a bare filename: §A6 makes the entity module IMPORT these
61
+ // constants, so the artifact has to land in the same directory the entity module
62
+ // does. Under outputLayout: "package" a bare name puts it at the target ROOT while
63
+ // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
64
+ // conflicting-duplicate-path failure as soon as two packages declare a
65
+ // same-bare-named entity.
66
+ entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
67
+
68
+ const superSpecifierFor = (obj: MetaObject): string | undefined => {
69
+ const sup = namesArtifactSuperOf(obj);
70
+ return sup === undefined
71
+ ? undefined
72
+ : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
73
+ };
74
+
75
+ const out: EmittedFile[] = [];
76
+ // Pass 1 — every matched object that participates in the database (#248).
77
+ // `emitted` tracks what pass 1 actually WROTE, not what it looked at: a matched
78
+ // abstract base emits nothing here, and seeding it as already-emitted is what would
79
+ // make pass 2 skip the very object it exists to produce.
80
+ const emitted = new Set<string>();
81
+ const participants = ctx.entities.filter(ctx.matches);
82
+ for (const entity of participants) {
83
+ const content = renderNamesDecl(entity, {
84
+ strategy, superSpecifier: superSpecifierFor(entity),
85
+ });
86
+ if (content === "") continue; // no primary source ⇒ no names artifact (#248)
87
+ emitted.add(entity.resolutionKey());
88
+ out.push({ path: pathOf(entity), content });
89
+ }
90
+
91
+ // Pass 2 — the abstract bases those participants EXTEND. Each carries the columns it
92
+ // declares, so a child states them once rather than restating its parent's.
93
+ //
94
+ // Reached by walking UP from a participant, never by scanning for abstracts: that is
95
+ // what keeps #248 intact. A sourceless object nothing persistable extends — an
96
+ // `object.value`, say — is not reached, so it acquires no artifact and no phantom
97
+ // participation. Two children of one base both reach it and emit the same file at
98
+ // the same path with the same bytes; the runner collapses byte-identical duplicates
99
+ // (#266), so it is written once. `emitted` keeps that from even arising, and keeps
100
+ // the walk from re-rendering the same base once per child.
101
+ for (const entity of participants) {
102
+ if (resolveObjectNames(entity, strategy) === undefined) continue;
103
+ for (let sup = namesArtifactSuperOf(entity); sup !== undefined;
104
+ sup = namesArtifactSuperOf(sup)) {
105
+ const key = sup.resolutionKey();
106
+ if (emitted.has(key)) break; // already emitted, and so is everything above it
107
+ emitted.add(key);
108
+ const content = renderNamesDecl(sup, {
109
+ strategy, superSpecifier: superSpecifierFor(sup),
110
+ // "Fragment" means "declares no source". Hardcoding `true` is right for the
111
+ // shape this pass was written for — an abstract base with columns and no table
112
+ // — and wrong for the one it also reaches: `meta gen --entities <Subtype>`
113
+ // walks up to a TPH BASE, which owns the shared table, and a fragment renders
114
+ // no source at all. The engine derives this itself now (`renderNamesDecl`
115
+ // consults the object), so the flag says only "this is an ancestor render".
116
+ fragment: true,
117
+ });
118
+ if (content === "") continue;
119
+ out.push({ path: pathOf(sup), content });
120
+ }
121
+ }
122
+ return out;
123
+ },
124
+ };
125
+ }
@@ -6,6 +6,11 @@
6
6
  // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
7
  // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
8
  // `Bun is not defined`. Use `node:` builtins instead.
9
+ // targets: Drizzle. Emitted helpers take `db` as a PARAMETER rather than importing
10
+ // a module singleton, so they compose with any caller that already holds a
11
+ // connection — including a server-rendered component. Swap the
12
+ // `render<Verb>Fn` primitives (findById/list/create/update/deleteById) and
13
+ // the inline `Db` type block above them to emit for another query builder.
9
14
  // use-when: you want generated typed CRUD finders (find<E>ById, list<E>s, create/update/delete)
10
15
  // over Drizzle. Drop it if you hand-write your data access.
11
16
  // emits: <target>/<Entity>.queries.ts per source-backed object (any source.rdb kind,
@@ -72,14 +77,21 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
72
77
 
73
78
  // `db` is parameter-passed into every finder (ADR-0008). Emit the dialect-correct
74
79
  // Drizzle type alias so signatures typecheck without the consumer constructing one.
80
+ // Keep EVERY type argument as open as Drizzle's own constraint allows — a `Db` this
81
+ // alias cannot name is a helper nobody can call, and uncompilable generated code looks
82
+ // exactly like unused generated code from outside. Both the driver axis (base classes,
83
+ // not `NodePgDatabase` / `<"async">`) and the schema axis (`Record<string, unknown>`,
84
+ // Drizzle's own `TFullSchema extends …` bound, NOT its `Record<string, never>` default)
85
+ // have been re-pinned once already; do not narrow either again. A schema-carrying
86
+ // `drizzle(client, { schema })` must assign, and so must a schema-less `drizzle(client)`.
75
87
  const dbTypeImport =
76
88
  ctx.dialect === "postgres"
77
89
  ? `import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";`
78
90
  : `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
79
91
  const dbTypeAlias =
80
92
  ctx.dialect === "postgres"
81
- ? `type Db = PgDatabase<PgQueryResultHKT, Record<string, never>>;`
82
- : `type Db = BaseSQLiteDatabase<"sync" | "async", unknown>;`;
93
+ ? `type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;`
94
+ : `type Db = BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>;`;
83
95
 
84
96
  const literalImports = code`
85
97
  ${dbTypeImport}
@@ -0,0 +1,113 @@
1
+ // REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/routes-hono.ts) and own it.
2
+ // Then import it LOCALLY in metaobjects.config.ts:
3
+ // import { routesFileHono } from "./codegen/generators/routes-hono.js";
4
+ //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`.
9
+ // targets: Hono. The emitted file imports `mountCrudRoutes` from
10
+ // `@metaobjectsdev/runtime-ts/hono` and takes its persistence client as
11
+ // INJECTED DEPS (`register<Entity>Routes(app, { db })`) rather than a
12
+ // module-singleton import — which is what makes it portable to any host
13
+ // that can hand Hono a request. If your framework is not Hono, THIS is the
14
+ // file to retarget: swap the mount helper and the exported signature; the
15
+ // metadata walk above it is framework-neutral and stays as-is.
16
+ // use-when: you want generated Hono CRUD routes per entity.
17
+ // emits: <target>/<Entity>.routes.hono.ts — full CRUD for write-through entities,
18
+ // read-only (GET list + GET :id) for projections. Skipped for any sourceless
19
+ // object and for TPH subtypes.
20
+ // customize: this generator is YOURS — edit it freely. For the emitted route
21
+ // composition, call `renderRoutesFileHono` (exported from the engine) and
22
+ // wrap its result, or replace the call entirely. Decide per generator what
23
+ // you consume: wire only the generators whose output you actually import,
24
+ // and narrow this one with its `filter`. There is no `@emit*` metadata
25
+ // attribute — those were never registered vocabulary, so `meta verify`
26
+ // rejects them (ERR_UNKNOWN_ATTR).
27
+ // composes-with: entity.ts (imports the table/schemas/allowlists), queries.ts.
28
+
29
+ import { type MetaObject } from "@metaobjectsdev/metadata";
30
+ import {
31
+ perEntity,
32
+ type Generator,
33
+ type GeneratorFactory,
34
+ renderRoutesFileHono,
35
+ resolveExpose,
36
+ type ExposeOption,
37
+ isTphSubtype,
38
+ hasAnyRdbSource,
39
+ formatTs,
40
+ entityOutputPath,
41
+ } from "@metaobjectsdev/codegen-ts";
42
+
43
+ export interface RoutesFileHonoOpts {
44
+ filter?: (entity: MetaObject) => boolean;
45
+ /**
46
+ * Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
47
+ * absent means all five and emits byte-identical output.
48
+ *
49
+ * routesFileHono({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
50
+ *
51
+ * A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
52
+ * only remove the whole surface, not restrict it to a subset of verbs.
53
+ */
54
+ expose?: ExposeOption;
55
+ target?: string;
56
+ }
57
+
58
+ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts): Generator {
59
+ const userFilter = opts?.filter ?? (() => true);
60
+ // Eligibility, minus the TPH question — stated ONCE so the emit set and the warn set
61
+ // below cannot drift apart. They differ only by `!isTphSubtype` vs `isTphSubtype`, and
62
+ // written out twice a later edit to one silently makes an entity either stop emitting
63
+ // without being named as held back, or get warned about while still emitting.
64
+ // (Same shape as tanstack's grid generator, which factors it the same way.)
65
+ const passesOtherGates = (e: MetaObject): boolean =>
66
+ hasAnyRdbSource(e) && userFilter(e);
67
+ const generator: Generator = {
68
+ name: "routes-file-hono",
69
+ // Marks this as the Hono routes generator so the runner can aggregate
70
+ // `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
71
+ emitsHonoRoutes: true,
72
+ // TPH subtypes are EXCLUDED, matching the Fastify generator. A TPH subtype shares
73
+ // its base's table, so mounting vanilla CRUD for it produced routes with no
74
+ // discriminator scoping at all: the list returned EVERY subtype's rows, and
75
+ // get/patch/delete by id happily operated on rows belonging to a different
76
+ // subtype. Silently wrong data, which is worse than no route. Fastify dispatches
77
+ // these to a discriminator-aware renderer; the Hono runtime has no discriminator
78
+ // support yet, so this fails CLOSED and the run says so (see the warning below)
79
+ // rather than shipping an artifact that returns the wrong rows.
80
+ filter: (e: MetaObject) => passesOtherGates(e) && !isTphSubtype(e),
81
+ generate: async (ctx) => {
82
+ // One note per run naming every TPH subtype held back, so the gap is visible at
83
+ // `meta gen` time rather than discovered as missing endpoints in production.
84
+ const skipped = ctx.entities.filter((e) => passesOtherGates(e) && isTphSubtype(e));
85
+ if (skipped.length > 0) {
86
+ ctx.warn(
87
+ `no Hono routes emitted for the TPH subtype(s) ${skipped.map((e) => e.name).join(", ")} — ` +
88
+ "the Hono adapter has no discriminator scoping yet, so per-subtype CRUD would " +
89
+ "return and mutate OTHER subtypes' rows. Use routesFile() (Fastify), which " +
90
+ "dispatches TPH correctly, or hand-write the scoped routes.",
91
+ );
92
+ }
93
+ return emit(ctx);
94
+ },
95
+ };
96
+ const emit = perEntity(async (entity, ctx) => {
97
+ if (!ctx.renderContext) {
98
+ throw new Error("routes-file-hono: renderContext is required (provided by runGen)");
99
+ }
100
+ return {
101
+ path: entityOutputPath(
102
+ ctx.config.outputLayout ?? "flat",
103
+ entity.package,
104
+ `${entity.name}.routes.hono.ts`,
105
+ ),
106
+ content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
107
+ };
108
+ });
109
+ if (opts?.target) {
110
+ generator.target = opts.target;
111
+ }
112
+ return generator;
113
+ } as GeneratorFactory<RoutesFileHonoOpts>;