@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
@@ -2,166 +2,363 @@
2
2
  // templates consume. The previous hand-coded `renderDocsFile()` mixed data
3
3
  // extraction with string emission; this module is the data-only half — the
4
4
  // markdown structure now lives in templates/docs/entity-page.md.mustache.
5
- import { TYPE_TEMPLATE, TEMPLATE_ATTR_PAYLOAD_REF, OBJECT_SUBTYPE_VALUE, IDENTITY_SUBTYPE_PRIMARY, IDENTITY_SUBTYPE_SECONDARY, IDENTITY_SUBTYPE_REFERENCE, IDENTITY_ATTR_GENERATION, RELATIONSHIP_ATTR_CARDINALITY, RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_SUBTYPE_COMPOSITION, RELATIONSHIP_SUBTYPE_AGGREGATION, RELATIONSHIP_SUBTYPE_ASSOCIATION, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_CLASS, FIELD_SUBTYPE_UUID, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_SHORT, FIELD_SUBTYPE_BYTE, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIME, FIELD_SUBTYPE_TIMESTAMP, FIELD_ATTR_REQUIRED, FIELD_ATTR_UNIQUE, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX, VALIDATOR_SUBTYPE_NUMERIC, VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_ATTR_PATTERN, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, DOC_ATTR_DESCRIPTION, stripPackage, } from "@metaobjectsdev/metadata";
6
- import { mapColumnType } from "../column-mapper.js";
7
- import { toPascalCase } from "../naming.js";
5
+ import { TYPE_TEMPLATE, TEMPLATE_ATTR_PAYLOAD_REF, OBJECT_SUBTYPE_VALUE, IDENTITY_SUBTYPE_PRIMARY, IDENTITY_SUBTYPE_SECONDARY, IDENTITY_SUBTYPE_REFERENCE, IDENTITY_ATTR_GENERATION, RELATIONSHIP_ATTR_CARDINALITY, RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH, RELATIONSHIP_ATTR_SOURCE_REF_FIELD, RELATIONSHIP_ATTR_SYMMETRIC, RELATIONSHIP_SUBTYPE_COMPOSITION, RELATIONSHIP_SUBTYPE_AGGREGATION, RELATIONSHIP_SUBTYPE_ASSOCIATION, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_REQUIRED, FIELD_ATTR_UNIQUE, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX, VALIDATOR_SUBTYPE_NUMERIC, VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_ATTR_PATTERN, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, DOC_ATTR_DESCRIPTION, DOC_ATTR_SUMMARY, FIELD_ATTR_DB_COLUMN_TYPE, stripPackage, } from "@metaobjectsdev/metadata";
6
+ import { docPageHref, docPageNode } from "../docs-paths.js";
7
+ import { fieldAnchorHtml } from "./field-anchor.js";
8
8
  import { enumValues } from "../enum-meta.js";
9
9
  import { hasWritableRdbSource } from "../source-detect.js";
10
10
  import { GENERATED_HEADER } from "../constants.js";
11
- const SCALAR_TS_BY_SUBTYPE = {
12
- [FIELD_SUBTYPE_STRING]: "string",
13
- [FIELD_SUBTYPE_CLASS]: "string",
14
- [FIELD_SUBTYPE_UUID]: "string",
15
- [FIELD_SUBTYPE_INT]: "number",
16
- [FIELD_SUBTYPE_SHORT]: "number",
17
- [FIELD_SUBTYPE_BYTE]: "number",
18
- [FIELD_SUBTYPE_LONG]: "number",
19
- [FIELD_SUBTYPE_DOUBLE]: "number",
20
- [FIELD_SUBTYPE_FLOAT]: "number",
21
- // field.decimal is precision-exact: surfaced as a TS `string` (Drizzle pg
22
- // `numeric` infers `string`). Keep the docs scalar mapping in lockstep.
23
- [FIELD_SUBTYPE_DECIMAL]: "string",
24
- [FIELD_SUBTYPE_CURRENCY]: "number",
25
- [FIELD_SUBTYPE_BOOLEAN]: "boolean",
26
- [FIELD_SUBTYPE_DATE]: "string",
27
- [FIELD_SUBTYPE_TIME]: "string",
28
- [FIELD_SUBTYPE_TIMESTAMP]: "string",
29
- };
30
- function enumTypeAliasName(entity, field) {
31
- const superField = field.resolveSuper();
32
- return superField !== undefined
33
- ? toPascalCase(superField.name)
34
- : `${entity.name}${toPascalCase(field.name)}`;
35
- }
36
- function isFieldRequired(field) {
37
- if (field.ownAttr(FIELD_ATTR_REQUIRED) === true)
11
+ import { renderEntityNeighborhoodErBlock } from "../templates/mermaid-er.js";
12
+ /** Whether a field is required — `@required` true OR a `validator.required`
13
+ * child. The SINGLE source of truth for required-ness across the Constraints
14
+ * table, the Storage nullable rule, and (via the api-docs field-shape builder)
15
+ * the documented model-field optionality. Exported so the field-shape builder
16
+ * reuses the EXACT same rule rather than re-deriving it. */
17
+ export function isFieldRequired(field) {
18
+ if (field.attr(FIELD_ATTR_REQUIRED) === true)
38
19
  return true;
39
20
  return field.validators().some((v) => v.subType === VALIDATOR_SUBTYPE_REQUIRED);
40
21
  }
41
- function tsTypeForStorage(entity, field, pkFieldNames) {
42
- let base;
43
- if (field.subType === FIELD_SUBTYPE_ENUM) {
44
- const values = enumValues(field);
45
- if (values !== undefined && values.length > 0) {
46
- if (field.isArray) {
47
- base = `${enumTypeAliasName(entity, field)}[]`;
48
- }
49
- else {
50
- base = values.map((v) => JSON.stringify(v)).join(" | ");
22
+ function collectValidatorParts(field) {
23
+ const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
24
+ const regexParts = [];
25
+ const lengthParts = [];
26
+ const numericParts = [];
27
+ for (const v of field.validators()) {
28
+ if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
29
+ const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
30
+ if (typeof pattern === "string" && pattern.length > 0) {
31
+ regexParts.push(`pattern \`${pattern}\``);
51
32
  }
52
33
  }
53
- else {
54
- base = field.isArray ? "string[]" : "string";
34
+ else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
35
+ const min = v.ownAttr(VALIDATOR_ATTR_MIN);
36
+ const max = v.ownAttr(VALIDATOR_ATTR_MAX);
37
+ if (typeof min === "number")
38
+ lengthParts.push(`minLength: ${min}`);
39
+ if (typeof max === "number" && typeof maxLenAttr !== "number")
40
+ lengthParts.push(`maxLength: ${max}`);
41
+ }
42
+ else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
43
+ const min = v.ownAttr(VALIDATOR_ATTR_MIN);
44
+ const max = v.ownAttr(VALIDATOR_ATTR_MAX);
45
+ if (typeof min === "number")
46
+ numericParts.push(`min: ${min}`);
47
+ if (typeof max === "number")
48
+ numericParts.push(`max: ${max}`);
55
49
  }
56
50
  }
57
- else if (field.subType === FIELD_SUBTYPE_OBJECT) {
58
- const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
59
- const refName = typeof ref === "string" && ref.length > 0 ? ref : "unknown";
60
- base = field.isArray ? `${refName}[]` : refName;
51
+ return {
52
+ maxLenAttr: typeof maxLenAttr === "number" ? maxLenAttr : undefined,
53
+ regexParts,
54
+ lengthParts,
55
+ numericParts,
56
+ };
57
+ }
58
+ /** The NEUTRAL logical type string (no backticks): the field's logical
59
+ * subtype (e.g. `string`, `enum`, `decimal`), suffixed `[]` for arrays, and
60
+ * the referenced object name for `field.object`. Language-agnostic — built
61
+ * from declared metadata, never re-derived into ANSI/ORM SQL. Shared by the
62
+ * Constraints table (`neutralTypeCell`) and the Storage table's physical-type
63
+ * fallback (`storageTypeCell`). */
64
+ export function neutralTypeStr(field) {
65
+ let base;
66
+ if (field.subType === FIELD_SUBTYPE_OBJECT) {
67
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
68
+ base = typeof ref === "string" && ref.length > 0 ? stripPackage(ref) : "object";
61
69
  }
62
70
  else {
63
- const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
64
- base = field.isArray ? `${scalar}[]` : scalar;
71
+ base = field.subType;
65
72
  }
66
- const required = pkFieldNames.has(field.name) || isFieldRequired(field);
67
- return required ? base : `${base} | null`;
73
+ if (field.isArray)
74
+ base = `${base}[]`;
75
+ return base;
68
76
  }
69
- function sqlColumnExpr(spec) {
70
- const dbName = JSON.stringify(spec.dbName);
71
- if (spec.fnOptions !== undefined && Object.keys(spec.fnOptions).length > 0) {
72
- const parts = [];
73
- for (const [k, v] of Object.entries(spec.fnOptions)) {
74
- const lit = JSON.stringify(v);
75
- if (Array.isArray(v)) {
76
- parts.push(`${k}: ${lit} as const`);
77
- }
78
- else {
79
- parts.push(`${k}: ${lit}`);
80
- }
77
+ /** Neutral logical type cell for the Constraints table — `neutralTypeStr`
78
+ * wrapped in backticks. */
79
+ function neutralTypeCell(field) {
80
+ return `\`${neutralTypeStr(field)}\``;
81
+ }
82
+ /** Neutral PHYSICAL type cell for the Storage table. Metadata-driven, no DDL
83
+ * re-derivation (ADR-0020): if the field declares a `@dbColumnType` physical
84
+ * override (e.g. `uuid`, `jsonb`, `timestamp_with_tz`) show it UPPERCASED;
85
+ * otherwise fall back to the same neutral LOGICAL type the Constraints table
86
+ * uses. Deliberately does NOT derive ANSI/ORM SQL so it can't drift vs the
87
+ * migrate engine or re-introduce language-specific DDL. Wrapped in backticks. */
88
+ function storageTypeCell(field) {
89
+ const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
90
+ if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
91
+ return `\`${dbColumnType.toUpperCase()}\``;
92
+ }
93
+ return `\`${neutralTypeStr(field)}\``;
94
+ }
95
+ /** Build one neutral Constraints-table row for a field. Reuses the same
96
+ * per-field constraint logic as `constraintsCell()` (required-ness, maxLength,
97
+ * enum CHECK-sets, validators, default, unique, references), but splits the
98
+ * facts across the Required / Limits / Rules columns instead of one cell.
99
+ * Renders for every field, with or without storage. */
100
+ function buildConstraintRow(entity, field, pkFieldNames, fkMap) {
101
+ const isPk = pkFieldNames.has(field.name);
102
+ const required = isPk || isFieldRequired(field);
103
+ const limits = [];
104
+ const rules = [];
105
+ if (isPk)
106
+ rules.push("primary key");
107
+ if (field.attr(FIELD_ATTR_UNIQUE) === true)
108
+ rules.push("unique");
109
+ if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
110
+ const values = enumValues(field);
111
+ if (values !== undefined && values.length > 0) {
112
+ const list = values.map((v) => `\`${v}\``).join(", ");
113
+ rules.push(`one of ${list}`);
81
114
  }
82
- return `${spec.fnName}(${dbName}, { ${parts.join(", ")} })`;
83
115
  }
84
- return `${spec.fnName}(${dbName})`;
116
+ // Same validator facts as constraintsCell() (shared walk), arranged across
117
+ // the Limits / Rules columns instead of one cell.
118
+ const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
119
+ rules.push(...regexParts);
120
+ if (maxLenAttr !== undefined)
121
+ limits.push(`maxLength: ${maxLenAttr}`);
122
+ limits.push(...lengthParts, ...numericParts);
123
+ const fk = fkMap.get(field.name);
124
+ if (fk !== undefined) {
125
+ rules.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
126
+ }
127
+ const def = field.attr(FIELD_ATTR_DEFAULT);
128
+ if (def !== undefined)
129
+ rules.push(`default: \`${String(def)}\``);
130
+ const sup = field.resolveSuper();
131
+ if (sup !== undefined)
132
+ rules.push(`extends \`${sup.name}\``);
133
+ return {
134
+ // The Field cell carries a stable HTML anchor (`<a id="field-<name>">`)
135
+ // before the backticked name, so the template-source annotator's
136
+ // `#field-<name>` links resolve. Slug = `fieldAnchorSlug(name)` — the SINGLE
137
+ // source shared with the annotator so anchor and link can't drift. The
138
+ // anchor is a language-independent HTML id, so the page stays neutral.
139
+ field: `${fieldAnchorHtml(field.name)}\`${field.name}\``,
140
+ required: required ? "yes" : "",
141
+ type: neutralTypeCell(field),
142
+ limits: limits.join(", "),
143
+ rules: rules.join(", "),
144
+ };
85
145
  }
86
- function constraintsCell(entity, field, pkFieldNames, fkMap) {
87
- const parts = [];
88
- if (pkFieldNames.has(field.name)) {
89
- parts.push("primary key");
90
- const primary = entity.primaryIdentity();
91
- const gen = primary?.ownAttr(IDENTITY_ATTR_GENERATION);
92
- if (typeof gen === "string") {
93
- parts.push(`generation: \`${gen}\``);
146
+ /** Build one row of the unified Fields table — collapses the per-field facts
147
+ * the old Storage + Constraints tables split between into a single Markdown
148
+ * row. PK/FK key role becomes a glyph prefix on the Field cell, the FK
149
+ * target becomes a `→ \`Target\`` suffix on the Type cell, the @column
150
+ * override (only when interesting) lands in the Storage cell, everything
151
+ * else (validators, defaults, enum CHECK-sets, references, unique, extends)
152
+ * goes into the Rules cell joined by " · ".
153
+ *
154
+ * Identity bullets remain a separate section above — they describe the
155
+ * *identity declarations* (composite keys, generation strategy, reference
156
+ * topology), not the per-field facts. */
157
+ function buildFieldRow(entity, field, pkFieldNames, fkMap) {
158
+ const isPk = pkFieldNames.has(field.name);
159
+ const fk = fkMap.get(field.name);
160
+ const required = isPk || isFieldRequired(field);
161
+ // Field cell — anchor + glyph + name.
162
+ let glyph = "";
163
+ if (isPk)
164
+ glyph = "🔑 ";
165
+ else if (fk !== undefined)
166
+ glyph = "🔗 ";
167
+ const fieldCell = `${fieldAnchorHtml(field.name)}${glyph}\`${field.name}\``;
168
+ // Type cell — neutral logical type; for FK, append the target as a link.
169
+ let typeCell = neutralTypeCell(field);
170
+ if (fk !== undefined) {
171
+ typeCell = `${typeCell} → \`${fk.targetEntity}\``;
172
+ }
173
+ // Storage cell — only populated when interesting:
174
+ // - @column override that differs from the field name, OR
175
+ // - @dbColumnType physical override set
176
+ // Otherwise empty. Keeps the column noise-free for the 90% case where
177
+ // field name and column name agree.
178
+ const columnName = field.column;
179
+ const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
180
+ const columnDiffers = typeof columnName === "string" && columnName !== field.name;
181
+ const hasPhysicalOverride = typeof dbColumnType === "string" && dbColumnType.length > 0;
182
+ let storageCell = "";
183
+ if (columnDiffers && hasPhysicalOverride) {
184
+ storageCell = `\`${columnName}\` \`${dbColumnType.toUpperCase()}\``;
185
+ }
186
+ else if (columnDiffers) {
187
+ storageCell = `\`${columnName}\``;
188
+ }
189
+ else if (hasPhysicalOverride) {
190
+ storageCell = `\`${dbColumnType.toUpperCase()}\``;
191
+ }
192
+ // Rules cell — joined facts. Same logic as buildConstraintRow's Rules
193
+ // column, plus the maxLength/length/numeric limits that used to live in
194
+ // the separate Limits cell (collapsed in to keep the table to 5 columns).
195
+ const rules = [];
196
+ if (field.attr(FIELD_ATTR_UNIQUE) === true)
197
+ rules.push("unique");
198
+ if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
199
+ const values = enumValues(field);
200
+ if (values !== undefined && values.length > 0) {
201
+ const list = values.map((v) => `\`${v}\``).join(", ");
202
+ rules.push(`one of ${list}`);
94
203
  }
95
204
  }
205
+ const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
206
+ rules.push(...regexParts);
207
+ if (maxLenAttr !== undefined)
208
+ rules.push(`maxLength: ${maxLenAttr}`);
209
+ rules.push(...lengthParts, ...numericParts);
210
+ // The FK reference is already encoded in typeCell — don't repeat it in rules.
211
+ const def = field.attr(FIELD_ATTR_DEFAULT);
212
+ if (def !== undefined)
213
+ rules.push(`default: \`${String(def)}\``);
214
+ const sup = field.resolveSuper();
215
+ if (sup !== undefined)
216
+ rules.push(`extends \`${sup.name}\``);
217
+ return {
218
+ field: field.name,
219
+ fieldCell,
220
+ typeCell,
221
+ requiredCell: required ? "yes" : "",
222
+ storageCell,
223
+ rulesCell: rules.join(" · "),
224
+ };
225
+ }
226
+ /** Build an expanded per-field detail block — `### \`name\`` heading, italic
227
+ * @summary lead-in, @description paragraph, then a bullet list of every
228
+ * notable rule (validators, default, FK, extends-enum, column override).
229
+ * Returns `undefined` when the field has nothing extra to say (just type +
230
+ * required) — the caller filters these out so the section stays tight.
231
+ *
232
+ * The validator list is the most important value-add: the Fields table
233
+ * collapses `pattern \`X\` · maxLength: 200 · minLength: 3` into a single
234
+ * Rules cell; this section breaks them out as individual bullets so the
235
+ * reader can scan each rule on its own line. */
236
+ function buildFieldDetail(field, pkFieldNames, fkMap) {
237
+ const desc = field.attr(DOC_ATTR_DESCRIPTION);
238
+ const summary = field.attr(DOC_ATTR_SUMMARY);
239
+ const hasDesc = typeof desc === "string" && desc.length > 0;
240
+ const hasSummary = typeof summary === "string" && summary.length > 0;
241
+ const sup = field.resolveSuper();
242
+ const fk = fkMap.get(field.name);
243
+ const def = field.attr(FIELD_ATTR_DEFAULT);
244
+ const columnName = field.column;
245
+ const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
246
+ const isUnique = field.attr(FIELD_ATTR_UNIQUE) === true;
247
+ const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.isArray;
248
+ const enumVals = isEnum ? enumValues(field) : undefined;
249
+ const validators = field.validators();
250
+ const hasValidatorChildren = validators.some(v => v.subType === VALIDATOR_SUBTYPE_LENGTH
251
+ || v.subType === VALIDATOR_SUBTYPE_REGEX
252
+ || v.subType === VALIDATOR_SUBTYPE_NUMERIC);
253
+ const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
254
+ // "Interesting enough to render a detail block" predicate. Plain typed
255
+ // fields with no authored annotations get skipped — the at-a-glance Fields
256
+ // table covered them already.
257
+ //
258
+ // Deliberately NOT counted as "interesting":
259
+ // - PK / required-ness (already a column in the table)
260
+ // - mechanical @column overrides (adopters typically set
261
+ // @column: PascalCase(name) wholesale; surfacing every field for
262
+ // that alone would defeat the section's purpose)
263
+ // The detail section's value is surfacing AUTHORED docs + validators +
264
+ // business rules, not physical column mapping.
265
+ const isInteresting = hasDesc
266
+ || hasSummary
267
+ || sup !== undefined
268
+ || fk !== undefined
269
+ || def !== undefined
270
+ || hasValidatorChildren
271
+ || typeof maxLenAttr === "number"
272
+ || (enumVals !== undefined && enumVals.length > 0)
273
+ || isUnique
274
+ || (typeof dbColumnType === "string" && dbColumnType.length > 0);
275
+ if (!isInteresting)
276
+ return undefined;
277
+ const parts = [`### \`${field.name}\``];
278
+ if (hasSummary) {
279
+ parts.push("");
280
+ parts.push(`*${summary}*`);
281
+ }
282
+ if (hasDesc) {
283
+ parts.push("");
284
+ parts.push(String(desc).trim());
285
+ }
286
+ // Bullet list — one fact per line. Order: type → FK → required/PK → column
287
+ // → default → unique → extends → enum values → validators.
288
+ const bullets = [];
289
+ bullets.push(`**Type:** ${neutralTypeCell(field)}`);
290
+ if (fk !== undefined) {
291
+ bullets.push(`**References:** [\`${fk.targetEntity}.${fk.targetField}\`](${fk.targetEntity}.md)`);
292
+ }
293
+ if (pkFieldNames.has(field.name)) {
294
+ bullets.push("**Primary key**");
295
+ }
96
296
  else if (isFieldRequired(field)) {
97
- parts.push("required");
297
+ bullets.push("**Required**");
98
298
  }
99
- else {
100
- parts.push("optional");
299
+ if (typeof columnName === "string" && columnName !== field.name) {
300
+ bullets.push(`**Column:** \`${columnName}\``);
101
301
  }
102
- if (field.ownAttr(FIELD_ATTR_UNIQUE) === true) {
103
- parts.push("unique");
302
+ if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
303
+ bullets.push(`**Physical type:** \`${dbColumnType.toUpperCase()}\``);
104
304
  }
105
- if (field.isArray) {
106
- parts.push("JSON column");
305
+ if (def !== undefined) {
306
+ bullets.push(`**Default:** \`${String(def)}\``);
107
307
  }
108
- if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
109
- const values = enumValues(field);
110
- if (values !== undefined && values.length > 0) {
111
- const list = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
112
- parts.push(`CHECK \`${field.column ?? field.name} IN (${list})\``);
113
- }
308
+ if (isUnique)
309
+ bullets.push("**Unique**");
310
+ if (sup !== undefined) {
311
+ // The postprocess script rewrites `extends \`Name\`` → enum anchor link.
312
+ bullets.push(`**Extends:** \`${sup.name}\``);
114
313
  }
115
- // Walk validators once, bucket by subtype. We re-emit in the original
116
- // emission order to preserve byte-identity with the docs-file-basic
117
- // conformance fixture: regex pattern → maxLength-from-@maxLength →
118
- // length-validator (min/max) → numeric-validator (min/max).
119
- const maxLenAttr = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
120
- const regexParts = [];
121
- const lengthParts = [];
122
- const numericParts = [];
123
- for (const v of field.validators()) {
314
+ if (enumVals !== undefined && enumVals.length > 0) {
315
+ const vals = enumVals.map((v) => `\`${v}\``).join(" · ");
316
+ bullets.push(`**Enum values:** ${vals}`);
317
+ }
318
+ // Validators one bullet per validator subtype (regex / length / numeric),
319
+ // rendered in declaration order so authors can rely on the order they
320
+ // wrote.
321
+ for (const v of validators) {
124
322
  if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
125
323
  const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
126
324
  if (typeof pattern === "string" && pattern.length > 0) {
127
- regexParts.push(`pattern \`${pattern}\``);
325
+ bullets.push(`**Validator (regex):** pattern \`${pattern}\``);
128
326
  }
129
327
  }
130
328
  else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
131
329
  const min = v.ownAttr(VALIDATOR_ATTR_MIN);
132
330
  const max = v.ownAttr(VALIDATOR_ATTR_MAX);
331
+ const fragments = [];
133
332
  if (typeof min === "number")
134
- lengthParts.push(`minLength: ${min}`);
135
- if (typeof max === "number" && typeof maxLenAttr !== "number")
136
- lengthParts.push(`maxLength: ${max}`);
333
+ fragments.push(`min ${min}`);
334
+ if (typeof max === "number")
335
+ fragments.push(`max ${max}`);
336
+ if (fragments.length > 0)
337
+ bullets.push(`**Validator (length):** ${fragments.join(", ")}`);
137
338
  }
138
339
  else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
139
340
  const min = v.ownAttr(VALIDATOR_ATTR_MIN);
140
341
  const max = v.ownAttr(VALIDATOR_ATTR_MAX);
342
+ const fragments = [];
141
343
  if (typeof min === "number")
142
- numericParts.push(`min: ${min}`);
344
+ fragments.push(`min ${min}`);
143
345
  if (typeof max === "number")
144
- numericParts.push(`max: ${max}`);
346
+ fragments.push(`max ${max}`);
347
+ if (fragments.length > 0)
348
+ bullets.push(`**Validator (numeric):** ${fragments.join(", ")}`);
145
349
  }
146
350
  }
147
- parts.push(...regexParts);
351
+ // @maxLength is the shorthand; render alongside validators for consistency.
148
352
  if (typeof maxLenAttr === "number") {
149
- parts.push(`maxLength: ${maxLenAttr}`);
150
- }
151
- parts.push(...lengthParts, ...numericParts);
152
- const fk = fkMap.get(field.name);
153
- if (fk !== undefined) {
154
- parts.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
155
- }
156
- const def = field.ownAttr(FIELD_ATTR_DEFAULT);
157
- if (def !== undefined) {
158
- parts.push(`default: \`${String(def)}\``);
159
- }
160
- const sup = field.resolveSuper();
161
- if (sup !== undefined) {
162
- parts.push(`extends \`${sup.name}\``);
163
- }
164
- return parts.join(", ");
353
+ bullets.push(`**Max length:** ${maxLenAttr}`);
354
+ }
355
+ parts.push("");
356
+ for (const b of bullets)
357
+ parts.push(`- ${b}`);
358
+ return {
359
+ field: field.name,
360
+ block: parts.join("\n"),
361
+ };
165
362
  }
166
363
  function buildFkMap(entity, root) {
167
364
  const out = new Map();
@@ -191,13 +388,17 @@ function entityDescription(entity) {
191
388
  const v = entity.attr(DOC_ATTR_DESCRIPTION);
192
389
  return typeof v === "string" && v.length > 0 ? v : undefined;
193
390
  }
391
+ function entitySummary(entity) {
392
+ const v = entity.attr(DOC_ATTR_SUMMARY);
393
+ return typeof v === "string" && v.length > 0 ? v : undefined;
394
+ }
194
395
  function describeIdentity(id) {
195
396
  const fields = id.fields;
196
397
  const fieldList = fields.length === 1
197
398
  ? `\`${fields[0]}\``
198
399
  : `(${fields.map((f) => `\`${f}\``).join(", ")})`;
199
400
  if (id.subType === IDENTITY_SUBTYPE_PRIMARY) {
200
- const gen = id.ownAttr(IDENTITY_ATTR_GENERATION);
401
+ const gen = id.attr(IDENTITY_ATTR_GENERATION);
201
402
  const genSuffix = typeof gen === "string" ? ` — generation: \`${gen}\`` : "";
202
403
  return `**Primary key:** ${fieldList}${genSuffix}`;
203
404
  }
@@ -235,33 +436,72 @@ function relationshipBullet(r) {
235
436
  break;
236
437
  default: label = subtype;
237
438
  }
439
+ // M:N (FR-018): the relationship traverses a junction (`@through`). Describe
440
+ // the edge as related-target THROUGH junction, and mark the self-join shape:
441
+ // symmetric (undirected) → "symmetric self-join"
442
+ // @sourceRefField set (directed) → "directed self-join via `<field>`"
443
+ // The junction/disambiguator are DECLARED facts (ADR-0020 — no re-derivation).
444
+ const throughRaw = r.ownAttr(RELATIONSHIP_ATTR_THROUGH);
445
+ if (typeof throughRaw === "string" && throughRaw.length > 0) {
446
+ const through = stripPackage(throughRaw);
447
+ const noteParts = [`${label}, through \`${through}\``];
448
+ if (r.ownAttr(RELATIONSHIP_ATTR_SYMMETRIC) === true) {
449
+ noteParts.push("symmetric self-join");
450
+ }
451
+ else {
452
+ const srcRef = r.ownAttr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
453
+ if (typeof srcRef === "string" && srcRef.length > 0) {
454
+ noteParts.push(`directed self-join via \`${srcRef}\``);
455
+ }
456
+ }
457
+ return `\`${r.name}\` — ${card} → \`${target}\` (${noteParts.join(", ")})`;
458
+ }
238
459
  return `\`${r.name}\` — ${card} → \`${target}\` (${label})`;
239
460
  }
240
461
  /** Build the EntityDocData payload for one entity. The single public-API
241
462
  * entry point exported by this module; the markdown template applies
242
463
  * against this shape. */
243
464
  export function buildEntityDocData(entity, opts) {
244
- const strategy = opts.columnNamingStrategy ?? "snake_case";
245
465
  const root = opts.loadedRoot;
466
+ const layout = opts.layout ?? "flat";
246
467
  const primary = entity.primaryIdentity();
247
468
  const pkFields = primary?.fields ?? [];
248
469
  const pkFieldNames = new Set(pkFields);
249
470
  const fkMap = buildFkMap(entity, root);
250
- // ---- Storage rows
471
+ // ---- Storage rows — NEUTRAL physical persistence MAPPING (ADR-0020): the
472
+ // physical column name, a neutral physical type (declared `@dbColumnType`
473
+ // override else the logical type), nullability, and the key role. NO
474
+ // TypeScript type, NO ORM DDL, NO ANSI re-derivation — declared metadata
475
+ // facts only. The value-add over the Constraints table is the field→column
476
+ // mapping + physical-type override + key role.
251
477
  const storageRows = entity.fields().map((field) => {
252
- const spec = mapColumnType(field, opts.dialect, strategy);
253
- const tsType = tsTypeForStorage(entity, field, pkFieldNames);
254
- const tsTypeCell = tsType.split("|").map((s) => s.trim()).join(" \\| ");
255
- const sqlExpr = sqlColumnExpr(spec);
256
- const cons = constraintsCell(entity, field, pkFieldNames, fkMap);
257
- const tsTypeCellStr = `\`${tsTypeCell}\``;
258
- const sqlExprCellStr = `\`${sqlExpr}\``;
478
+ const isPk = pkFieldNames.has(field.name);
479
+ // Physical column name: the field's `@column` override if set, else the
480
+ // field name. (The Storage section shows the RAW declared mapping; column
481
+ // naming-strategy folding stays a codegen concern, not a docs fact.)
482
+ const columnName = field.column ?? field.name;
483
+ const columnCell = `\`${columnName}\``;
484
+ const typeCell = storageTypeCell(field);
485
+ // Nullable iff not required and not the PK (matches the Constraints table's
486
+ // required-ness rule).
487
+ const nullable = !(isPk || isFieldRequired(field));
488
+ const nullableCell = nullable ? "yes" : "no";
489
+ let keyCell = "";
490
+ if (isPk) {
491
+ keyCell = "primary key";
492
+ }
493
+ else {
494
+ const fk = fkMap.get(field.name);
495
+ if (fk !== undefined)
496
+ keyCell = `foreign key → \`${fk.targetEntity}\``;
497
+ }
259
498
  return {
260
499
  name: field.name,
261
- tsTypeCell: tsTypeCellStr,
262
- sqlExprCell: sqlExprCellStr,
263
- constraintsCell: cons,
264
- rowLine: `| \`${field.name}\` | ${tsTypeCellStr} | ${sqlExprCellStr} | ${cons} |`,
500
+ columnCell,
501
+ typeCell,
502
+ nullableCell,
503
+ keyCell,
504
+ rowLine: `| ${columnCell} | ${typeCell} | ${nullableCell} | ${keyCell} |`,
265
505
  };
266
506
  });
267
507
  const isValue = entity.subType === OBJECT_SUBTYPE_VALUE;
@@ -276,13 +516,39 @@ export function buildEntityDocData(entity, opts) {
276
516
  const relationships = rels.length > 0
277
517
  ? rels.map((r) => ({ bullet: relationshipBullet(r) }))
278
518
  : undefined;
279
- // ---- Validation
280
- const lower = entity.name.charAt(0).toLowerCase() + entity.name.slice(1);
281
- const validation = {
282
- insertSchema: `${entity.name}InsertSchema`,
283
- updateSchema: `${entity.name}UpdateSchema`,
284
- entityFile: `${entity.name}.ts`,
285
- lower,
519
+ // ---- Constraints (NEUTRAL — built from the object's OWN field metadata, so
520
+ // it renders for every object including value objects with no storage).
521
+ // KEPT FOR BACK-COMPAT — new templates render `fields` instead.
522
+ const constraintRows = entity
523
+ .fields()
524
+ .map((field) => buildConstraintRow(entity, field, pkFieldNames, fkMap));
525
+ const constraints = {
526
+ hasConstraints: constraintRows.length > 0,
527
+ rows: constraintRows,
528
+ };
529
+ // ---- Fields (merged Storage + Constraints) — the single per-field table
530
+ // the new entity-page template renders. Same source of truth as the two
531
+ // legacy tables, just folded into one row.
532
+ const fieldRows = entity
533
+ .fields()
534
+ .map((field) => buildFieldRow(entity, field, pkFieldNames, fkMap));
535
+ const fields = {
536
+ hasFields: fieldRows.length > 0,
537
+ rows: fieldRows,
538
+ };
539
+ // ---- Field details — expanded per-field section, skipping plain fields
540
+ // that the at-a-glance table already covered. The deeper "field details
541
+ // below the table" pattern adopted by Stripe / FHIR / GraphQL — keep the
542
+ // table tight, surface authoring + validation depth below.
543
+ const fieldDetailRows = [];
544
+ for (const field of entity.fields()) {
545
+ const detail = buildFieldDetail(field, pkFieldNames, fkMap);
546
+ if (detail !== undefined)
547
+ fieldDetailRows.push(detail);
548
+ }
549
+ const fieldDetails = {
550
+ hasDetails: fieldDetailRows.length > 0,
551
+ rows: fieldDetailRows,
286
552
  };
287
553
  // ---- UsedBy
288
554
  const usedByMatches = [];
@@ -294,36 +560,16 @@ export function buildEntityDocData(entity, opts) {
294
560
  continue;
295
561
  if (stripPackage(ref) !== entity.name)
296
562
  continue;
563
+ // Link to the template's own doc page. The href is derived from the SAME
564
+ // page-placement function used to write the template page, so it resolves
565
+ // in BOTH layouts (flat → `./<Tmpl>.md`; package → a correct relative path
566
+ // like `../comms/OrderEmail.md`).
567
+ const href = docPageHref(layout, docPageNode(entity), docPageNode(child));
297
568
  usedByMatches.push({
298
- bullet: `\`template.${child.subType} ${child.name}\` — uses \`${entity.name}\` as \`@payloadRef\``,
569
+ bullet: `[\`template.${child.subType} ${child.name}\`](${href}) — uses \`${entity.name}\` as \`@payloadRef\``,
299
570
  });
300
571
  }
301
572
  const usedBy = usedByMatches.length > 0 ? usedByMatches : undefined;
302
- // ---- Generated
303
- const gens = opts.generatorNames ?? new Set();
304
- const generated = [];
305
- generated.push({
306
- filename: `${entity.name}.ts`,
307
- description: "Drizzle table, Zod schemas, type aliases, enum literal unions.",
308
- });
309
- if (gens.has("queries-file") && !isValue) {
310
- generated.push({
311
- filename: `${entity.name}.queries.ts`,
312
- description: "typed CRUD helpers (find / list / create / update / delete; takes `db` as first param per ADR-0008).",
313
- });
314
- }
315
- if (gens.has("routes-file") && !isValue) {
316
- generated.push({
317
- filename: `${entity.name}.routes.ts`,
318
- description: `Fastify CRUD-5 route registration (\`register${entity.name}Routes\`).`,
319
- });
320
- }
321
- if (gens.has("routes-file-hono") && !isValue) {
322
- generated.push({
323
- filename: `${entity.name}.routes.hono.ts`,
324
- description: `Hono CRUD-5 route registration (\`register${entity.name}Routes\`).`,
325
- });
326
- }
327
573
  // Preamble header — built up exactly as the legacy emitter did.
328
574
  const preambleLines = [];
329
575
  const typeStr = `${entity.type}.${entity.subType}`;
@@ -341,6 +587,10 @@ export function buildEntityDocData(entity, opts) {
341
587
  if (desc !== undefined) {
342
588
  descriptionQuote = desc.split("\n").map((l) => `> ${l}`.trimEnd()).join("\n");
343
589
  }
590
+ // Summary — short single-line tagline. Rendered as italic lead-in just under
591
+ // the H1, ABOVE @description. Distinct enough that an entity can carry both
592
+ // (description = paragraph; summary = headline).
593
+ const summary = entitySummary(entity);
344
594
  const data = {
345
595
  generatedMarker: `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`,
346
596
  entity: {
@@ -348,13 +598,29 @@ export function buildEntityDocData(entity, opts) {
348
598
  type: typeStr,
349
599
  },
350
600
  preambleHeader,
351
- validation,
352
- generated,
601
+ fields,
602
+ fieldDetails,
603
+ constraints,
353
604
  };
354
605
  if (desc !== undefined)
355
606
  data.entity.description = desc;
607
+ if (summary !== undefined) {
608
+ data.entity.summary = summary;
609
+ data.summaryLead = `*${summary}*`;
610
+ }
356
611
  if (descriptionQuote !== undefined)
357
612
  data.descriptionQuote = descriptionQuote;
613
+ // 1-hop neighborhood diagram — every entity it FKs into + every entity that
614
+ // FKs into it. Rendered just above the Relationships section in the entity
615
+ // page template. Skipped when the entity has zero neighbors (no orphan
616
+ // empty diagram block).
617
+ const neighborhoodErBlock = hasStorage
618
+ ? renderEntityNeighborhoodErBlock(entity, root)
619
+ : undefined;
620
+ if (neighborhoodErBlock !== undefined) {
621
+ data.neighborhoodErBlock = neighborhoodErBlock;
622
+ data.hasNeighborhoodEr = true;
623
+ }
358
624
  if (src !== undefined)
359
625
  data.entity.source = src;
360
626
  if (entity.package !== undefined && entity.package !== "") {
@@ -362,7 +628,7 @@ export function buildEntityDocData(entity, opts) {
362
628
  }
363
629
  if (hasStorage) {
364
630
  data.storage = {
365
- tableHeader: "| Field | TypeScript type | SQL column | Constraints |\n|---|---|---|---|",
631
+ tableHeader: "| Column | Type | Nullable | Key |\n|---|---|---|---|",
366
632
  rows: storageRows,
367
633
  };
368
634
  data.hasStorage = true;
@@ -379,6 +645,12 @@ export function buildEntityDocData(entity, opts) {
379
645
  data.usedBy = usedBy;
380
646
  data.hasUsedBy = true;
381
647
  }
648
+ // Cross-link to the api surfaces — present ONLY when the caller computed the
649
+ // hrefs (api surfaces emitted alongside model); model-only runs stay identical.
650
+ // `last` flags the final ref so the template renders an inline ` · ` separator.
651
+ if (opts.apiRefs !== undefined) {
652
+ data.apiRefs = opts.apiRefs.map((r, i, arr) => ({ ...r, last: i === arr.length - 1 }));
653
+ }
382
654
  return data;
383
655
  }
384
656
  //# sourceMappingURL=docs-data-builder.js.map