@pattern-stack/codegen 0.2.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 (279) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +214 -0
  3. package/dist/runtime/analytics/index.d.ts +6 -0
  4. package/dist/runtime/analytics/index.js +49 -0
  5. package/dist/runtime/analytics/index.js.map +1 -0
  6. package/dist/runtime/analytics/metrics.d.ts +75 -0
  7. package/dist/runtime/analytics/metrics.js +1 -0
  8. package/dist/runtime/analytics/metrics.js.map +1 -0
  9. package/dist/runtime/analytics/packs/crm-entity-measures.d.ts +21 -0
  10. package/dist/runtime/analytics/packs/crm-entity-measures.js +1 -0
  11. package/dist/runtime/analytics/packs/crm-entity-measures.js.map +1 -0
  12. package/dist/runtime/analytics/packs/index.d.ts +3 -0
  13. package/dist/runtime/analytics/packs/index.js +1 -0
  14. package/dist/runtime/analytics/packs/index.js.map +1 -0
  15. package/dist/runtime/analytics/packs/monetary-measures.d.ts +21 -0
  16. package/dist/runtime/analytics/packs/monetary-measures.js +1 -0
  17. package/dist/runtime/analytics/packs/monetary-measures.js.map +1 -0
  18. package/dist/runtime/analytics/specs.d.ts +49 -0
  19. package/dist/runtime/analytics/specs.js +1 -0
  20. package/dist/runtime/analytics/specs.js.map +1 -0
  21. package/dist/runtime/analytics/types.d.ts +85 -0
  22. package/dist/runtime/analytics/types.js +49 -0
  23. package/dist/runtime/analytics/types.js.map +1 -0
  24. package/dist/runtime/base-classes/activity-entity-repository.d.ts +26 -0
  25. package/dist/runtime/base-classes/activity-entity-repository.js +195 -0
  26. package/dist/runtime/base-classes/activity-entity-repository.js.map +1 -0
  27. package/dist/runtime/base-classes/activity-entity-service.d.ts +39 -0
  28. package/dist/runtime/base-classes/activity-entity-service.js +214 -0
  29. package/dist/runtime/base-classes/activity-entity-service.js.map +1 -0
  30. package/dist/runtime/base-classes/base-read-use-cases.d.ts +68 -0
  31. package/dist/runtime/base-classes/base-read-use-cases.js +32 -0
  32. package/dist/runtime/base-classes/base-read-use-cases.js.map +1 -0
  33. package/dist/runtime/base-classes/base-repository.d.ts +99 -0
  34. package/dist/runtime/base-classes/base-repository.js +160 -0
  35. package/dist/runtime/base-classes/base-repository.js.map +1 -0
  36. package/dist/runtime/base-classes/base-service.d.ts +98 -0
  37. package/dist/runtime/base-classes/base-service.js +186 -0
  38. package/dist/runtime/base-classes/base-service.js.map +1 -0
  39. package/dist/runtime/base-classes/index.d.ts +18 -0
  40. package/dist/runtime/base-classes/index.js +617 -0
  41. package/dist/runtime/base-classes/index.js.map +1 -0
  42. package/dist/runtime/base-classes/knowledge-entity-repository.d.ts +17 -0
  43. package/dist/runtime/base-classes/knowledge-entity-repository.js +166 -0
  44. package/dist/runtime/base-classes/knowledge-entity-repository.js.map +1 -0
  45. package/dist/runtime/base-classes/knowledge-entity-service.d.ts +15 -0
  46. package/dist/runtime/base-classes/knowledge-entity-service.js +192 -0
  47. package/dist/runtime/base-classes/knowledge-entity-service.js.map +1 -0
  48. package/dist/runtime/base-classes/lifecycle-events.d.ts +49 -0
  49. package/dist/runtime/base-classes/lifecycle-events.js +76 -0
  50. package/dist/runtime/base-classes/lifecycle-events.js.map +1 -0
  51. package/dist/runtime/base-classes/metadata-entity-repository.d.ts +27 -0
  52. package/dist/runtime/base-classes/metadata-entity-repository.js +212 -0
  53. package/dist/runtime/base-classes/metadata-entity-repository.js.map +1 -0
  54. package/dist/runtime/base-classes/metadata-entity-service.d.ts +39 -0
  55. package/dist/runtime/base-classes/metadata-entity-service.js +214 -0
  56. package/dist/runtime/base-classes/metadata-entity-service.js.map +1 -0
  57. package/dist/runtime/base-classes/synced-entity-repository.d.ts +32 -0
  58. package/dist/runtime/base-classes/synced-entity-repository.js +203 -0
  59. package/dist/runtime/base-classes/synced-entity-repository.js.map +1 -0
  60. package/dist/runtime/base-classes/synced-entity-service.d.ts +41 -0
  61. package/dist/runtime/base-classes/synced-entity-service.js +215 -0
  62. package/dist/runtime/base-classes/synced-entity-service.js.map +1 -0
  63. package/dist/runtime/base-classes/with-analytics.d.ts +18 -0
  64. package/dist/runtime/base-classes/with-analytics.js +11 -0
  65. package/dist/runtime/base-classes/with-analytics.js.map +1 -0
  66. package/dist/runtime/constants/tokens.d.ts +29 -0
  67. package/dist/runtime/constants/tokens.js +8 -0
  68. package/dist/runtime/constants/tokens.js.map +1 -0
  69. package/dist/runtime/subsystems/analytics/analytics-query.protocol.d.ts +30 -0
  70. package/dist/runtime/subsystems/analytics/analytics-query.protocol.js +1 -0
  71. package/dist/runtime/subsystems/analytics/analytics-query.protocol.js.map +1 -0
  72. package/dist/runtime/subsystems/analytics/analytics.module.d.ts +34 -0
  73. package/dist/runtime/subsystems/analytics/analytics.module.js +117 -0
  74. package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -0
  75. package/dist/runtime/subsystems/analytics/analytics.tokens.d.ts +24 -0
  76. package/dist/runtime/subsystems/analytics/analytics.tokens.js +10 -0
  77. package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -0
  78. package/dist/runtime/subsystems/analytics/cube-backend.d.ts +28 -0
  79. package/dist/runtime/subsystems/analytics/cube-backend.js +71 -0
  80. package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -0
  81. package/dist/runtime/subsystems/analytics/index.d.ts +6 -0
  82. package/dist/runtime/subsystems/analytics/index.js +122 -0
  83. package/dist/runtime/subsystems/analytics/index.js.map +1 -0
  84. package/dist/runtime/subsystems/analytics/noop-backend.d.ts +7 -0
  85. package/dist/runtime/subsystems/analytics/noop-backend.js +25 -0
  86. package/dist/runtime/subsystems/analytics/noop-backend.js.map +1 -0
  87. package/dist/runtime/subsystems/cache/cache.drizzle-backend.d.ts +43 -0
  88. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +133 -0
  89. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -0
  90. package/dist/runtime/subsystems/cache/cache.memory-backend.d.ts +21 -0
  91. package/dist/runtime/subsystems/cache/cache.memory-backend.js +100 -0
  92. package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -0
  93. package/dist/runtime/subsystems/cache/cache.module.d.ts +37 -0
  94. package/dist/runtime/subsystems/cache/cache.module.js +272 -0
  95. package/dist/runtime/subsystems/cache/cache.module.js.map +1 -0
  96. package/dist/runtime/subsystems/cache/cache.protocol.d.ts +42 -0
  97. package/dist/runtime/subsystems/cache/cache.protocol.js +1 -0
  98. package/dist/runtime/subsystems/cache/cache.protocol.js.map +1 -0
  99. package/dist/runtime/subsystems/cache/cache.schema.d.ts +64 -0
  100. package/dist/runtime/subsystems/cache/cache.schema.js +18 -0
  101. package/dist/runtime/subsystems/cache/cache.schema.js.map +1 -0
  102. package/dist/runtime/subsystems/cache/cache.tokens.d.ts +18 -0
  103. package/dist/runtime/subsystems/cache/cache.tokens.js +8 -0
  104. package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -0
  105. package/dist/runtime/subsystems/cache/index.d.ts +11 -0
  106. package/dist/runtime/subsystems/cache/index.js +277 -0
  107. package/dist/runtime/subsystems/cache/index.js.map +1 -0
  108. package/dist/runtime/subsystems/events/domain-events.schema.d.ts +187 -0
  109. package/dist/runtime/subsystems/events/domain-events.schema.js +32 -0
  110. package/dist/runtime/subsystems/events/domain-events.schema.js.map +1 -0
  111. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +38 -0
  112. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +199 -0
  113. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -0
  114. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +18 -0
  115. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +71 -0
  116. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -0
  117. package/dist/runtime/subsystems/events/event-bus.protocol.d.ts +52 -0
  118. package/dist/runtime/subsystems/events/event-bus.protocol.js +1 -0
  119. package/dist/runtime/subsystems/events/event-bus.protocol.js.map +1 -0
  120. package/dist/runtime/subsystems/events/event-bus.redis-backend.d.ts +95 -0
  121. package/dist/runtime/subsystems/events/event-bus.redis-backend.js +229 -0
  122. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -0
  123. package/dist/runtime/subsystems/events/events.module.d.ts +46 -0
  124. package/dist/runtime/subsystems/events/events.module.js +531 -0
  125. package/dist/runtime/subsystems/events/events.module.js.map +1 -0
  126. package/dist/runtime/subsystems/events/events.tokens.d.ts +19 -0
  127. package/dist/runtime/subsystems/events/events.tokens.js +8 -0
  128. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -0
  129. package/dist/runtime/subsystems/events/index.d.ts +12 -0
  130. package/dist/runtime/subsystems/events/index.js +536 -0
  131. package/dist/runtime/subsystems/events/index.js.map +1 -0
  132. package/dist/runtime/subsystems/index.d.ts +24 -0
  133. package/dist/runtime/subsystems/index.js +1643 -0
  134. package/dist/runtime/subsystems/index.js.map +1 -0
  135. package/dist/runtime/subsystems/jobs/index.d.ts +14 -0
  136. package/dist/runtime/subsystems/jobs/index.js +680 -0
  137. package/dist/runtime/subsystems/jobs/index.js.map +1 -0
  138. package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.d.ts +54 -0
  139. package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js +186 -0
  140. package/dist/runtime/subsystems/jobs/job-queue.bullmq-backend.js.map +1 -0
  141. package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.d.ts +38 -0
  142. package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js +228 -0
  143. package/dist/runtime/subsystems/jobs/job-queue.drizzle-backend.js.map +1 -0
  144. package/dist/runtime/subsystems/jobs/job-queue.memory-backend.d.ts +12 -0
  145. package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js +44 -0
  146. package/dist/runtime/subsystems/jobs/job-queue.memory-backend.js.map +1 -0
  147. package/dist/runtime/subsystems/jobs/job-queue.protocol.d.ts +48 -0
  148. package/dist/runtime/subsystems/jobs/job-queue.protocol.js +1 -0
  149. package/dist/runtime/subsystems/jobs/job-queue.protocol.js.map +1 -0
  150. package/dist/runtime/subsystems/jobs/job-queue.redis-backend.d.ts +46 -0
  151. package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js +187 -0
  152. package/dist/runtime/subsystems/jobs/job-queue.redis-backend.js.map +1 -0
  153. package/dist/runtime/subsystems/jobs/job-queue.schema.d.ts +237 -0
  154. package/dist/runtime/subsystems/jobs/job-queue.schema.js +44 -0
  155. package/dist/runtime/subsystems/jobs/job-queue.schema.js.map +1 -0
  156. package/dist/runtime/subsystems/jobs/jobs.module.d.ts +18 -0
  157. package/dist/runtime/subsystems/jobs/jobs.module.js +676 -0
  158. package/dist/runtime/subsystems/jobs/jobs.module.js.map +1 -0
  159. package/dist/runtime/subsystems/jobs/jobs.tokens.d.ts +13 -0
  160. package/dist/runtime/subsystems/jobs/jobs.tokens.js +8 -0
  161. package/dist/runtime/subsystems/jobs/jobs.tokens.js.map +1 -0
  162. package/dist/runtime/subsystems/storage/index.d.ts +6 -0
  163. package/dist/runtime/subsystems/storage/index.js +204 -0
  164. package/dist/runtime/subsystems/storage/index.js.map +1 -0
  165. package/dist/runtime/subsystems/storage/storage.local-backend.d.ts +18 -0
  166. package/dist/runtime/subsystems/storage/storage.local-backend.js +108 -0
  167. package/dist/runtime/subsystems/storage/storage.local-backend.js.map +1 -0
  168. package/dist/runtime/subsystems/storage/storage.memory-backend.d.ts +28 -0
  169. package/dist/runtime/subsystems/storage/storage.memory-backend.js +72 -0
  170. package/dist/runtime/subsystems/storage/storage.memory-backend.js.map +1 -0
  171. package/dist/runtime/subsystems/storage/storage.module.d.ts +40 -0
  172. package/dist/runtime/subsystems/storage/storage.module.js +201 -0
  173. package/dist/runtime/subsystems/storage/storage.module.js.map +1 -0
  174. package/dist/runtime/subsystems/storage/storage.protocol.d.ts +69 -0
  175. package/dist/runtime/subsystems/storage/storage.protocol.js +1 -0
  176. package/dist/runtime/subsystems/storage/storage.protocol.js.map +1 -0
  177. package/dist/runtime/subsystems/storage/storage.tokens.d.ts +11 -0
  178. package/dist/runtime/subsystems/storage/storage.tokens.js +6 -0
  179. package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -0
  180. package/dist/runtime/subsystems/storage/storage.utils.d.ts +9 -0
  181. package/dist/runtime/subsystems/storage/storage.utils.js +18 -0
  182. package/dist/runtime/subsystems/storage/storage.utils.js.map +1 -0
  183. package/dist/runtime/types/drizzle.d.ts +17 -0
  184. package/dist/runtime/types/drizzle.js +1 -0
  185. package/dist/runtime/types/drizzle.js.map +1 -0
  186. package/dist/src/cli/index.d.ts +1 -0
  187. package/dist/src/cli/index.js +7365 -0
  188. package/dist/src/cli/index.js.map +1 -0
  189. package/dist/src/index.d.ts +2384 -0
  190. package/dist/src/index.js +2198 -0
  191. package/dist/src/index.js.map +1 -0
  192. package/package.json +114 -0
  193. package/templates/broadcast/new/backend-interface.ejs.t +47 -0
  194. package/templates/broadcast/new/bridge-listener.ejs.t +67 -0
  195. package/templates/broadcast/new/channel.ejs.t +77 -0
  196. package/templates/broadcast/new/index.ejs.t +21 -0
  197. package/templates/broadcast/new/memory-backend.ejs.t +87 -0
  198. package/templates/broadcast/new/module.ejs.t +57 -0
  199. package/templates/broadcast/new/prompt.js +268 -0
  200. package/templates/broadcast/new/websocket-backend.ejs.t +259 -0
  201. package/templates/entity/new/backend/application/commands/create.ejs.t +55 -0
  202. package/templates/entity/new/backend/application/commands/delete.ejs.t +45 -0
  203. package/templates/entity/new/backend/application/commands/grouped-index.ejs.t +149 -0
  204. package/templates/entity/new/backend/application/commands/index.ejs.t +15 -0
  205. package/templates/entity/new/backend/application/commands/update.ejs.t +58 -0
  206. package/templates/entity/new/backend/application/queries/declarative-queries.ejs.t +36 -0
  207. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +42 -0
  208. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +81 -0
  209. package/templates/entity/new/backend/application/queries/index.ejs.t +14 -0
  210. package/templates/entity/new/backend/application/queries/list.ejs.t +36 -0
  211. package/templates/entity/new/backend/application/schemas/_inject-index.ejs.t +7 -0
  212. package/templates/entity/new/backend/application/schemas/dto.ejs.t +45 -0
  213. package/templates/entity/new/backend/database/_inject-index.ejs.t +7 -0
  214. package/templates/entity/new/backend/database/electric-migration.ejs.t +21 -0
  215. package/templates/entity/new/backend/database/repository.ejs.t +450 -0
  216. package/templates/entity/new/backend/database/schema.ejs.t +248 -0
  217. package/templates/entity/new/backend/domain/_inject-index.ejs.t +12 -0
  218. package/templates/entity/new/backend/domain/entity.ejs.t +108 -0
  219. package/templates/entity/new/backend/domain/grouped-index.ejs.t +163 -0
  220. package/templates/entity/new/backend/domain/index.ejs.t +15 -0
  221. package/templates/entity/new/backend/domain/repository-interface.ejs.t +71 -0
  222. package/templates/entity/new/backend/modules/core/_ensure-anchor-tokens.ejs.t +10 -0
  223. package/templates/entity/new/backend/modules/core/_inject-token.ejs.t +7 -0
  224. package/templates/entity/new/backend/modules/core/module.ejs.t +67 -0
  225. package/templates/entity/new/backend/modules/trpc/module.ejs.t +67 -0
  226. package/templates/entity/new/backend/presentation/controller.ejs.t +201 -0
  227. package/templates/entity/new/clean-lite-ps/controller.ejs.t +37 -0
  228. package/templates/entity/new/clean-lite-ps/dto/create.ejs.t +17 -0
  229. package/templates/entity/new/clean-lite-ps/dto/output.ejs.t +25 -0
  230. package/templates/entity/new/clean-lite-ps/dto/update.ejs.t +11 -0
  231. package/templates/entity/new/clean-lite-ps/entity.ejs.t +52 -0
  232. package/templates/entity/new/clean-lite-ps/index.ejs.t +20 -0
  233. package/templates/entity/new/clean-lite-ps/module.ejs.t +43 -0
  234. package/templates/entity/new/clean-lite-ps/prompt-extension.js +617 -0
  235. package/templates/entity/new/clean-lite-ps/repository.ejs.t +62 -0
  236. package/templates/entity/new/clean-lite-ps/service.ejs.t +34 -0
  237. package/templates/entity/new/clean-lite-ps/use-cases/declarative-queries.ejs.t +34 -0
  238. package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +16 -0
  239. package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +16 -0
  240. package/templates/entity/new/frontend/_inject-entities-entry.ejs.t +7 -0
  241. package/templates/entity/new/frontend/_inject-entities-import.ejs.t +7 -0
  242. package/templates/entity/new/frontend/collections/_ensure-anchor-collections.ejs.t +10 -0
  243. package/templates/entity/new/frontend/collections/_inject-index.ejs.t +9 -0
  244. package/templates/entity/new/frontend/collections/_inject-schema-import.ejs.t +9 -0
  245. package/templates/entity/new/frontend/collections/collection.ejs.t +61 -0
  246. package/templates/entity/new/frontend/collections/collections-base.ejs.t +24 -0
  247. package/templates/entity/new/frontend/entity/collection.ejs.t +172 -0
  248. package/templates/entity/new/frontend/entity/combined.ejs.t +474 -0
  249. package/templates/entity/new/frontend/entity/fields.ejs.t +104 -0
  250. package/templates/entity/new/frontend/entity/hooks.ejs.t +73 -0
  251. package/templates/entity/new/frontend/entity/index.ejs.t +21 -0
  252. package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +84 -0
  253. package/templates/entity/new/frontend/entity/mutations.ejs.t +38 -0
  254. package/templates/entity/new/frontend/entity/types.ejs.t +59 -0
  255. package/templates/entity/new/frontend/generated/_inject-index-export.ejs.t +7 -0
  256. package/templates/entity/new/frontend/generated/_inject-index-import.ejs.t +7 -0
  257. package/templates/entity/new/frontend/generated/_inject-index-registry.ejs.t +7 -0
  258. package/templates/entity/new/frontend/store/_inject-collection-import.ejs.t +9 -0
  259. package/templates/entity/new/frontend/store/_inject-collections.ejs.t +9 -0
  260. package/templates/entity/new/frontend/store/_inject-entity.ejs.t +9 -0
  261. package/templates/entity/new/frontend/store/_inject-import.ejs.t +9 -0
  262. package/templates/entity/new/frontend/store/_inject-lookups.ejs.t +9 -0
  263. package/templates/entity/new/frontend/store/_inject-resolve.ejs.t +10 -0
  264. package/templates/entity/new/frontend/store/hooks.ejs.t +72 -0
  265. package/templates/entity/new/frontend/unified-entity.ejs.t +28 -0
  266. package/templates/entity/new/prompt.js +1421 -0
  267. package/templates/relationship/new/controller.ejs.t +36 -0
  268. package/templates/relationship/new/dto/create.ejs.t +41 -0
  269. package/templates/relationship/new/dto/output.ejs.t +44 -0
  270. package/templates/relationship/new/dto/update.ejs.t +10 -0
  271. package/templates/relationship/new/entity.ejs.t +98 -0
  272. package/templates/relationship/new/index.ejs.t +19 -0
  273. package/templates/relationship/new/module.ejs.t +35 -0
  274. package/templates/relationship/new/prompt.js +682 -0
  275. package/templates/relationship/new/repository.ejs.t +54 -0
  276. package/templates/relationship/new/service.ejs.t +31 -0
  277. package/templates/relationship/new/use-cases/declarative-queries.ejs.t +34 -0
  278. package/templates/relationship/new/use-cases/find-by-id.ejs.t +16 -0
  279. package/templates/relationship/new/use-cases/list.ejs.t +16 -0
@@ -0,0 +1,1421 @@
1
+ /**
2
+ * Hygen prompt.js - Loads entity YAML and prepares template locals
3
+ *
4
+ * Usage: bunx hygen entity new --yaml entities/opportunity.yaml
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import yaml from "yaml";
10
+ import {
11
+ BACKEND_LAYERS,
12
+ BASE_PATHS,
13
+ FOLDER_STRUCTURES,
14
+ FILE_GROUPINGS,
15
+ LOCATIONS,
16
+ getEntityPaths,
17
+ getEntityFileNames,
18
+ getImportPaths,
19
+ getLayoutConfig,
20
+ getDatabaseDialect,
21
+ getProjectConfig,
22
+ getPipelinesConfig,
23
+ getGenerateConfig,
24
+ } from "../../../src/config/paths.mjs";
25
+ import { getNamingConfig } from "../../../src/config/naming-config.mjs";
26
+
27
+ // ============================================================================
28
+ // Behavior Registry (inline to avoid import issues with Hygen)
29
+ // ============================================================================
30
+
31
+ const behaviorRegistry = {
32
+ timestamps: {
33
+ name: "timestamps",
34
+ fields: [
35
+ {
36
+ name: "created_at",
37
+ camelName: "createdAt",
38
+ type: "datetime",
39
+ tsType: "Date",
40
+ drizzleType: "timestamp",
41
+ zodType: "z.coerce.date()",
42
+ nullable: false,
43
+ },
44
+ {
45
+ name: "updated_at",
46
+ camelName: "updatedAt",
47
+ type: "datetime",
48
+ tsType: "Date",
49
+ drizzleType: "timestamp",
50
+ zodType: "z.coerce.date()",
51
+ nullable: false,
52
+ },
53
+ ],
54
+ drizzleImports: ["timestamp"],
55
+ configKey: "timestamps",
56
+ },
57
+ soft_delete: {
58
+ name: "soft_delete",
59
+ fields: [
60
+ {
61
+ name: "deleted_at",
62
+ camelName: "deletedAt",
63
+ type: "datetime",
64
+ tsType: "Date | null",
65
+ drizzleType: "timestamp",
66
+ zodType: "z.coerce.date().nullable()",
67
+ nullable: true,
68
+ },
69
+ ],
70
+ drizzleImports: ["timestamp"],
71
+ configKey: "softDelete",
72
+ },
73
+ user_tracking: {
74
+ name: "user_tracking",
75
+ fields: [
76
+ {
77
+ name: "created_by",
78
+ camelName: "createdBy",
79
+ type: "uuid",
80
+ tsType: "string | null",
81
+ drizzleType: "uuid",
82
+ zodType: "z.string().uuid().nullable()",
83
+ nullable: true,
84
+ foreignKey: "users.id",
85
+ },
86
+ {
87
+ name: "updated_by",
88
+ camelName: "updatedBy",
89
+ type: "uuid",
90
+ tsType: "string | null",
91
+ drizzleType: "uuid",
92
+ zodType: "z.string().uuid().nullable()",
93
+ nullable: true,
94
+ foreignKey: "users.id",
95
+ },
96
+ ],
97
+ drizzleImports: ["uuid"],
98
+ configKey: "userTracking",
99
+ },
100
+ temporal_validity: {
101
+ name: "temporal_validity",
102
+ fields: [
103
+ {
104
+ name: "valid_from",
105
+ camelName: "validFrom",
106
+ type: "datetime",
107
+ tsType: "Date | null",
108
+ drizzleType: "timestamp",
109
+ zodType: "z.coerce.date().nullable()",
110
+ nullable: true,
111
+ },
112
+ {
113
+ name: "valid_to",
114
+ camelName: "validTo",
115
+ type: "datetime",
116
+ tsType: "Date | null",
117
+ drizzleType: "timestamp",
118
+ zodType: "z.coerce.date().nullable()",
119
+ nullable: true,
120
+ },
121
+ {
122
+ name: "is_active",
123
+ camelName: "isActive",
124
+ type: "boolean",
125
+ tsType: "boolean",
126
+ drizzleType: "boolean",
127
+ zodType: "z.boolean()",
128
+ nullable: false,
129
+ default: true,
130
+ },
131
+ ],
132
+ drizzleImports: ["timestamp", "boolean"],
133
+ configKey: "temporalValidity",
134
+ },
135
+ };
136
+
137
+ /**
138
+ * Load codegen config from codegen.config.yaml
139
+ */
140
+ function loadCodegenConfig(cwd) {
141
+ const configPath = path.resolve(cwd, "codegen.config.yaml");
142
+ const defaultConfig = { behaviors: { strategy: "inline" } };
143
+
144
+ if (!fs.existsSync(configPath)) {
145
+ return defaultConfig;
146
+ }
147
+
148
+ try {
149
+ const content = fs.readFileSync(configPath, "utf-8");
150
+ const parsed = yaml.parse(content);
151
+
152
+ return {
153
+ behaviors: {
154
+ strategy: parsed?.behaviors?.strategy || "inline",
155
+ },
156
+ };
157
+ } catch {
158
+ return defaultConfig;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Normalize behavior config (string or object with name/options)
164
+ */
165
+ function normalizeBehaviorConfig(config) {
166
+ if (typeof config === "string") {
167
+ return { name: config, options: {} };
168
+ }
169
+ return { name: config.name, options: config.options || {} };
170
+ }
171
+
172
+ /**
173
+ * Resolve behaviors from entity YAML
174
+ */
175
+ function resolveBehaviors(behaviorConfigs) {
176
+ const configs = (behaviorConfigs || []).map(normalizeBehaviorConfig);
177
+ const fields = [];
178
+ const drizzleImports = new Set();
179
+ const addedFieldNames = new Set();
180
+
181
+ const enabledNames = new Set(configs.map((c) => c.name));
182
+
183
+ for (const config of configs) {
184
+ const behavior = behaviorRegistry[config.name];
185
+ if (!behavior) continue;
186
+
187
+ for (const field of behavior.fields) {
188
+ if (!addedFieldNames.has(field.name)) {
189
+ fields.push(field);
190
+ addedFieldNames.add(field.name);
191
+ }
192
+ }
193
+
194
+ for (const imp of behavior.drizzleImports) {
195
+ drizzleImports.add(imp);
196
+ }
197
+ }
198
+
199
+ const hasTimestamps = enabledNames.has("timestamps");
200
+ const hasSoftDelete = enabledNames.has("soft_delete");
201
+ const hasUserTracking = enabledNames.has("user_tracking");
202
+ const hasTemporalValidity = enabledNames.has("temporal_validity");
203
+
204
+ return {
205
+ configs,
206
+ fields,
207
+ drizzleImports: Array.from(drizzleImports).sort(),
208
+ repositoryConfig: {
209
+ timestamps: hasTimestamps,
210
+ softDelete: hasSoftDelete,
211
+ userTracking: hasUserTracking,
212
+ temporalValidity: hasTemporalValidity,
213
+ versionable: false,
214
+ },
215
+ hasBehaviors: configs.length > 0,
216
+ hasTimestamps,
217
+ hasSoftDelete,
218
+ hasUserTracking,
219
+ hasTemporalValidity,
220
+ };
221
+ }
222
+
223
+ export default {
224
+ prompt: async ({ args }) => {
225
+ const yamlPath = args.yaml;
226
+ if (!yamlPath) {
227
+ throw new Error(
228
+ "Missing --yaml argument. Usage: bunx hygen entity new --yaml entities/opportunity.yaml",
229
+ );
230
+ }
231
+
232
+ // Load and parse YAML
233
+ const fullPath = path.resolve(process.cwd(), yamlPath);
234
+ if (!fs.existsSync(fullPath)) {
235
+ throw new Error(`File not found: ${fullPath}`);
236
+ }
237
+
238
+ const content = fs.readFileSync(fullPath, "utf-8");
239
+ const definition = yaml.parse(content);
240
+
241
+ // Load global codegen config
242
+ const codegenConfig = loadCodegenConfig(process.cwd());
243
+
244
+ // Load frontend config from project config (used for auth, sync, parsers)
245
+ const frontendConfig = getProjectConfig()?.frontend ?? {};
246
+ const frontendSync = frontendConfig.sync ?? {};
247
+
248
+ // Prepare locals for templates
249
+ const entity = definition.entity;
250
+ const fields = definition.fields || {};
251
+ const relationships = definition.relationships || {};
252
+ const behaviors = definition.behaviors || [];
253
+
254
+ // v2 blocks (optional — absent in v1 entities)
255
+ const queriesBlock = definition.queries || null;
256
+ const syncBlock = definition.sync || null;
257
+ const eventsBlock = definition.events || null;
258
+
259
+ // Helper functions
260
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
261
+ const camelCase = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
262
+ const pascalCase = (s) => capitalize(camelCase(s));
263
+ const pluralize = (s) => {
264
+ if (s.endsWith("y")) return s.slice(0, -1) + "ies";
265
+ if (
266
+ s.endsWith("s") ||
267
+ s.endsWith("x") ||
268
+ s.endsWith("ch") ||
269
+ s.endsWith("sh")
270
+ )
271
+ return s + "es";
272
+ return s + "s";
273
+ };
274
+
275
+ // ============================================================================
276
+ // UI Metadata Inference Functions
277
+ // ============================================================================
278
+
279
+ /**
280
+ * Format field name as human-readable label
281
+ * e.g., "created_at" -> "Created At", "account_id" -> "Account Id"
282
+ */
283
+ const formatLabel = (fieldName) => {
284
+ return fieldName
285
+ .replace(/_/g, " ")
286
+ .replace(/\b\w/g, (c) => c.toUpperCase());
287
+ };
288
+
289
+ /**
290
+ * Infer UI type from field definition
291
+ * Considers explicit ui_type, field type, choices, foreign keys, and name patterns
292
+ */
293
+ const inferUiType = (fieldName, field) => {
294
+ // If explicit ui_type provided, use it
295
+ if (field.ui_type) return field.ui_type;
296
+
297
+ // Check for choices (enum)
298
+ if (Array.isArray(field.choices) && field.choices.length > 0)
299
+ return "enum";
300
+
301
+ // Check for foreign key (reference)
302
+ if (field.foreign_key) return "reference";
303
+
304
+ // Check field name patterns
305
+ const nameLower = fieldName.toLowerCase();
306
+ if (nameLower.includes("email")) return "email";
307
+ if (nameLower.includes("url") || nameLower.includes("website"))
308
+ return "url";
309
+ if (nameLower.includes("password")) return "password";
310
+ if (
311
+ nameLower.includes("price") ||
312
+ nameLower.includes("amount") ||
313
+ nameLower.includes("cost") ||
314
+ nameLower.includes("value") ||
315
+ nameLower.includes("revenue")
316
+ )
317
+ return "money";
318
+ if (nameLower.includes("percent") || nameLower.includes("rate"))
319
+ return "percentage";
320
+
321
+ // Infer from field type
322
+ const typeMap = {
323
+ string:
324
+ field.max_length && field.max_length > 500 ? "textarea" : "text",
325
+ integer: "number",
326
+ decimal: "number",
327
+ boolean: "boolean",
328
+ uuid: "text",
329
+ date: "date",
330
+ datetime: "datetime",
331
+ json: "json",
332
+ };
333
+
334
+ return typeMap[field.type] || "text";
335
+ };
336
+
337
+ /**
338
+ * Infer UI group from field name patterns
339
+ */
340
+ const inferUiGroup = (fieldName, field) => {
341
+ if (field.ui_group) return field.ui_group;
342
+
343
+ const nameLower = fieldName.toLowerCase();
344
+
345
+ // Common field groupings
346
+ if (["id", "uuid"].includes(nameLower)) return "identification";
347
+ if (["created_at", "updated_at", "deleted_at"].includes(nameLower))
348
+ return "metadata";
349
+ if (
350
+ nameLower.includes("price") ||
351
+ nameLower.includes("amount") ||
352
+ nameLower.includes("cost") ||
353
+ nameLower.includes("value") ||
354
+ nameLower.includes("revenue")
355
+ )
356
+ return "financial";
357
+ if (nameLower.includes("email") || nameLower.includes("phone"))
358
+ return "contact";
359
+ if (nameLower.includes("name") || nameLower.includes("title"))
360
+ return "identification";
361
+ if (nameLower.includes("description") || nameLower.includes("notes"))
362
+ return "content";
363
+ if (
364
+ nameLower.includes("status") ||
365
+ nameLower.includes("state") ||
366
+ nameLower.includes("stage")
367
+ )
368
+ return "status";
369
+ if (field.foreign_key) return "relationships";
370
+
371
+ return "general";
372
+ };
373
+
374
+ /**
375
+ * Infer UI importance from field properties
376
+ */
377
+ const inferUiImportance = (fieldName, field) => {
378
+ if (field.ui_importance) return field.ui_importance;
379
+
380
+ const nameLower = fieldName.toLowerCase();
381
+
382
+ // Auto-generated fields are tertiary
383
+ if (["id", "created_at", "updated_at", "deleted_at"].includes(nameLower))
384
+ return "tertiary";
385
+
386
+ // Foreign keys that are likely internal references
387
+ if (field.foreign_key && nameLower.endsWith("_id")) return "secondary";
388
+
389
+ // Required fields are primary by default
390
+ if (field.required) return "primary";
391
+
392
+ // Name/title fields are typically primary
393
+ if (nameLower.includes("name") || nameLower.includes("title"))
394
+ return "primary";
395
+
396
+ return "secondary";
397
+ };
398
+
399
+ // Entity name variations
400
+ const name = entity.name; // opportunity
401
+ const plural = entity.plural; // opportunities
402
+ const table = entity.table; // opportunities
403
+ const className = pascalCase(name); // Opportunity
404
+ const classNamePlural = pascalCase(plural); // Opportunities
405
+ const camelName = camelCase(name); // opportunity
406
+ const repositoryToken = `${pascalCase(name).toUpperCase()}_REPOSITORY`; // OPPORTUNITY_REPOSITORY
407
+
408
+ // Frontend store naming
409
+ const singularCamelName = camelCase(name); // "dealState" from "deal_state"
410
+ const pluralCamelName = camelCase(plural); // "dealStates" from "deal_states"
411
+ const collectionVarName = singularCamelName + "Collection"; // "dealStateCollection"
412
+ const collectionVarNamePlural = pluralCamelName + "Collection"; // "dealStatesCollection"
413
+
414
+ // Layout configuration (folder structure + file grouping)
415
+ // See tools/codegen/config/paths.js for options
416
+ const layout = getLayoutConfig(entity);
417
+ const { folderStructure, fileGrouping, isNested, isGrouped } = layout;
418
+
419
+ // Behavior strategy (base_class vs inline)
420
+ // Per-entity override takes precedence over global config
421
+ const behaviorStrategy =
422
+ entity.behavior_strategy || codegenConfig.behaviors.strategy;
423
+
424
+ // Resolve behaviors
425
+ const resolvedBehaviors = resolveBehaviors(behaviors);
426
+
427
+ // Compute paths using centralized config
428
+ // See tools/codegen/config/paths.js for path definitions
429
+ const paths = getEntityPaths({ name, plural, isNested, isGrouped });
430
+
431
+ // Load naming configuration for file naming
432
+ const namingConfig = getNamingConfig();
433
+
434
+ // File names using centralized config with naming configuration
435
+ const fileNames = getEntityFileNames({ name, plural, isNested, isGrouped, namingConfig });
436
+
437
+ // Terminology-aware class name suffixes
438
+ // Supports 'command' vs 'use-case' naming for application layer
439
+ const applicationLayerSuffix = namingConfig.terminology.command === 'use-case' ? 'UseCase' : 'Command';
440
+ const queryLayerSuffix = namingConfig.terminology.query === 'use-case' ? 'UseCase' : 'Query';
441
+
442
+ // Pre-computed class names using configured terminology
443
+ const createCommandClass = `Create${className}${applicationLayerSuffix}`;
444
+ const updateCommandClass = `Update${className}${applicationLayerSuffix}`;
445
+ const deleteCommandClass = `Delete${className}${applicationLayerSuffix}`;
446
+ const getByIdQueryClass = `Get${className}ById${queryLayerSuffix}`;
447
+ const listQueryClass = `List${classNamePlural}${queryLayerSuffix}`;
448
+
449
+ // Step 1: Compute all possible output paths
450
+ const src = BASE_PATHS.backendSrc;
451
+ const allPaths = {
452
+ // Domain layer
453
+ entity: `${src}/${paths.domain}/${fileNames.entity}`,
454
+ repositoryInterface: `${src}/${paths.domain}/${fileNames.repositoryInterface}`,
455
+ domainGroupedIndex: `${src}/${paths.domain}/index.ts`,
456
+
457
+ // Application layer - commands
458
+ createCommand: `${src}/${paths.commands}/${fileNames.createCommand}`,
459
+ updateCommand: `${src}/${paths.commands}/${fileNames.updateCommand}`,
460
+ deleteCommand: `${src}/${paths.commands}/${fileNames.deleteCommand}`,
461
+ commandsIndex: `${src}/${paths.commands}/index.ts`,
462
+
463
+ // Application layer - queries
464
+ getByIdQuery: `${src}/${paths.queries}/${fileNames.getByIdQuery}`,
465
+ listQuery: `${src}/${paths.queries}/${fileNames.listQuery}`,
466
+ queriesIndex: `${src}/${paths.queries}/index.ts`,
467
+
468
+ // Application layer - schemas (always generated)
469
+ dto: `${src}/${paths.schemas}/${fileNames.dto}`,
470
+
471
+ // Infrastructure layer (always generated)
472
+ drizzleSchema: `${src}/${paths.drizzle}/${fileNames.schema}`,
473
+ repository: `${src}/${paths.repositories}/${fileNames.repository}`,
474
+
475
+ // Presentation layer (always generated)
476
+ controller: `${src}/${paths.controllers}/${fileNames.controller}`,
477
+
478
+ // Modules (always generated)
479
+ module: `${src}/${paths.modules}/${fileNames.module}`,
480
+ };
481
+
482
+ // Step 2: Apply mode filter (null = skip generation)
483
+ const outputPaths = {
484
+ // Domain: separate files OR grouped index
485
+ entity: !isGrouped ? allPaths.entity : null,
486
+ repositoryInterface: !isGrouped ? allPaths.repositoryInterface : null,
487
+ domainGroupedIndex: isGrouped ? allPaths.domainGroupedIndex : null,
488
+
489
+ // Commands: separate files OR grouped index
490
+ createCommand: !isGrouped ? allPaths.createCommand : null,
491
+ updateCommand: !isGrouped ? allPaths.updateCommand : null,
492
+ deleteCommand: !isGrouped ? allPaths.deleteCommand : null,
493
+ commandsIndex: (!isGrouped && isNested) ? allPaths.commandsIndex : null,
494
+ commandsGroupedIndex: isGrouped ? allPaths.commandsIndex : null,
495
+
496
+ // Queries: separate files OR grouped index
497
+ getByIdQuery: !isGrouped ? allPaths.getByIdQuery : null,
498
+ listQuery: !isGrouped ? allPaths.listQuery : null,
499
+ queriesIndex: (!isGrouped && isNested) ? allPaths.queriesIndex : null,
500
+ queriesGroupedIndex: isGrouped ? allPaths.queriesIndex : null,
501
+
502
+ // Always generated (mode-independent)
503
+ dto: allPaths.dto,
504
+ drizzleSchema: allPaths.drizzleSchema,
505
+ repository: allPaths.repository,
506
+ controller: allPaths.controller,
507
+ module: allPaths.module,
508
+ };
509
+
510
+ // Import paths using centralized config
511
+ // See tools/codegen/config/paths.js for path definitions
512
+ const importHelpers = getImportPaths({ isNested });
513
+ const imports = {
514
+ // From commands/queries to other locations
515
+ constants: importHelpers.constants(name),
516
+ domain: importHelpers.domain(name),
517
+ schemas: importHelpers.schemas(name),
518
+ // From domain to other domain files (same folder when nested)
519
+ domainEntity: importHelpers.domainEntity(name),
520
+ // From module (modules/) to commands/queries
521
+ moduleToGetByIdQuery: importHelpers.moduleToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
522
+ moduleToListQuery: importHelpers.moduleToQuery(name, fileNames.listQuery.replace('.ts', '')),
523
+ moduleToDeclarativeQueries: importHelpers.moduleToQuery(name, 'declarative-queries'),
524
+ moduleToCreateCommand: importHelpers.moduleToCommand(name, fileNames.createCommand.replace('.ts', '')),
525
+ moduleToUpdateCommand: importHelpers.moduleToCommand(name, fileNames.updateCommand.replace('.ts', '')),
526
+ moduleToDeleteCommand: importHelpers.moduleToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
527
+ moduleToRepository: importHelpers.moduleToRepository(fileNames.repository.replace('.ts', '')),
528
+ moduleToConstants: importHelpers.moduleToConstants(),
529
+ moduleToDatabaseModule: importHelpers.moduleToDatabaseModule(),
530
+ moduleToController: importHelpers.moduleToController(fileNames.controller.replace('.ts', '')),
531
+ // From controller (presentation/rest/) to queries/commands
532
+ controllerToGetByIdQuery: importHelpers.controllerToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
533
+ controllerToListQuery: importHelpers.controllerToQuery(name, fileNames.listQuery.replace('.ts', '')),
534
+ controllerToCreateCommand: importHelpers.controllerToCommand(name, fileNames.createCommand.replace('.ts', '')),
535
+ controllerToUpdateCommand: importHelpers.controllerToCommand(name, fileNames.updateCommand.replace('.ts', '')),
536
+ controllerToDeleteCommand: importHelpers.controllerToCommand(name, fileNames.deleteCommand.replace('.ts', '')),
537
+ controllerToSchemas: importHelpers.controllerToSchemas(),
538
+ controllerToDomain: importHelpers.controllerToDomain(),
539
+ // From app.module.ts to modules
540
+ appModuleToModule: importHelpers.appModuleToModule(fileNames.module.replace('.ts', '')),
541
+ appModuleToTrpcModule: importHelpers.appModuleToTrpcModule(`${name}-trpc.module`),
542
+ // From repository to constants (relative path)
543
+ repositoryToConstants: importHelpers.repositoryToConstants(),
544
+ // For domain/index.ts export
545
+ domainExport: isNested ? `./${name}` : null,
546
+ // Electric-related imports
547
+ controllerToAuthGuard: importHelpers.controllerToAuthGuard(),
548
+ controllerToCurrentUser: importHelpers.controllerToCurrentUser(),
549
+ controllerToElectricService: importHelpers.controllerToElectricService(),
550
+ moduleToElectricModule: importHelpers.moduleToElectricModule(),
551
+ };
552
+
553
+ // Type mappings
554
+ const tsTypes = {
555
+ string: "string",
556
+ integer: "number",
557
+ decimal: "number",
558
+ boolean: "boolean",
559
+ uuid: "string",
560
+ date: "Date",
561
+ datetime: "Date",
562
+ json: "unknown",
563
+ string_array: "string[]",
564
+ entity_ref: "EntityRef", // Placeholder - handled specially
565
+ enum: "string", // Actual type generated from choices
566
+ };
567
+
568
+ const drizzleTypes = {
569
+ string: "varchar",
570
+ integer: "integer",
571
+ decimal: "decimal",
572
+ boolean: "boolean",
573
+ uuid: "uuid",
574
+ date: "date",
575
+ datetime: "timestamp",
576
+ json: "jsonb",
577
+ string_array: "text_array",
578
+ entity_ref: "entity_ref", // Placeholder - handled specially
579
+ enum: "enum", // Placeholder - pgEnum generated
580
+ };
581
+
582
+ const zodTypes = {
583
+ string: "z.string()",
584
+ integer: "z.number().int()",
585
+ decimal: "z.number()",
586
+ boolean: "z.boolean()",
587
+ uuid: "z.string().uuid()",
588
+ date: "z.coerce.date()",
589
+ datetime: "z.coerce.date()",
590
+ json: "z.unknown()",
591
+ string_array: "z.array(z.string())",
592
+ entity_ref: "entity_ref", // Placeholder - handled specially
593
+ enum: "z.enum()", // Placeholder - choices added
594
+ };
595
+
596
+ /**
597
+ * Load choices from an external YAML file (for choices_from option)
598
+ */
599
+ const loadChoicesFromFile = (choicesFromPath, yamlDir) => {
600
+ // Try relative to entities directory first
601
+ const entitiesPath = path.resolve(
602
+ process.cwd(),
603
+ "entities",
604
+ choicesFromPath,
605
+ );
606
+ if (fs.existsSync(entitiesPath)) {
607
+ const content = fs.readFileSync(entitiesPath, "utf-8");
608
+ const parsed = yaml.parse(content);
609
+ // For relationship_types.yaml, extract keys from relationship_types section
610
+ if (parsed.relationship_types) {
611
+ return Object.keys(parsed.relationship_types);
612
+ }
613
+ // Otherwise extract top-level keys
614
+ return Object.keys(parsed);
615
+ }
616
+
617
+ // Try relative to the YAML file directory
618
+ const relativePath = path.resolve(yamlDir, choicesFromPath);
619
+ if (fs.existsSync(relativePath)) {
620
+ const content = fs.readFileSync(relativePath, "utf-8");
621
+ const parsed = yaml.parse(content);
622
+ if (parsed.relationship_types) {
623
+ return Object.keys(parsed.relationship_types);
624
+ }
625
+ return Object.keys(parsed);
626
+ }
627
+
628
+ throw new Error(`choices_from file not found: ${choicesFromPath}`);
629
+ };
630
+
631
+ // Process fields for templates
632
+ const processedFields = [];
633
+ const entityRefFields = []; // Track entity_ref fields for special handling
634
+
635
+ for (const [fieldName, field] of Object.entries(fields)) {
636
+ // Skip 'id' field - it's always added explicitly in templates to avoid duplicates
637
+ if (fieldName === 'id') continue;
638
+
639
+ // Handle entity_ref type specially - generates TWO fields
640
+ if (field.type === "entity_ref") {
641
+ const allowedTypes = field.allowed_types || [];
642
+ const baseName = fieldName;
643
+ const baseCamel = camelCase(fieldName);
644
+
645
+ // Track for later use (composite indexes, query methods)
646
+ entityRefFields.push({
647
+ name: baseName,
648
+ camelName: baseCamel,
649
+ pascalName: pascalCase(baseName),
650
+ allowedTypes,
651
+ required: field.required ?? false,
652
+ nullable: field.nullable ?? false,
653
+ });
654
+
655
+ // Generate the type field (enum)
656
+ processedFields.push({
657
+ name: `${baseName}_entity_type`,
658
+ camelName: `${baseCamel}EntityType`,
659
+ type: "entity_ref_type",
660
+ tsType: "EntityType",
661
+ drizzleType: "entity_type_enum",
662
+ zodType: "entityTypeSchema",
663
+ required: field.required ?? false,
664
+ nullable: field.nullable ?? false,
665
+ isEntityRefType: true,
666
+ entityRefBase: baseName,
667
+ allowedTypes,
668
+ // UI metadata for entity ref
669
+ ui_type: "enum",
670
+ ui_label: formatLabel(`${baseName} type`),
671
+ ui_importance: "secondary",
672
+ ui_group: "relationships",
673
+ ui_visible: false,
674
+ });
675
+
676
+ // Generate the id field (uuid)
677
+ processedFields.push({
678
+ name: `${baseName}_entity_id`,
679
+ camelName: `${baseCamel}EntityId`,
680
+ type: "entity_ref_id",
681
+ tsType: "string",
682
+ drizzleType: "uuid",
683
+ zodType: "z.string().uuid()",
684
+ required: field.required ?? false,
685
+ nullable: field.nullable ?? false,
686
+ isEntityRefId: true,
687
+ entityRefBase: baseName,
688
+ // UI metadata
689
+ ui_type: "text",
690
+ ui_label: formatLabel(`${baseName} id`),
691
+ ui_importance: "secondary",
692
+ ui_group: "relationships",
693
+ ui_visible: false,
694
+ });
695
+
696
+ continue; // Skip normal processing
697
+ }
698
+
699
+ // Handle enum type with choices or choices_from
700
+ let choices = field.choices;
701
+ if (field.type === "enum" && field.choices_from) {
702
+ try {
703
+ choices = loadChoicesFromFile(field.choices_from, path.dirname(fullPath));
704
+ } catch (e) {
705
+ console.warn(
706
+ `Warning: Could not load choices from ${field.choices_from}: ${e.message}`,
707
+ );
708
+ choices = [];
709
+ }
710
+ }
711
+
712
+ const hasChoices = Array.isArray(choices) && choices.length > 0;
713
+
714
+ // For choice fields, generate literal union type instead of string
715
+ let tsType = tsTypes[field.type] || "unknown";
716
+ if (hasChoices) {
717
+ tsType = choices.map((c) => `'${c}'`).join(" | ");
718
+ }
719
+
720
+ // For choice fields, we'll use pgEnum instead of varchar
721
+ let drizzleType = drizzleTypes[field.type] || "varchar";
722
+ if (hasChoices || field.type === "enum") {
723
+ drizzleType = "enum"; // Special marker for enum handling
724
+ }
725
+
726
+ let zodType = zodTypes[field.type] || "z.unknown()";
727
+ if (hasChoices) {
728
+ zodType = `z.enum([${choices.map((c) => `'${c}'`).join(", ")}])`;
729
+ }
730
+
731
+ // Generate enum name for Drizzle pgEnum (camelCase + 'Enum')
732
+ const enumName = hasChoices ? camelCase(fieldName) + "Enum" : null;
733
+
734
+ // Infer UI metadata with defaults
735
+ const ui_type = inferUiType(fieldName, field);
736
+ const ui_label = field.ui_label || formatLabel(fieldName);
737
+ const ui_importance = inferUiImportance(fieldName, field);
738
+ const ui_group = inferUiGroup(fieldName, field);
739
+ const ui_sortable = field.ui_sortable ?? false;
740
+ const ui_filterable = field.ui_filterable ?? false;
741
+ // Default visibility: hide id and timestamp fields
742
+ const ui_visible =
743
+ field.ui_visible ??
744
+ !["id", "created_at", "updated_at", "deleted_at"].includes(fieldName);
745
+
746
+ processedFields.push({
747
+ name: fieldName,
748
+ camelName: camelCase(fieldName),
749
+ type: field.type,
750
+ tsType,
751
+ drizzleType,
752
+ zodType,
753
+ required: field.required ?? false,
754
+ nullable: field.nullable ?? false,
755
+ maxLength: field.max_length,
756
+ minLength: field.min_length,
757
+ min: field.min,
758
+ max: field.max,
759
+ choices,
760
+ choicesFrom: field.choices_from,
761
+ hasChoices,
762
+ enumName,
763
+ default: field.default,
764
+ index: field.index ?? false,
765
+ unique: field.unique ?? false,
766
+ foreignKey: field.foreign_key,
767
+ // UI metadata
768
+ ui_type,
769
+ ui_label,
770
+ ui_importance,
771
+ ui_group,
772
+ ui_sortable,
773
+ ui_filterable,
774
+ ui_visible,
775
+ ui_placeholder: field.ui_placeholder,
776
+ ui_help: field.ui_help,
777
+ ui_format: field.ui_format,
778
+ });
779
+ }
780
+
781
+ // Collect enum fields for Drizzle pgEnum generation
782
+ const enumFields = processedFields.filter((f) => f.hasChoices);
783
+
784
+ // Process relationships by type
785
+ const belongsToRelations = Object.entries(relationships)
786
+ .filter(([_, rel]) => rel.type === "belongs_to")
787
+ .map(([relName, rel]) => ({
788
+ name: relName,
789
+ type: "belongs_to",
790
+ target: rel.target,
791
+ targetClass: pascalCase(rel.target),
792
+ targetPlural: pluralize(rel.target),
793
+ targetPluralClass: pascalCase(pluralize(rel.target)),
794
+ foreignKey: rel.foreign_key,
795
+ foreignKeyCamel: camelCase(rel.foreign_key),
796
+ foreignKeyPascal: pascalCase(rel.foreign_key),
797
+ }));
798
+
799
+ const hasManyRelations = Object.entries(relationships)
800
+ .filter(([_, rel]) => rel.type === "has_many")
801
+ .map(([relName, rel]) => ({
802
+ name: relName,
803
+ type: "has_many",
804
+ target: rel.target,
805
+ targetClass: pascalCase(rel.target),
806
+ targetPlural: pluralize(rel.target),
807
+ targetPluralClass: pascalCase(pluralize(rel.target)),
808
+ inverseForeignKey: rel.foreign_key,
809
+ inverseForeignKeyCamel: camelCase(rel.foreign_key),
810
+ }));
811
+
812
+ const hasOneRelations = Object.entries(relationships)
813
+ .filter(([_, rel]) => rel.type === "has_one")
814
+ .map(([relName, rel]) => ({
815
+ name: relName,
816
+ type: "has_one",
817
+ target: rel.target,
818
+ targetClass: pascalCase(rel.target),
819
+ targetPlural: pluralize(rel.target),
820
+ inverseForeignKey: rel.foreign_key,
821
+ inverseForeignKeyCamel: camelCase(rel.foreign_key),
822
+ }));
823
+
824
+ // All relationships combined
825
+ const allRelationships = [
826
+ ...belongsToRelations,
827
+ ...hasManyRelations,
828
+ ...hasOneRelations,
829
+ ];
830
+
831
+ // Check which related entities have generated domain files
832
+ // This allows the repository to skip importing entities that don't exist yet
833
+ const checkEntityExists = (targetName) => {
834
+ const domainBase = `${BASE_PATHS.backendSrc}/domain`;
835
+ const nestedPath = path.resolve(
836
+ process.cwd(),
837
+ `${domainBase}/${targetName}/${targetName}.entity.ts`,
838
+ );
839
+ const flatPath = path.resolve(
840
+ process.cwd(),
841
+ `${domainBase}/${targetName}.entity.ts`,
842
+ );
843
+ return fs.existsSync(nestedPath) || fs.existsSync(flatPath);
844
+ };
845
+
846
+ // Mark each relationship with whether its target entity exists
847
+ for (const rel of allRelationships) {
848
+ rel.targetExists = checkEntityExists(rel.target);
849
+ }
850
+ for (const rel of belongsToRelations) {
851
+ rel.targetExists = checkEntityExists(rel.target);
852
+ }
853
+ for (const rel of hasManyRelations) {
854
+ rel.targetExists = checkEntityExists(rel.target);
855
+ }
856
+ for (const rel of hasOneRelations) {
857
+ rel.targetExists = checkEntityExists(rel.target);
858
+ }
859
+
860
+ // Filter to only relationships with existing targets for repository imports
861
+ const existingRelationships = allRelationships.filter(
862
+ (r) => r.targetExists,
863
+ );
864
+ const existingBelongsTo = belongsToRelations.filter((r) => r.targetExists);
865
+ const existingHasMany = hasManyRelations.filter((r) => r.targetExists);
866
+ const existingHasOne = hasOneRelations.filter((r) => r.targetExists);
867
+
868
+ // Convenience flags
869
+ const hasRelationships = allRelationships.length > 0;
870
+ const hasExistingRelationships = existingRelationships.length > 0;
871
+ const hasBelongsTo = belongsToRelations.length > 0;
872
+ const hasHasMany = hasManyRelations.length > 0;
873
+ const hasHasOne = hasOneRelations.length > 0;
874
+
875
+ // Legacy format for backward compatibility
876
+ const processedRelationships = allRelationships;
877
+
878
+ // Separate required vs optional fields for DTOs
879
+ const requiredFields = processedFields.filter((f) => f.required);
880
+ const optionalFields = processedFields.filter((f) => !f.required);
881
+
882
+ // Compute which Drizzle imports are needed (always need pgTable, uuid for id)
883
+ // Note: timestamp is NOT always needed - only if behaviors include timestamps or soft_delete
884
+ const drizzleImportsNeeded = new Set(["pgTable", "uuid"]);
885
+
886
+ // Add pgEnum if we have any enum fields
887
+ if (enumFields.length > 0) {
888
+ drizzleImportsNeeded.add("pgEnum");
889
+ }
890
+
891
+ // Check if we have entity_ref fields (need to import entity type enum)
892
+ const hasEntityRefFields = entityRefFields.length > 0;
893
+ if (hasEntityRefFields) {
894
+ drizzleImportsNeeded.add("pgEnum");
895
+ }
896
+
897
+ for (const field of processedFields) {
898
+ // Map drizzle type to import name (skip 'enum' as it's handled via pgEnum)
899
+ const importMap = {
900
+ varchar: "varchar",
901
+ integer: "integer",
902
+ decimal: "numeric",
903
+ boolean: "boolean",
904
+ uuid: "uuid",
905
+ date: "date",
906
+ timestamp: "timestamp",
907
+ jsonb: "jsonb",
908
+ text_array: "text",
909
+ };
910
+ const importName = importMap[field.drizzleType];
911
+ if (importName) {
912
+ drizzleImportsNeeded.add(importName);
913
+ }
914
+ }
915
+
916
+ // Add Drizzle imports from behaviors
917
+ for (const imp of resolvedBehaviors.drizzleImports) {
918
+ drizzleImportsNeeded.add(imp);
919
+ }
920
+
921
+ const drizzleImports = Array.from(drizzleImportsNeeded).sort();
922
+
923
+ // Get database dialect from config
924
+ const databaseDialect = getDatabaseDialect();
925
+
926
+ // Derive Electric where clause FK field from entity fields
927
+ // Look for foreign_key to users or tenants
928
+ let electricWhereColumn = 'tenant_id'; // fallback
929
+ let electricWhereValue = 'user.tenantId'; // fallback
930
+
931
+ for (const field of processedFields) {
932
+ if (field.foreignKey) {
933
+ // Check if it references users table
934
+ if (field.foreignKey.startsWith('users.')) {
935
+ electricWhereColumn = field.name;
936
+ electricWhereValue = `user.${field.camelName}`;
937
+ break;
938
+ }
939
+ // Check if it references tenants table
940
+ if (field.foreignKey.startsWith('tenants.')) {
941
+ electricWhereColumn = field.name;
942
+ electricWhereValue = `user.${field.camelName}`;
943
+ // Don't break - users FK takes precedence
944
+ }
945
+ }
946
+ }
947
+
948
+ // ============================================================================
949
+ // Architecture Target + pipeline gates (from generate config)
950
+ //
951
+ // `generate.architecture` is the single source of truth for which backend
952
+ // template set runs. Values:
953
+ // - 'clean' → templates/entity/new/backend/ (Clean Architecture)
954
+ // - 'clean-lite-ps' → templates/entity/new/clean-lite-ps/ (modules/ layout)
955
+ //
956
+ // `generate.frontend` gates the frontend pipeline entirely.
957
+ // ============================================================================
958
+
959
+ const generateConfig = getGenerateConfig();
960
+ const architectureTarget = generateConfig.architecture;
961
+ const isCleanArchitecture = architectureTarget === 'clean';
962
+ const isCleanLitePs = architectureTarget === 'clean-lite-ps';
963
+ const frontendEnabled = generateConfig.frontend === true;
964
+
965
+ // ============================================================================
966
+ // v2: Family
967
+ // ============================================================================
968
+
969
+ const FAMILY_REPOSITORY_MAP = {
970
+ 'synced': 'SyncedEntityRepository',
971
+ 'activity': 'ActivityEntityRepository',
972
+ 'knowledge': 'KnowledgeEntityRepository',
973
+ 'metadata': 'MetadataEntityRepository',
974
+ };
975
+
976
+ const FAMILY_SERVICE_MAP = {
977
+ 'synced': 'SyncedEntityService',
978
+ 'activity': 'ActivityEntityService',
979
+ 'knowledge': 'KnowledgeEntityService',
980
+ 'metadata': 'MetadataEntityService',
981
+ };
982
+
983
+ const family = entity.family ?? null;
984
+ const hasFamily = family != null;
985
+ const familyBaseRepository = family ? (FAMILY_REPOSITORY_MAP[family] ?? null) : null;
986
+ const familyBaseService = family ? (FAMILY_SERVICE_MAP[family] ?? null) : null;
987
+
988
+ // ============================================================================
989
+ // v2: Queries
990
+ // ============================================================================
991
+
992
+ /**
993
+ * Derive a camelCase method name from a query spec.
994
+ *
995
+ * Rules:
996
+ * select present → findXsByY (e.g., select:[email], by:[opportunity_id] → findEmailsByOpportunityId)
997
+ * otherwise → findByX (e.g., by:[user_id] → findByUserId)
998
+ * (e.g., by:[user_id, account_id] → findByUserIdAndAccountId)
999
+ */
1000
+ function deriveQueryMethodName(query) {
1001
+ const byFields = Array.isArray(query.by) ? query.by : [];
1002
+ const selectFields = Array.isArray(query.select) ? query.select : [];
1003
+
1004
+ // Convert snake_case field list to PascalCase joined by "And"
1005
+ const byPart = byFields.map((f) => pascalCase(f)).join('And');
1006
+
1007
+ if (selectFields.length > 0) {
1008
+ // findEmailsByOpportunityId — select fields come first (plural implied)
1009
+ const selectPart = selectFields.map((f) => pascalCase(f)).join('And') + 's';
1010
+ return `find${selectPart}By${byPart}`;
1011
+ }
1012
+
1013
+ return `findBy${byPart}`;
1014
+ }
1015
+
1016
+ const hasQueries = queriesBlock != null && queriesBlock.length > 0;
1017
+
1018
+ // Build a lookup of field name → TS type for query param resolution
1019
+ const fieldTypeMap = {};
1020
+ for (const pf of processedFields) {
1021
+ fieldTypeMap[pf.name] = pf.tsType;
1022
+ fieldTypeMap[pf.camelName] = pf.tsType;
1023
+ }
1024
+
1025
+ const processedQueries = hasQueries
1026
+ ? queriesBlock.map((q) => {
1027
+ const byFields = Array.isArray(q.by) ? q.by : [];
1028
+ const selectFields = Array.isArray(q.select) ? q.select : [];
1029
+ const isUnique = q.unique ?? false;
1030
+ const viaTable = q.via ?? null;
1031
+
1032
+ // Build typed params from by fields
1033
+ const params = byFields.map((f) => ({
1034
+ name: f,
1035
+ camelName: camelCase(f),
1036
+ tsType: fieldTypeMap[f] || fieldTypeMap[camelCase(f)] || 'string',
1037
+ }));
1038
+
1039
+ // Parse order: "created_at desc" → { column, direction }
1040
+ let orderBy = null;
1041
+ let orderDirection = null;
1042
+ if (q.order) {
1043
+ const parts = q.order.trim().split(/\s+/);
1044
+ orderBy = camelCase(parts[0]);
1045
+ orderDirection = parts[1] || 'asc';
1046
+ }
1047
+
1048
+ // Derive method name
1049
+ const methodName = deriveQueryMethodName(q);
1050
+
1051
+ // Derive return type
1052
+ let returnType;
1053
+ if (isUnique) {
1054
+ returnType = `${className} | null`;
1055
+ } else if (selectFields.length > 0) {
1056
+ // Projection — return picked fields
1057
+ const camelFields = selectFields.map((f) => camelCase(f));
1058
+ returnType = selectFields.length === 1
1059
+ ? `${fieldTypeMap[selectFields[0]] || fieldTypeMap[camelFields[0]] || 'string'}[]`
1060
+ : `Pick<${className}, ${camelFields.map((f) => `'${f}'`).join(' | ')}>[]`;
1061
+ } else {
1062
+ returnType = `${className}[]`;
1063
+ }
1064
+
1065
+ // Use case class name
1066
+ const useCaseClassName = pascalCase(methodName) + queryLayerSuffix;
1067
+
1068
+ return {
1069
+ // Raw YAML fields
1070
+ by: byFields,
1071
+ unique: isUnique,
1072
+ select: selectFields,
1073
+ order: q.order ?? null,
1074
+ limit: q.limit ?? null,
1075
+ via: viaTable,
1076
+ // Derived
1077
+ methodName,
1078
+ returnType,
1079
+ params,
1080
+ isUnique,
1081
+ orderBy,
1082
+ orderDirection,
1083
+ viaTable,
1084
+ viaTableCamel: viaTable ? camelCase(viaTable) : null,
1085
+ selectFields: selectFields.map((f) => camelCase(f)),
1086
+ useCaseClassName,
1087
+ // Convenience flags
1088
+ hasVia: viaTable != null,
1089
+ hasSelect: selectFields.length > 0,
1090
+ hasOrder: q.order != null,
1091
+ hasLimit: q.limit != null,
1092
+ hasMultipleParams: params.length > 1,
1093
+ };
1094
+ })
1095
+ : [];
1096
+
1097
+ const hasDeclarativeQueries = processedQueries.length > 0;
1098
+ const declarativeQueryClasses = processedQueries.map((q) => q.useCaseClassName);
1099
+
1100
+ // Check if any query needs 'and' import (multi-field WHERE)
1101
+ const hasMultiFieldQuery = processedQueries.some((q) => q.hasMultipleParams);
1102
+ // Check if any query needs 'desc'/'asc' import (ordered)
1103
+ const hasOrderedQuery = processedQueries.some((q) => q.hasOrder);
1104
+
1105
+ // ============================================================================
1106
+ // v2: Sync
1107
+ // ============================================================================
1108
+
1109
+ const hasSyncBlock = syncBlock != null;
1110
+ const syncElectric = hasSyncBlock ? (syncBlock.electric ?? false) : false;
1111
+ const rawSyncProviders = hasSyncBlock ? (syncBlock.providers ?? {}) : {};
1112
+ const hasSyncProviders = Object.keys(rawSyncProviders).length > 0;
1113
+
1114
+ const syncProviders = hasSyncProviders
1115
+ ? Object.entries(rawSyncProviders).map(([providerName, cfg]) => {
1116
+ // Normalize field_mapping: { local: key, remote: value }[]
1117
+ const rawMapping = cfg.field_mapping ?? {};
1118
+ const fieldMapping = Object.entries(rawMapping).map(([local, remote]) => ({
1119
+ local,
1120
+ remote,
1121
+ }));
1122
+
1123
+ return {
1124
+ name: providerName,
1125
+ remoteEntity: cfg.remote_entity ?? null,
1126
+ direction: cfg.direction ?? 'bidirectional',
1127
+ cdc: cfg.cdc ?? false,
1128
+ fieldMapping,
1129
+ readOnlyFields: cfg.read_only_fields ?? [],
1130
+ };
1131
+ })
1132
+ : [];
1133
+
1134
+ // ============================================================================
1135
+ // v2: Events
1136
+ // ============================================================================
1137
+
1138
+ const hasEvents = eventsBlock != null && eventsBlock.length > 0;
1139
+ const processedEvents = hasEvents
1140
+ ? eventsBlock.map((ev) => {
1141
+ // Convert body: { field: type } to array of { field, type }
1142
+ const rawBody = ev.body ?? {};
1143
+ const body = Object.entries(rawBody).map(([field, type]) => ({ field, type }));
1144
+
1145
+ // Derive class names from event name (snake_case → PascalCase + Event)
1146
+ const className = pascalCase(ev.name) + 'Event';
1147
+ const handlerClassName = pascalCase(ev.name) + 'Handler';
1148
+
1149
+ return {
1150
+ name: ev.name,
1151
+ queue: ev.queue ?? null,
1152
+ body,
1153
+ generateHandler: ev.generate_handler ?? false,
1154
+ className,
1155
+ handlerClassName,
1156
+ };
1157
+ })
1158
+ : [];
1159
+
1160
+ const locals = {
1161
+ // Database configuration
1162
+ databaseDialect,
1163
+ schemaDir: BASE_PATHS.schemaDir,
1164
+
1165
+ // Entity names
1166
+ name,
1167
+ plural,
1168
+ table,
1169
+ className,
1170
+ classNamePlural,
1171
+ camelName,
1172
+ repositoryToken,
1173
+
1174
+ // Frontend store naming
1175
+ singularCamelName,
1176
+ pluralCamelName,
1177
+ collectionVarName,
1178
+ collectionVarNamePlural,
1179
+
1180
+ // Fields
1181
+ fields: processedFields,
1182
+ requiredFields,
1183
+ optionalFields,
1184
+ enumFields,
1185
+
1186
+ // Entity reference fields (polymorphic refs)
1187
+ entityRefFields,
1188
+ hasEntityRefFields,
1189
+
1190
+ // Relationships - separated by type
1191
+ relationships: allRelationships,
1192
+ belongsToRelations,
1193
+ hasManyRelations,
1194
+ hasOneRelations,
1195
+
1196
+ // Relationship flags
1197
+ hasRelationships,
1198
+ hasExistingRelationships,
1199
+ hasBelongsTo,
1200
+ hasHasMany,
1201
+ hasHasOne,
1202
+
1203
+ // Filtered relationships (only those with existing target entities)
1204
+ existingRelationships,
1205
+ existingBelongsTo,
1206
+ existingHasMany,
1207
+ existingHasOne,
1208
+
1209
+ // Drizzle imports (only what's needed)
1210
+ drizzleImports,
1211
+
1212
+ // Layout configuration
1213
+ // folder_structure: "nested" | "flat" - controls directory nesting
1214
+ // file_grouping: "separate" | "grouped" - controls file organization
1215
+ layout,
1216
+ folderStructure,
1217
+ fileGrouping,
1218
+ isNested,
1219
+ isGrouped,
1220
+ paths,
1221
+ fileNames,
1222
+ imports,
1223
+
1224
+ // Base paths for templates (from centralized config)
1225
+ basePaths: BASE_PATHS,
1226
+ backendLayers: BACKEND_LAYERS,
1227
+
1228
+ // Unified locations (path + import alias)
1229
+ // Usage: locations.dbEntities.path, locations.dbEntities.import
1230
+ locations: LOCATIONS,
1231
+
1232
+ // Frontend configuration
1233
+ // Note: Use hasOwnProperty checks for values where null is meaningful (disables the feature)
1234
+ frontend: {
1235
+ auth: {
1236
+ // null means "no auth function" - don't fall back to default
1237
+ function: frontendConfig.auth?.hasOwnProperty?.('function')
1238
+ ? frontendConfig.auth.function
1239
+ : 'getAuthorizationHeader',
1240
+ },
1241
+ sync: {
1242
+ shapeUrl: frontendSync.shapeUrl ?? '/v1/shape',
1243
+ useTableParam: frontendSync.useTableParam ?? true,
1244
+ // Column mapper for snake_case to camelCase conversion (e.g., 'snakeCamelMapper')
1245
+ // Set to null/undefined if DB columns already match JS property names
1246
+ columnMapper: frontendSync.hasOwnProperty?.('columnMapper')
1247
+ ? frontendSync.columnMapper
1248
+ : 'snakeCamelMapper',
1249
+ // Whether to wrap shapeUrl in new URL() constructor
1250
+ wrapInUrlConstructor: frontendSync.wrapInUrlConstructor ?? true,
1251
+ // Whether columnMapper needs () to call (true for functions, false for objects)
1252
+ columnMapperNeedsCall: frontendSync.columnMapperNeedsCall ?? true,
1253
+ // Import path for API_BASE_URL (if needed)
1254
+ apiBaseUrlImport: frontendSync.apiBaseUrlImport ?? null,
1255
+ },
1256
+ parsers: frontendConfig.parsers ?? {
1257
+ timestamptz: '(date: string) => new Date(date)',
1258
+ },
1259
+ collections: {
1260
+ // Schema prefix: 'schema.' for namespace import, '' for direct import
1261
+ schemaPrefix: frontendConfig.collections?.schemaPrefix ?? 'schema.',
1262
+ },
1263
+ },
1264
+
1265
+ // Naming configuration (for templates that need it)
1266
+ namingConfig,
1267
+ applicationLayerSuffix,
1268
+ queryLayerSuffix,
1269
+
1270
+ // Pre-computed class names with configured terminology
1271
+ createCommandClass,
1272
+ updateCommandClass,
1273
+ deleteCommandClass,
1274
+ getByIdQueryClass,
1275
+ listQueryClass,
1276
+
1277
+ // Generation toggles (what to generate)
1278
+ generate: {
1279
+ fieldMetadata: getProjectConfig()?.generate?.fieldMetadata ?? true,
1280
+ collections: getProjectConfig()?.generate?.collections ?? true,
1281
+ // Whether to generate index.ts in collections folder (for multi-file collection structure)
1282
+ collectionsIndex: getProjectConfig()?.generate?.collectionsIndex ?? false,
1283
+ hooks: getProjectConfig()?.generate?.hooks ?? true,
1284
+ mutations: getProjectConfig()?.generate?.mutations ?? true,
1285
+ // Backend toggles
1286
+ drizzleSchema: getProjectConfig()?.generate?.drizzleSchema ?? true,
1287
+ commands: getProjectConfig()?.generate?.commands ?? true,
1288
+ queries: getProjectConfig()?.generate?.queries ?? true,
1289
+ dtos: getProjectConfig()?.generate?.dtos ?? true,
1290
+ schemaServer: getProjectConfig()?.generate?.schemaServer ?? false,
1291
+ schemaClient: getProjectConfig()?.generate?.schemaClient ?? false,
1292
+ electricMigrations: getProjectConfig()?.generate?.electricMigrations ?? false,
1293
+ // Hook style: 'collection' uses collection.useMany(), 'useLiveQuery' uses TanStack DB pattern
1294
+ hookStyle: getProjectConfig()?.generate?.hookStyle ?? 'collection',
1295
+ // Output structure mode: 'entity-first' | 'concern-first' | 'monolithic'
1296
+ // entity-first: generated/{entity}/types.ts, collection.ts, hooks.ts...
1297
+ // concern-first: generated/types/{entity}.ts, collections/{entity}.ts...
1298
+ // monolithic: generated/{entity}.ts (single file per entity)
1299
+ structure: getProjectConfig()?.generate?.structure ?? 'monolithic',
1300
+ // Type naming: 'plain' = Opportunity, 'entity' = OpportunityEntity
1301
+ typeNaming: getProjectConfig()?.generate?.typeNaming ?? 'plain',
1302
+ // FK resolution: true = import related collections, false = skip (useful when collections don't exist)
1303
+ fkResolution: getProjectConfig()?.generate?.fkResolution ?? true,
1304
+ // Collection variable naming: 'singular' = opportunityCollection, 'plural' = opportunitiesCollection
1305
+ collectionNaming: getProjectConfig()?.generate?.collectionNaming ?? 'singular',
1306
+ // File naming: 'singular' = opportunity.ts, 'plural' = opportunities.ts
1307
+ fileNaming: getProjectConfig()?.generate?.fileNaming ?? 'singular',
1308
+ // Hook return style: 'generic' = { data }, 'named' = { opportunities }
1309
+ hookReturnStyle: getProjectConfig()?.generate?.hookReturnStyle ?? 'generic',
1310
+ },
1311
+
1312
+ // Pre-computed output paths for templates (avoids ternary in YAML frontmatter)
1313
+ outputPaths,
1314
+
1315
+ // Behavior strategy and resolved behaviors
1316
+ behaviorStrategy,
1317
+ behaviors: resolvedBehaviors,
1318
+ behaviorFields: resolvedBehaviors.fields,
1319
+ hasBehaviors: resolvedBehaviors.hasBehaviors,
1320
+ hasTimestamps: resolvedBehaviors.hasTimestamps,
1321
+ hasSoftDelete: resolvedBehaviors.hasSoftDelete,
1322
+ hasUserTracking: resolvedBehaviors.hasUserTracking,
1323
+ hasTemporalValidity: resolvedBehaviors.hasTemporalValidity,
1324
+ repositoryBehaviorConfig: resolvedBehaviors.repositoryConfig,
1325
+
1326
+ // Expose configuration (which layers to generate)
1327
+ expose: entity.expose || ["repository", "rest", "trpc"],
1328
+ exposeRepository: (
1329
+ entity.expose || ["repository", "rest", "trpc"]
1330
+ ).includes("repository"),
1331
+ exposeRest: (entity.expose || ["repository", "rest", "trpc"]).includes(
1332
+ "rest",
1333
+ ),
1334
+ exposeTrpc: (entity.expose || ["repository", "rest", "trpc"]).includes(
1335
+ "trpc",
1336
+ ),
1337
+ exposeElectric: (
1338
+ entity.expose || ["repository", "rest", "trpc"]
1339
+ ).includes("electric"),
1340
+
1341
+ // Electric SQL where clause (derived from entity FK fields)
1342
+ electricWhereColumn,
1343
+ electricWhereValue,
1344
+
1345
+ // ======================================================================
1346
+ // v2 variables
1347
+ // ======================================================================
1348
+
1349
+ // Architecture target (from generate.architecture config)
1350
+ architectureTarget,
1351
+ isCleanArchitecture,
1352
+ isCleanLitePs,
1353
+ frontendEnabled,
1354
+
1355
+ // Family
1356
+ family,
1357
+ hasFamily,
1358
+ familyBaseRepository,
1359
+ familyBaseService,
1360
+
1361
+ // Queries
1362
+ hasQueries,
1363
+ processedQueries,
1364
+ hasDeclarativeQueries,
1365
+ declarativeQueryClasses,
1366
+ hasMultiFieldQuery,
1367
+ hasOrderedQuery,
1368
+
1369
+ // Sync
1370
+ hasSyncBlock,
1371
+ syncElectric,
1372
+ hasSyncProviders,
1373
+ syncProviders,
1374
+
1375
+ // Events
1376
+ hasEvents,
1377
+ processedEvents,
1378
+ };
1379
+
1380
+ // ========================================================================
1381
+ // Clean-Lite-PS template locals
1382
+ //
1383
+ // Populated only when `generate.architecture === 'clean-lite-ps'`.
1384
+ // When the architecture is 'clean', stub locals are injected so CLP
1385
+ // template bodies can render without crashing; their `to:` guards resolve
1386
+ // to null which causes Hygen to skip file writing.
1387
+ // ========================================================================
1388
+ if (isCleanLitePs) {
1389
+ const { buildCleanLitePsLocals } = await import('./clean-lite-ps/prompt-extension.js');
1390
+ Object.assign(locals, buildCleanLitePsLocals(definition, locals));
1391
+ } else {
1392
+ // Inject safe stub locals so CLP template bodies can render without crashing.
1393
+ // The to: guard resolves to "null" which causes Hygen to skip file writing.
1394
+ const _n = definition.entity?.name || '';
1395
+ const _p = definition.entity?.plural || _n + 's';
1396
+ Object.assign(locals, {
1397
+ clpOutputPaths: undefined,
1398
+ entityName: _n,
1399
+ entityNamePlural: _p,
1400
+ entityNamePascal: _n,
1401
+ entityNamePluralPascal: _p,
1402
+ classNames: {},
1403
+ clpDrizzleImports: [],
1404
+ clpProcessedFields: [],
1405
+ clpCreateDtoFields: [],
1406
+ clpOutputDtoFields: [],
1407
+ clpBelongsTo: [],
1408
+ clpBelongsToFkFields: [],
1409
+ clpHasRelationsBlock: false,
1410
+ repositoryBaseClass: '',
1411
+ serviceBaseClass: '',
1412
+ repositoryBaseImport: '',
1413
+ serviceBaseImport: '',
1414
+ repositoryInheritedMethods: [],
1415
+ serviceInheritedMethods: [],
1416
+ });
1417
+ }
1418
+
1419
+ return locals;
1420
+ },
1421
+ };