irgen 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 (244) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +312 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/dsl/aggregator.d.ts +8 -0
  9. package/dist/dsl/aggregator.d.ts.map +1 -0
  10. package/dist/dsl/aggregator.js +64 -0
  11. package/dist/dsl/aggregator.js.map +1 -0
  12. package/dist/dsl/frontend-runtime.d.ts +486 -0
  13. package/dist/dsl/frontend-runtime.d.ts.map +1 -0
  14. package/dist/dsl/frontend-runtime.js +232 -0
  15. package/dist/dsl/frontend-runtime.js.map +1 -0
  16. package/dist/dsl/runtime.d.ts +33 -0
  17. package/dist/dsl/runtime.d.ts.map +1 -0
  18. package/dist/dsl/runtime.js +120 -0
  19. package/dist/dsl/runtime.js.map +1 -0
  20. package/dist/emit/backend/adapters.d.ts +11 -0
  21. package/dist/emit/backend/adapters.d.ts.map +1 -0
  22. package/dist/emit/backend/adapters.js +374 -0
  23. package/dist/emit/backend/adapters.js.map +1 -0
  24. package/dist/emit/backend/backend-tsmorph.d.ts +5 -0
  25. package/dist/emit/backend/backend-tsmorph.d.ts.map +1 -0
  26. package/dist/emit/backend/backend-tsmorph.js +858 -0
  27. package/dist/emit/backend/backend-tsmorph.js.map +1 -0
  28. package/dist/emit/backend/fake-backend.d.ts +2 -0
  29. package/dist/emit/backend/fake-backend.d.ts.map +1 -0
  30. package/dist/emit/backend/fake-backend.js +19 -0
  31. package/dist/emit/backend/fake-backend.js.map +1 -0
  32. package/dist/emit/backend/packaging.d.ts +3 -0
  33. package/dist/emit/backend/packaging.d.ts.map +1 -0
  34. package/dist/emit/backend/packaging.js +71 -0
  35. package/dist/emit/backend/packaging.js.map +1 -0
  36. package/dist/emit/backend/server.d.ts +4 -0
  37. package/dist/emit/backend/server.d.ts.map +1 -0
  38. package/dist/emit/backend/server.js +169 -0
  39. package/dist/emit/backend/server.js.map +1 -0
  40. package/dist/emit/cli/cli-fake.d.ts +2 -0
  41. package/dist/emit/cli/cli-fake.d.ts.map +1 -0
  42. package/dist/emit/cli/cli-fake.js +33 -0
  43. package/dist/emit/cli/cli-fake.js.map +1 -0
  44. package/dist/emit/electron/electron-shell.d.ts +3 -0
  45. package/dist/emit/electron/electron-shell.d.ts.map +1 -0
  46. package/dist/emit/electron/electron-shell.js +454 -0
  47. package/dist/emit/electron/electron-shell.js.map +1 -0
  48. package/dist/emit/engine.d.ts +14 -0
  49. package/dist/emit/engine.d.ts.map +1 -0
  50. package/dist/emit/engine.js +25 -0
  51. package/dist/emit/engine.js.map +1 -0
  52. package/dist/emit/format.d.ts +2 -0
  53. package/dist/emit/format.d.ts.map +1 -0
  54. package/dist/emit/format.js +23 -0
  55. package/dist/emit/format.js.map +1 -0
  56. package/dist/emit/frontend/frontend-react.d.ts +4 -0
  57. package/dist/emit/frontend/frontend-react.d.ts.map +1 -0
  58. package/dist/emit/frontend/frontend-react.js +2021 -0
  59. package/dist/emit/frontend/frontend-react.js.map +1 -0
  60. package/dist/emit/frontend/registry.d.ts +20 -0
  61. package/dist/emit/frontend/registry.d.ts.map +1 -0
  62. package/dist/emit/frontend/registry.js +46 -0
  63. package/dist/emit/frontend/registry.js.map +1 -0
  64. package/dist/emit/frontend/runtime-emitter.d.ts +4 -0
  65. package/dist/emit/frontend/runtime-emitter.d.ts.map +1 -0
  66. package/dist/emit/frontend/runtime-emitter.js +435 -0
  67. package/dist/emit/frontend/runtime-emitter.js.map +1 -0
  68. package/dist/emit/frontend/runtime-template.d.ts +28 -0
  69. package/dist/emit/frontend/runtime-template.d.ts.map +1 -0
  70. package/dist/emit/frontend/runtime-template.js +218 -0
  71. package/dist/emit/frontend/runtime-template.js.map +1 -0
  72. package/dist/emit/frontend/ssg.d.ts +8 -0
  73. package/dist/emit/frontend/ssg.d.ts.map +1 -0
  74. package/dist/emit/frontend/ssg.js +219 -0
  75. package/dist/emit/frontend/ssg.js.map +1 -0
  76. package/dist/emit/registry.d.ts +17 -0
  77. package/dist/emit/registry.d.ts.map +1 -0
  78. package/dist/emit/registry.js +38 -0
  79. package/dist/emit/registry.js.map +1 -0
  80. package/dist/emit/static-site/css.d.ts +5 -0
  81. package/dist/emit/static-site/css.d.ts.map +1 -0
  82. package/dist/emit/static-site/css.js +872 -0
  83. package/dist/emit/static-site/css.js.map +1 -0
  84. package/dist/emit/static-site/enhancements.d.ts +11 -0
  85. package/dist/emit/static-site/enhancements.d.ts.map +1 -0
  86. package/dist/emit/static-site/enhancements.js +266 -0
  87. package/dist/emit/static-site/enhancements.js.map +1 -0
  88. package/dist/emit/static-site/static-site-html.d.ts +3 -0
  89. package/dist/emit/static-site/static-site-html.d.ts.map +1 -0
  90. package/dist/emit/static-site/static-site-html.js +1172 -0
  91. package/dist/emit/static-site/static-site-html.js.map +1 -0
  92. package/dist/emit/utils/sdk.d.ts +15 -0
  93. package/dist/emit/utils/sdk.d.ts.map +1 -0
  94. package/dist/emit/utils/sdk.js +34 -0
  95. package/dist/emit/utils/sdk.js.map +1 -0
  96. package/dist/extensions/context.d.ts +23 -0
  97. package/dist/extensions/context.d.ts.map +1 -0
  98. package/dist/extensions/context.js +43 -0
  99. package/dist/extensions/context.js.map +1 -0
  100. package/dist/index.d.ts +32 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +135 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/ir/decl/backend.raw.schema.d.ts +128 -0
  105. package/dist/ir/decl/backend.raw.schema.d.ts.map +1 -0
  106. package/dist/ir/decl/backend.raw.schema.js +24 -0
  107. package/dist/ir/decl/backend.raw.schema.js.map +1 -0
  108. package/dist/ir/decl/bundle.d.ts +15 -0
  109. package/dist/ir/decl/bundle.d.ts.map +1 -0
  110. package/dist/ir/decl/bundle.js +8 -0
  111. package/dist/ir/decl/bundle.js.map +1 -0
  112. package/dist/ir/decl/cli.raw.schema.d.ts +133 -0
  113. package/dist/ir/decl/cli.raw.schema.d.ts.map +1 -0
  114. package/dist/ir/decl/cli.raw.schema.js +20 -0
  115. package/dist/ir/decl/cli.raw.schema.js.map +1 -0
  116. package/dist/ir/decl/frontend.raw.schema.d.ts +6631 -0
  117. package/dist/ir/decl/frontend.raw.schema.d.ts.map +1 -0
  118. package/dist/ir/decl/frontend.raw.schema.js +272 -0
  119. package/dist/ir/decl/frontend.raw.schema.js.map +1 -0
  120. package/dist/ir/decl/index.d.ts +6 -0
  121. package/dist/ir/decl/index.d.ts.map +1 -0
  122. package/dist/ir/decl/index.js +6 -0
  123. package/dist/ir/decl/index.js.map +1 -0
  124. package/dist/ir/decl/normalize.schema.d.ts +9154 -0
  125. package/dist/ir/decl/normalize.schema.d.ts.map +1 -0
  126. package/dist/ir/decl/normalize.schema.js +71 -0
  127. package/dist/ir/decl/normalize.schema.js.map +1 -0
  128. package/dist/ir/domain/backend.d.ts +19 -0
  129. package/dist/ir/domain/backend.d.ts.map +1 -0
  130. package/dist/ir/domain/backend.js +3 -0
  131. package/dist/ir/domain/backend.js.map +1 -0
  132. package/dist/ir/domain/cli.d.ts +18 -0
  133. package/dist/ir/domain/cli.d.ts.map +1 -0
  134. package/dist/ir/domain/cli.js +2 -0
  135. package/dist/ir/domain/cli.js.map +1 -0
  136. package/dist/ir/domain/frontend/index.d.ts +190 -0
  137. package/dist/ir/domain/frontend/index.d.ts.map +1 -0
  138. package/dist/ir/domain/frontend/index.js +2 -0
  139. package/dist/ir/domain/frontend/index.js.map +1 -0
  140. package/dist/ir/domain/frontend.d.ts +2 -0
  141. package/dist/ir/domain/frontend.d.ts.map +1 -0
  142. package/dist/ir/domain/frontend.js +3 -0
  143. package/dist/ir/domain/frontend.js.map +1 -0
  144. package/dist/ir/frontend-contract.d.ts +187 -0
  145. package/dist/ir/frontend-contract.d.ts.map +1 -0
  146. package/dist/ir/frontend-contract.js +6 -0
  147. package/dist/ir/frontend-contract.js.map +1 -0
  148. package/dist/ir/target/backend.d.ts +11 -0
  149. package/dist/ir/target/backend.d.ts.map +1 -0
  150. package/dist/ir/target/backend.js +2 -0
  151. package/dist/ir/target/backend.js.map +1 -0
  152. package/dist/ir/target/backend.policy.d.ts +896 -0
  153. package/dist/ir/target/backend.policy.d.ts.map +1 -0
  154. package/dist/ir/target/backend.policy.js +106 -0
  155. package/dist/ir/target/backend.policy.js.map +1 -0
  156. package/dist/ir/target/cli.d.ts +3 -0
  157. package/dist/ir/target/cli.d.ts.map +1 -0
  158. package/dist/ir/target/cli.js +2 -0
  159. package/dist/ir/target/cli.js.map +1 -0
  160. package/dist/ir/target/electron.d.ts +99 -0
  161. package/dist/ir/target/electron.d.ts.map +1 -0
  162. package/dist/ir/target/electron.js +2 -0
  163. package/dist/ir/target/electron.js.map +1 -0
  164. package/dist/ir/target/electron.policy.d.ts +7015 -0
  165. package/dist/ir/target/electron.policy.d.ts.map +1 -0
  166. package/dist/ir/target/electron.policy.js +119 -0
  167. package/dist/ir/target/electron.policy.js.map +1 -0
  168. package/dist/ir/target/frontend.d.ts +12 -0
  169. package/dist/ir/target/frontend.d.ts.map +1 -0
  170. package/dist/ir/target/frontend.js +2 -0
  171. package/dist/ir/target/frontend.js.map +1 -0
  172. package/dist/ir/target/frontend.policy.d.ts +268 -0
  173. package/dist/ir/target/frontend.policy.d.ts.map +1 -0
  174. package/dist/ir/target/frontend.policy.js +33 -0
  175. package/dist/ir/target/frontend.policy.js.map +1 -0
  176. package/dist/ir/target/index.d.ts +6 -0
  177. package/dist/ir/target/index.d.ts.map +1 -0
  178. package/dist/ir/target/index.js +6 -0
  179. package/dist/ir/target/index.js.map +1 -0
  180. package/dist/ir/target/static-site.d.ts +18 -0
  181. package/dist/ir/target/static-site.d.ts.map +1 -0
  182. package/dist/ir/target/static-site.js +2 -0
  183. package/dist/ir/target/static-site.js.map +1 -0
  184. package/dist/ir/target/static-site.policy.d.ts +2911 -0
  185. package/dist/ir/target/static-site.policy.d.ts.map +1 -0
  186. package/dist/ir/target/static-site.policy.js +127 -0
  187. package/dist/ir/target/static-site.policy.js.map +1 -0
  188. package/dist/lowering/backend.d.ts +4 -0
  189. package/dist/lowering/backend.d.ts.map +1 -0
  190. package/dist/lowering/backend.js +57 -0
  191. package/dist/lowering/backend.js.map +1 -0
  192. package/dist/lowering/cli.d.ts +4 -0
  193. package/dist/lowering/cli.d.ts.map +1 -0
  194. package/dist/lowering/cli.js +22 -0
  195. package/dist/lowering/cli.js.map +1 -0
  196. package/dist/lowering/engine.d.ts +18 -0
  197. package/dist/lowering/engine.d.ts.map +1 -0
  198. package/dist/lowering/engine.js +47 -0
  199. package/dist/lowering/engine.js.map +1 -0
  200. package/dist/lowering/frontend.d.ts +9 -0
  201. package/dist/lowering/frontend.d.ts.map +1 -0
  202. package/dist/lowering/frontend.js +246 -0
  203. package/dist/lowering/frontend.js.map +1 -0
  204. package/dist/lowering/targets/to-backend.d.ts +9 -0
  205. package/dist/lowering/targets/to-backend.d.ts.map +1 -0
  206. package/dist/lowering/targets/to-backend.js +55 -0
  207. package/dist/lowering/targets/to-backend.js.map +1 -0
  208. package/dist/lowering/targets/to-cli.d.ts +4 -0
  209. package/dist/lowering/targets/to-cli.d.ts.map +1 -0
  210. package/dist/lowering/targets/to-cli.js +11 -0
  211. package/dist/lowering/targets/to-cli.js.map +1 -0
  212. package/dist/lowering/targets/to-electron.d.ts +30 -0
  213. package/dist/lowering/targets/to-electron.d.ts.map +1 -0
  214. package/dist/lowering/targets/to-electron.js +87 -0
  215. package/dist/lowering/targets/to-electron.js.map +1 -0
  216. package/dist/lowering/targets/to-frontend.d.ts +4 -0
  217. package/dist/lowering/targets/to-frontend.d.ts.map +1 -0
  218. package/dist/lowering/targets/to-frontend.js +30 -0
  219. package/dist/lowering/targets/to-frontend.js.map +1 -0
  220. package/dist/lowering/targets/to-static-site.d.ts +16 -0
  221. package/dist/lowering/targets/to-static-site.d.ts.map +1 -0
  222. package/dist/lowering/targets/to-static-site.js +30 -0
  223. package/dist/lowering/targets/to-static-site.js.map +1 -0
  224. package/dist/mappers/index.d.ts +12 -0
  225. package/dist/mappers/index.d.ts.map +1 -0
  226. package/dist/mappers/index.js +60 -0
  227. package/dist/mappers/index.js.map +1 -0
  228. package/dist/types/extension.d.ts +3 -0
  229. package/dist/types/extension.d.ts.map +1 -0
  230. package/dist/types/extension.js +2 -0
  231. package/dist/types/extension.js.map +1 -0
  232. package/dist/utils/array.d.ts +2 -0
  233. package/dist/utils/array.d.ts.map +1 -0
  234. package/dist/utils/array.js +4 -0
  235. package/dist/utils/array.js.map +1 -0
  236. package/dist/utils/index.d.ts +3 -0
  237. package/dist/utils/index.d.ts.map +1 -0
  238. package/dist/utils/index.js +3 -0
  239. package/dist/utils/index.js.map +1 -0
  240. package/dist/utils/string.d.ts +13 -0
  241. package/dist/utils/string.d.ts.map +1 -0
  242. package/dist/utils/string.js +56 -0
  243. package/dist/utils/string.js.map +1 -0
  244. package/package.json +112 -0
@@ -0,0 +1,858 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { Project, QuoteKind, IndentationText, ScriptTarget, Scope } from "ts-morph";
4
+ import { BackendPolicySchema } from "../../ir/target/backend.policy.js";
5
+ import { emitterEngine } from "../engine.js";
6
+ import { formatDirectory } from "../format.js";
7
+ import { emitIdAdapter, emitLoggerAdapter, emitHttpAdapter, emitResponseAdapter, emitAuthAdapter, emitContextAdapter, emitErrorAdapter, emitValidationAdapter, emitPaginationAdapter } from "./adapters.js";
8
+ import { emitHttpServer } from "./server.js";
9
+ import { emitPackageJson, emitTsConfig } from "./packaging.js";
10
+ function ensureDir(p) {
11
+ fs.mkdirSync(p, { recursive: true });
12
+ }
13
+ function cleanDir(p) {
14
+ if (!fs.existsSync(p))
15
+ return;
16
+ fs.rmSync(p, { recursive: true, force: true });
17
+ }
18
+ function getBackendPolicies(ir) {
19
+ const p = ir?.policies ?? {};
20
+ const rawBackend = p.backend ?? p ?? {};
21
+ const backend = BackendPolicySchema.parse(rawBackend);
22
+ const legacy = rawBackend;
23
+ const core = backend.core ?? {};
24
+ return {
25
+ ...backend,
26
+ core,
27
+ // legacy fields fallback for emitters still reading old keys
28
+ generateId: legacy.generateId ?? core.generateId,
29
+ loggerImpl: legacy.loggerImpl ?? core.loggerImpl,
30
+ httpClient: legacy.httpClient ?? core.httpClient,
31
+ formatter: legacy.formatter ?? core.formatter,
32
+ db: legacy.db ?? core.db,
33
+ idProvider: legacy.idProvider ?? (core.generateId === "shortid" ? "shortId" : "newId"),
34
+ };
35
+ }
36
+ export function emitBackendToProject(project, outDir, ir) {
37
+ const policies = getBackendPolicies(ir);
38
+ ensureDir(outDir);
39
+ ensureDir(path.join(outDir, "lib"));
40
+ ensureDir(path.join(outDir, "services"));
41
+ ensureDir(path.join(outDir, "controllers"));
42
+ // adapters
43
+ emitIdAdapter(project, outDir, policies);
44
+ emitLoggerAdapter(project, outDir, policies);
45
+ emitContextAdapter(project, outDir, policies);
46
+ emitHttpAdapter(project, outDir, policies);
47
+ emitResponseAdapter(project, outDir, policies);
48
+ emitAuthAdapter(project, outDir, policies);
49
+ emitErrorAdapter(project, outDir, policies);
50
+ emitValidationAdapter(project, outDir, policies);
51
+ emitPaginationAdapter(project, outDir, policies);
52
+ // Prisma support
53
+ const dbProvider = policies?.db?.provider;
54
+ if (dbProvider === "prisma") {
55
+ emitPrismaSchema(outDir, policies, ir.entities);
56
+ }
57
+ emitModels(project, outDir, ir.entities);
58
+ for (const entity of ir.entities) {
59
+ emitRepositoryInterface(project, outDir, entity);
60
+ emitInMemoryRepository(project, outDir, entity);
61
+ if (dbProvider === "prisma") {
62
+ emitPrismaRepository(project, outDir, entity);
63
+ }
64
+ emitBaseService(project, outDir, entity);
65
+ emitUserService(project, outDir, entity);
66
+ emitServiceTest(project, outDir, entity);
67
+ emitController(project, outDir, entity, policies);
68
+ }
69
+ // package.json injection based on policies
70
+ emitPackageJson(outDir, ir.appName, policies);
71
+ emitTsConfig(outDir);
72
+ emitHttpServer(project, outDir, ir, policies);
73
+ emitOpenAPI(project, outDir, ir, policies);
74
+ }
75
+ export function emitBackend(ir, outDir) {
76
+ // We DO NOT clean the entire directory anymore because we want to preserve user files
77
+ ensureDir(outDir);
78
+ const policies = getBackendPolicies(ir);
79
+ const project = new Project({
80
+ useInMemoryFileSystem: false,
81
+ manipulationSettings: {
82
+ quoteKind: QuoteKind.Double,
83
+ indentationText: IndentationText.TwoSpaces,
84
+ },
85
+ compilerOptions: { target: ScriptTarget.ES2022 },
86
+ });
87
+ emitBackendToProject(project, outDir, ir);
88
+ project.saveSync();
89
+ // optional project-level formatting based on policy (default: prettier)
90
+ try {
91
+ formatDirectory(outDir, policies.formatter);
92
+ }
93
+ catch (e) {
94
+ // ignore format failures
95
+ }
96
+ }
97
+ // Register the backend emitter with the emitter engine
98
+ try {
99
+ emitterEngine.registerEmitter("backend-tsmorph", async (ir, outDir) => {
100
+ const project = new Project({
101
+ useInMemoryFileSystem: false,
102
+ manipulationSettings: {
103
+ quoteKind: QuoteKind.Double,
104
+ indentationText: IndentationText.TwoSpaces,
105
+ },
106
+ compilerOptions: { target: ScriptTarget.ES2022 },
107
+ });
108
+ // Strategy: We rely on overwrite for base artifacts.
109
+ // cleanDir(outDir); // DISABLE FULL CLEAN
110
+ emitBackendToProject(project, outDir, ir);
111
+ project.saveSync();
112
+ // apply the chosen formatter to the output directory
113
+ try {
114
+ formatDirectory(outDir, getBackendPolicies(ir)?.formatter);
115
+ }
116
+ catch (e) { /* ignore */ }
117
+ }, { force: true });
118
+ // register default target -> emitter mapping
119
+ try {
120
+ const { registerTargetEmitter } = await import("../registry.js");
121
+ registerTargetEmitter("backend", "backend-tsmorph", { force: true });
122
+ }
123
+ catch (e) {
124
+ // ignore
125
+ }
126
+ }
127
+ catch (e) {
128
+ // ignore double-registration in test runs
129
+ }
130
+ function emitOpenAPI(project, outDir, ir, policies) {
131
+ const rest = policies?.interfaces?.rest ?? { enabled: true, basePath: "/api", openapi: { enabled: true, title: "API", version: "1.0.0" } };
132
+ if (!rest.enabled || rest.openapi?.enabled === false)
133
+ return;
134
+ const basePath = rest.basePath ?? "/api";
135
+ const envelopeKeys = policies?.envelope?.keys ?? { data: "data", meta: "meta", error: "error" };
136
+ const metaKeys = policies?.envelope?.meta ?? { requestIdKey: "requestId" };
137
+ const componentsSchemas = {};
138
+ function mapType(t) {
139
+ if (!t)
140
+ return { type: "string" };
141
+ const lower = t.toLowerCase();
142
+ if (lower.endsWith("[]"))
143
+ return { type: "array", items: mapType(t.slice(0, -2)) };
144
+ if (["string", "uuid"].includes(lower))
145
+ return { type: "string" };
146
+ if (["number", "int", "integer", "float", "double"].includes(lower))
147
+ return { type: "number" };
148
+ if (["boolean", "bool"].includes(lower))
149
+ return { type: "boolean" };
150
+ return { type: "string" };
151
+ }
152
+ ir.entities.forEach((entity) => {
153
+ const props = {};
154
+ if (entity.model) {
155
+ Object.entries(entity.model).forEach(([k, v]) => {
156
+ props[k] = mapType(v);
157
+ });
158
+ }
159
+ componentsSchemas[entity.name] = {
160
+ type: "object",
161
+ properties: props,
162
+ };
163
+ });
164
+ const paths = {};
165
+ ir.entities.forEach((entity) => {
166
+ const resource = entity.name.toLowerCase();
167
+ const entityRef = { $ref: `#/components/schemas/${entity.name}` };
168
+ const listOp = entity.operations.find((o) => o.kind === "LIST");
169
+ if (listOp) {
170
+ paths[`${basePath}/${resource}`] = {
171
+ get: {
172
+ summary: `List ${entity.name}`,
173
+ parameters: [
174
+ { name: "page", in: "query", schema: { type: "integer", minimum: 1 } },
175
+ { name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
176
+ ],
177
+ responses: {
178
+ 200: {
179
+ description: "OK",
180
+ content: {
181
+ "application/json": {
182
+ schema: {
183
+ type: "object",
184
+ properties: {
185
+ [envelopeKeys.data]: { type: "array", items: entityRef },
186
+ [envelopeKeys.meta]: {
187
+ type: "object",
188
+ properties: {
189
+ [metaKeys.requestIdKey]: { type: "string" },
190
+ page: { type: "integer" },
191
+ limit: { type: "integer" },
192
+ total: { type: "integer" },
193
+ hasNext: { type: "boolean" },
194
+ },
195
+ },
196
+ [envelopeKeys.error]: { nullable: true },
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ };
205
+ }
206
+ const createOp = entity.operations.find((o) => o.kind === "CREATE");
207
+ if (createOp) {
208
+ paths[`${basePath}/${resource}`] = {
209
+ ...(paths[`${basePath}/${resource}`] ?? {}),
210
+ post: {
211
+ summary: `Create ${entity.name}`,
212
+ requestBody: {
213
+ required: true,
214
+ content: { "application/json": { schema: entityRef } },
215
+ },
216
+ responses: {
217
+ 201: {
218
+ description: "Created",
219
+ content: {
220
+ "application/json": {
221
+ schema: {
222
+ type: "object",
223
+ properties: {
224
+ [envelopeKeys.data]: entityRef,
225
+ [envelopeKeys.meta]: { type: "object", properties: { [metaKeys.requestIdKey]: { type: "string" } } },
226
+ [envelopeKeys.error]: { nullable: true },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ },
232
+ },
233
+ },
234
+ };
235
+ }
236
+ const getOp = entity.operations.find((o) => o.kind === "GET");
237
+ if (getOp) {
238
+ paths[`${basePath}/${resource}/{id}`] = {
239
+ ...(paths[`${basePath}/${resource}/{id}`] ?? {}),
240
+ get: {
241
+ summary: `Get ${entity.name} by id`,
242
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
243
+ responses: {
244
+ 200: {
245
+ description: "OK",
246
+ content: {
247
+ "application/json": {
248
+ schema: {
249
+ type: "object",
250
+ properties: {
251
+ [envelopeKeys.data]: entityRef,
252
+ [envelopeKeys.meta]: { type: "object", properties: { [metaKeys.requestIdKey]: { type: "string" } } },
253
+ [envelopeKeys.error]: { nullable: true },
254
+ },
255
+ },
256
+ },
257
+ },
258
+ },
259
+ 404: { description: "Not found" },
260
+ },
261
+ },
262
+ };
263
+ }
264
+ const updateOp = entity.operations.find((o) => o.kind === "UPDATE");
265
+ if (updateOp) {
266
+ paths[`${basePath}/${resource}/{id}`] = {
267
+ ...(paths[`${basePath}/${resource}/{id}`] ?? {}),
268
+ patch: {
269
+ summary: `Update ${entity.name}`,
270
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
271
+ requestBody: {
272
+ required: true,
273
+ content: { "application/json": { schema: entityRef } },
274
+ },
275
+ responses: {
276
+ 200: {
277
+ description: "OK",
278
+ content: {
279
+ "application/json": {
280
+ schema: {
281
+ type: "object",
282
+ properties: {
283
+ [envelopeKeys.data]: entityRef,
284
+ [envelopeKeys.meta]: { type: "object", properties: { [metaKeys.requestIdKey]: { type: "string" } } },
285
+ [envelopeKeys.error]: { nullable: true },
286
+ },
287
+ },
288
+ },
289
+ },
290
+ },
291
+ 404: { description: "Not found" },
292
+ },
293
+ },
294
+ };
295
+ }
296
+ const deleteOp = entity.operations.find((o) => o.kind === "REMOVE");
297
+ if (deleteOp) {
298
+ paths[`${basePath}/${resource}/{id}`] = {
299
+ ...(paths[`${basePath}/${resource}/{id}`] ?? {}),
300
+ delete: {
301
+ summary: `Delete ${entity.name}`,
302
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
303
+ responses: {
304
+ 200: { description: "Deleted" },
305
+ 404: { description: "Not found" },
306
+ },
307
+ },
308
+ };
309
+ }
310
+ });
311
+ const securitySchemes = {};
312
+ const security = [];
313
+ if (policies?.auth?.jwt?.enabled) {
314
+ securitySchemes["bearerAuth"] = { type: "http", scheme: "bearer", bearerFormat: "JWT" };
315
+ security.push({ bearerAuth: [] });
316
+ }
317
+ const spec = {
318
+ openapi: "3.0.3",
319
+ info: {
320
+ title: rest.openapi?.title ?? "API",
321
+ version: rest.openapi?.version ?? "1.0.0",
322
+ },
323
+ servers: rest.openapi?.serverUrl ? [{ url: rest.openapi.serverUrl }] : [],
324
+ paths,
325
+ components: {
326
+ schemas: componentsSchemas,
327
+ securitySchemes,
328
+ },
329
+ };
330
+ if (security.length > 0)
331
+ spec.security = security;
332
+ project.createSourceFile(path.join(outDir, "openapi.json"), JSON.stringify(spec, null, 2), { overwrite: true });
333
+ }
334
+ function emitModels(project, outDir, entities) {
335
+ const sf = project.createSourceFile(path.join(outDir, "lib", "models.ts"), "", { overwrite: true });
336
+ sf.addStatements([`// Generated: model interfaces`]);
337
+ for (const e of entities) {
338
+ if (e.model && Object.keys(e.model).length > 0) {
339
+ const iface = sf.addInterface({
340
+ name: e.name,
341
+ isExported: true,
342
+ });
343
+ for (const [k, t] of Object.entries(e.model)) {
344
+ iface.addProperty({ name: k, type: t });
345
+ }
346
+ }
347
+ else {
348
+ sf.addStatements([`export type ${e.name} = Record<string, any>;`]);
349
+ }
350
+ sf.addStatements(["", ""]);
351
+ }
352
+ }
353
+ function emitRepositoryInterface(project, outDir, entity) {
354
+ const fileName = `${entity.name.toLowerCase()}.repository.ts`;
355
+ // Place interface in lib/repositories or similar
356
+ ensureDir(path.join(outDir, "lib", "repositories"));
357
+ const sf = project.createSourceFile(path.join(outDir, "lib", "repositories", fileName), "", { overwrite: true });
358
+ const modelType = entity.model ? entity.name : "any";
359
+ if (entity.model) {
360
+ sf.addImportDeclaration({ moduleSpecifier: `../models`, namedImports: [entity.name] });
361
+ }
362
+ const iface = sf.addInterface({
363
+ name: `I${entity.name}Repository`,
364
+ isExported: true,
365
+ });
366
+ // Standard CRUD signature
367
+ iface.addMethod({ name: "create", parameters: [{ name: "data", type: modelType }], returnType: `Promise<${modelType}>` });
368
+ iface.addMethod({ name: "findById", parameters: [{ name: "id", type: "string" }], returnType: `Promise<${modelType} | null>` });
369
+ iface.addMethod({ name: "list", parameters: [], returnType: `Promise<${modelType}[]>` });
370
+ iface.addMethod({ name: "update", parameters: [{ name: "id", type: "string" }, { name: "data", type: `Partial<${modelType}>` }], returnType: `Promise<${modelType} | null>` });
371
+ iface.addMethod({ name: "delete", parameters: [{ name: "id", type: "string" }], returnType: `Promise<boolean>` });
372
+ }
373
+ function emitInMemoryRepository(project, outDir, entity) {
374
+ const fileName = `${entity.name.toLowerCase()}.memory-repository.ts`;
375
+ // Place adapter in generated/base/repositories
376
+ ensureDir(path.join(outDir, "base", "repositories"));
377
+ const sf = project.createSourceFile(path.join(outDir, "base", "repositories", fileName), "", { overwrite: true });
378
+ const modelType = entity.model ? entity.name : "Record<string, any>";
379
+ sf.addImportDeclaration({ moduleSpecifier: `../../lib/repositories/${entity.name.toLowerCase()}.repository`, namedImports: [`I${entity.name}Repository`] });
380
+ if (entity.model) {
381
+ sf.addImportDeclaration({ moduleSpecifier: `../../lib/models`, namedImports: [entity.name] });
382
+ }
383
+ sf.addImportDeclaration({ moduleSpecifier: `../../lib/id`, namedImports: ["newId"] });
384
+ const cls = sf.addClass({
385
+ name: `InMemory${entity.name}Repository`,
386
+ isExported: true,
387
+ implements: [`I${entity.name}Repository`],
388
+ });
389
+ cls.addProperty({
390
+ name: "store",
391
+ type: `Map<string, ${modelType}>`,
392
+ initializer: "new Map()",
393
+ scope: Scope.Private,
394
+ });
395
+ // IMPLEMENTATION
396
+ // create
397
+ cls.addMethod({
398
+ name: "create",
399
+ isAsync: true,
400
+ parameters: [{ name: "data", type: modelType }],
401
+ returnType: `Promise<${modelType}>`,
402
+ statements: [
403
+ `const id = newId();`,
404
+ // rough logic depending on if data already has id
405
+ `const row = { ...data, id } as any;`,
406
+ `this.store.set(id, row);`,
407
+ `return row;`
408
+ ]
409
+ });
410
+ // findById
411
+ cls.addMethod({
412
+ name: "findById",
413
+ isAsync: true,
414
+ parameters: [{ name: "id", type: "string" }],
415
+ returnType: `Promise<${modelType} | null>`,
416
+ statements: [`return this.store.get(id) ?? null;`]
417
+ });
418
+ // list
419
+ cls.addMethod({
420
+ name: "list",
421
+ isAsync: true,
422
+ parameters: [],
423
+ returnType: `Promise<${modelType}[]>`,
424
+ statements: [`return Array.from(this.store.values());`]
425
+ });
426
+ // update
427
+ cls.addMethod({
428
+ name: "update",
429
+ isAsync: true,
430
+ parameters: [{ name: "id", type: "string" }, { name: "data", type: `Partial<${modelType}>` }],
431
+ returnType: `Promise<${modelType} | null>`,
432
+ statements: [
433
+ `const existing = this.store.get(id);`,
434
+ `if (!existing) return null;`,
435
+ `const updated = { ...existing, ...data } as any;`,
436
+ `this.store.set(id, updated);`,
437
+ `return updated;`
438
+ ]
439
+ });
440
+ // delete
441
+ cls.addMethod({
442
+ name: "delete",
443
+ isAsync: true,
444
+ parameters: [{ name: "id", type: "string" }],
445
+ returnType: `Promise<boolean>`,
446
+ statements: [`return this.store.delete(id);`]
447
+ });
448
+ }
449
+ function emitPrismaSchema(outDir, policies, entities) {
450
+ // Generate schema.prisma
451
+ const lines = [];
452
+ // Datasource & Generator
453
+ const provider = policies?.db?.provider === "prisma" ? "sqlite" : "sqlite"; // Default to sqlite for dev
454
+ const url = policies?.db?.url ?? "file:./dev.db";
455
+ lines.push(`datasource db {`);
456
+ lines.push(` provider = "${provider}"`);
457
+ lines.push(` url = "${url}"`);
458
+ lines.push(`}`);
459
+ lines.push(``);
460
+ lines.push(`generator client {`);
461
+ lines.push(` provider = "prisma-client-js"`);
462
+ lines.push(`}`);
463
+ lines.push(``);
464
+ for (const entity of entities ?? []) {
465
+ lines.push(`model ${entity.name} {`);
466
+ // Always assume ID
467
+ lines.push(` id String @id @default(uuid())`);
468
+ if (entity.model) {
469
+ for (const [key, type] of Object.entries(entity.model)) {
470
+ if (key === "id")
471
+ continue; // handled above
472
+ let prismaType = "String";
473
+ if (type === "number")
474
+ prismaType = "Float"; // or Int, simplify for now
475
+ if (type === "boolean")
476
+ prismaType = "Boolean";
477
+ // date?
478
+ lines.push(` ${key} ${prismaType}`);
479
+ }
480
+ }
481
+ lines.push(`}`);
482
+ lines.push(``);
483
+ }
484
+ const prismaDir = path.join(outDir, "prisma");
485
+ ensureDir(prismaDir);
486
+ fs.writeFileSync(path.join(prismaDir, "schema.prisma"), lines.join("\n"));
487
+ }
488
+ function emitPrismaRepository(project, outDir, entity) {
489
+ const fileName = `${entity.name.toLowerCase()}.prisma-repository.ts`;
490
+ // Place adapter in generated/base/repositories
491
+ ensureDir(path.join(outDir, "base", "repositories"));
492
+ const sf = project.createSourceFile(path.join(outDir, "base", "repositories", fileName), "", { overwrite: true });
493
+ const modelType = entity.model ? entity.name : "any";
494
+ sf.addImportDeclaration({ moduleSpecifier: `../../lib/repositories/${entity.name.toLowerCase()}.repository`, namedImports: [`I${entity.name}Repository`] });
495
+ if (entity.model) {
496
+ sf.addImportDeclaration({ moduleSpecifier: `../../lib/models`, namedImports: [entity.name] });
497
+ }
498
+ // Import Prisma Client (usually from a singleton or instantiated here)
499
+ sf.addImportDeclaration({
500
+ moduleSpecifier: "@prisma/client",
501
+ namedImports: ["PrismaClient"]
502
+ });
503
+ const cls = sf.addClass({
504
+ name: `Prisma${entity.name}Repository`,
505
+ isExported: true,
506
+ implements: [`I${entity.name}Repository`],
507
+ });
508
+ // For simplicity, instantiate client here. In real app, inject singleton.
509
+ cls.addProperty({
510
+ name: "prisma",
511
+ initializer: "new PrismaClient()",
512
+ scope: Scope.Private
513
+ });
514
+ // IMPLEMENTATION using Prisma Client
515
+ // Prisma generates model accessor as camelCase of model name (e.g. Product -> product)
516
+ const delegate = entity.name.charAt(0).toLowerCase() + entity.name.slice(1);
517
+ // create
518
+ cls.addMethod({
519
+ name: "create",
520
+ isAsync: true,
521
+ parameters: [{ name: "data", type: modelType }],
522
+ returnType: `Promise<${modelType}>`,
523
+ statements: [
524
+ // prisma create requires strictly matching data, we might need cast
525
+ `return this.prisma.${delegate}.create({ data: data as any }) as any;`
526
+ ]
527
+ });
528
+ // findById
529
+ cls.addMethod({
530
+ name: "findById",
531
+ isAsync: true,
532
+ parameters: [{ name: "id", type: "string" }],
533
+ returnType: `Promise<${modelType} | null>`,
534
+ statements: [`return this.prisma.${delegate}.findUnique({ where: { id } }) as any;`]
535
+ });
536
+ // list
537
+ cls.addMethod({
538
+ name: "list",
539
+ isAsync: true,
540
+ parameters: [],
541
+ returnType: `Promise<${modelType}[]>`,
542
+ statements: [`return this.prisma.${delegate}.findMany() as any;`]
543
+ });
544
+ // update
545
+ cls.addMethod({
546
+ name: "update",
547
+ isAsync: true,
548
+ parameters: [{ name: "id", type: "string" }, { name: "data", type: `Partial<${modelType}>` }],
549
+ returnType: `Promise<${modelType} | null>`,
550
+ statements: [
551
+ `return this.prisma.${delegate}.update({ where: { id }, data: data as any }) as any;`
552
+ ]
553
+ });
554
+ // delete
555
+ cls.addMethod({
556
+ name: "delete",
557
+ isAsync: true,
558
+ parameters: [{ name: "id", type: "string" }],
559
+ returnType: `Promise<boolean>`,
560
+ statements: [
561
+ `try {`,
562
+ ` await this.prisma.${delegate}.delete({ where: { id } });`,
563
+ ` return true;`,
564
+ `} catch { return false; }`
565
+ ]
566
+ });
567
+ }
568
+ function emitController(project, outDir, entity, policies) {
569
+ const fileName = `${entity.name.toLowerCase()}.controller.ts`;
570
+ const sf = project.createSourceFile(path.join(outDir, "controllers", fileName), "", { overwrite: true });
571
+ // Use relative sibling imports
572
+ sf.addImportDeclaration({
573
+ moduleSpecifier: `../services/${entity.name.toLowerCase()}.service`,
574
+ namedImports: [`${entity.name}Service`],
575
+ });
576
+ // Decide which repo adapter to import
577
+ const dbProvider = policies?.db?.provider;
578
+ const isPrisma = dbProvider === "prisma";
579
+ if (isPrisma) {
580
+ sf.addImportDeclaration({
581
+ moduleSpecifier: `../base/repositories/${entity.name.toLowerCase()}.prisma-repository`,
582
+ namedImports: [`Prisma${entity.name}Repository`],
583
+ });
584
+ }
585
+ else {
586
+ sf.addImportDeclaration({
587
+ moduleSpecifier: `../base/repositories/${entity.name.toLowerCase()}.memory-repository`,
588
+ namedImports: [`InMemory${entity.name}Repository`],
589
+ });
590
+ }
591
+ sf.addImportDeclaration({
592
+ moduleSpecifier: `../lib/models`,
593
+ namedImports: [entity.name],
594
+ });
595
+ const className = `${entity.name}Controller`;
596
+ const cls = sf.addClass({ name: className, isExported: true });
597
+ // Wiring: Create Repo -> Inject to Service
598
+ const repoClass = isPrisma ? `Prisma${entity.name}Repository` : `InMemory${entity.name}Repository`;
599
+ cls.addProperty({
600
+ name: "service",
601
+ scope: Scope.Private,
602
+ type: `${entity.name}Service`,
603
+ initializer: `new ${entity.name}Service(new ${repoClass}())`
604
+ });
605
+ for (const op of entity.operations) {
606
+ // Note: Ops are now async because repo is async
607
+ const returnType = entity.model ? entity.name : "any";
608
+ if (op.kind === "CREATE") {
609
+ cls.addMethod({
610
+ name: op.methodName, isAsync: true,
611
+ parameters: [{ name: "payload", type: entity.name }],
612
+ returnType: `Promise<${returnType}>`,
613
+ statements: [`return this.service.${op.methodName}(payload);`],
614
+ });
615
+ }
616
+ else if (op.kind === "GET") {
617
+ cls.addMethod({
618
+ name: op.methodName, isAsync: true,
619
+ parameters: [{ name: "id", type: "string" }],
620
+ returnType: `Promise<${returnType} | null>`,
621
+ statements: [`return this.service.${op.methodName}(id);`],
622
+ });
623
+ }
624
+ else if (op.kind === "LIST") {
625
+ cls.addMethod({
626
+ name: op.methodName, isAsync: true,
627
+ parameters: [],
628
+ returnType: `Promise<${returnType}[]>`,
629
+ statements: [`return this.service.${op.methodName}();`],
630
+ });
631
+ }
632
+ else if (op.kind === "UPDATE") {
633
+ cls.addMethod({
634
+ name: op.methodName, isAsync: true,
635
+ parameters: [
636
+ { name: "id", type: "string" },
637
+ { name: "payload", type: `Partial<${entity.name}>` },
638
+ ],
639
+ returnType: `Promise<${returnType} | null>`,
640
+ statements: [`return this.service.${op.methodName}(id, payload);`],
641
+ });
642
+ }
643
+ else if (op.kind === "REMOVE") {
644
+ cls.addMethod({
645
+ name: op.methodName, isAsync: true,
646
+ parameters: [{ name: "id", type: "string" }],
647
+ returnType: "Promise<boolean>",
648
+ statements: [`return this.service.${op.methodName}(id);`],
649
+ });
650
+ }
651
+ }
652
+ }
653
+ function emitBaseService(project, outDir, entity) {
654
+ const fileName = `${entity.name.toLowerCase()}.service.base.ts`;
655
+ // Ensure base directory exists
656
+ ensureDir(path.join(outDir, "base", "services"));
657
+ const sf = project.createSourceFile(path.join(outDir, "base", "services", fileName), "", { overwrite: true });
658
+ const className = `Base${entity.name}Service`;
659
+ const cls = sf.addClass({
660
+ name: className,
661
+ isExported: true,
662
+ isAbstract: true,
663
+ });
664
+ // Import Repository Interface
665
+ sf.addImportDeclaration({
666
+ moduleSpecifier: `../../lib/repositories/${entity.name.toLowerCase()}.repository`,
667
+ namedImports: [`I${entity.name}Repository`],
668
+ });
669
+ // import model type if available
670
+ const modelType = entity.model ? entity.name : "any";
671
+ if (entity.model) {
672
+ sf.addImportDeclaration({ moduleSpecifier: "../../lib/models", namedImports: [entity.name] });
673
+ }
674
+ // Constructor Injection
675
+ cls.addConstructor({
676
+ parameters: [{ name: "repo", type: `I${entity.name}Repository`, scope: Scope.Protected }]
677
+ });
678
+ // LIFECYCLE HOOKS
679
+ // Create
680
+ cls.addMethod({
681
+ name: "beforeCreate", scope: Scope.Protected, isAsync: true,
682
+ parameters: [{ name: "data", type: modelType }],
683
+ returnType: `Promise<${modelType}>`,
684
+ statements: [`return data;`]
685
+ });
686
+ cls.addMethod({
687
+ name: "afterCreate", scope: Scope.Protected, isAsync: true,
688
+ parameters: [{ name: "result", type: modelType }],
689
+ returnType: `Promise<void>`,
690
+ statements: []
691
+ });
692
+ // Update
693
+ cls.addMethod({
694
+ name: "beforeUpdate", scope: Scope.Protected, isAsync: true,
695
+ parameters: [{ name: "id", type: "string" }, { name: "data", type: `Partial<${modelType}>` }],
696
+ returnType: `Promise<Partial<${modelType}>>`,
697
+ statements: [`return data;`]
698
+ });
699
+ cls.addMethod({
700
+ name: "afterUpdate", scope: Scope.Protected, isAsync: true,
701
+ parameters: [{ name: "id", type: "string" }, { name: "result", type: modelType }],
702
+ returnType: `Promise<void>`,
703
+ statements: []
704
+ });
705
+ // Delete
706
+ cls.addMethod({
707
+ name: "beforeDelete", scope: Scope.Protected, isAsync: true,
708
+ parameters: [{ name: "id", type: "string" }],
709
+ returnType: `Promise<boolean>`,
710
+ statements: [`return true;`]
711
+ });
712
+ cls.addMethod({
713
+ name: "afterDelete", scope: Scope.Protected, isAsync: true,
714
+ parameters: [{ name: "id", type: "string" }],
715
+ returnType: `Promise<void>`,
716
+ statements: []
717
+ });
718
+ for (const op of entity.operations) {
719
+ if (op.kind === "CREATE") {
720
+ cls.addMethod({
721
+ name: op.methodName, isAsync: true,
722
+ parameters: [{ name: "data", type: modelType }],
723
+ returnType: `Promise<${modelType}>`,
724
+ statements: [
725
+ `const processed = await this.beforeCreate(data);`,
726
+ `const result = await this.repo.create(processed);`,
727
+ `await this.afterCreate(result);`,
728
+ `return result;`,
729
+ ],
730
+ });
731
+ }
732
+ else if (op.kind === "GET") {
733
+ cls.addMethod({
734
+ name: op.methodName, isAsync: true,
735
+ parameters: [{ name: "id", type: "string" }],
736
+ returnType: `Promise<${modelType} | null>`,
737
+ statements: [
738
+ `return this.repo.findById(id);`,
739
+ ],
740
+ });
741
+ }
742
+ else if (op.kind === "LIST") {
743
+ cls.addMethod({
744
+ name: op.methodName, isAsync: true,
745
+ parameters: [],
746
+ returnType: `Promise<${modelType}[]>`,
747
+ statements: [
748
+ `return this.repo.list();`,
749
+ ],
750
+ });
751
+ }
752
+ else if (op.kind === "UPDATE") {
753
+ cls.addMethod({
754
+ name: op.methodName, isAsync: true,
755
+ parameters: [
756
+ { name: "id", type: "string" },
757
+ { name: "data", type: `Partial<${modelType}>` },
758
+ ],
759
+ returnType: `Promise<${modelType} | null>`,
760
+ statements: [
761
+ `const processed = await this.beforeUpdate(id, data);`,
762
+ `const result = await this.repo.update(id, processed);`,
763
+ `if (result) { await this.afterUpdate(id, result); }`,
764
+ `return result;`,
765
+ ],
766
+ });
767
+ }
768
+ else if (op.kind === "REMOVE") {
769
+ cls.addMethod({
770
+ name: op.methodName, isAsync: true,
771
+ parameters: [{ name: "id", type: "string" }],
772
+ returnType: "Promise<boolean>",
773
+ statements: [
774
+ `if (!(await this.beforeDelete(id))) return false;`,
775
+ `const success = await this.repo.delete(id);`,
776
+ `if (success) { await this.afterDelete(id); }`,
777
+ `return success;`,
778
+ ],
779
+ });
780
+ }
781
+ }
782
+ }
783
+ function emitUserService(project, outDir, entity) {
784
+ // Move user code to 'services' directory WITHIN the outDir
785
+ // outDir is the root of the generated app.
786
+ const userServiceDir = path.join(outDir, "services");
787
+ ensureDir(userServiceDir);
788
+ const fileName = `${entity.name.toLowerCase()}.service.ts`;
789
+ const filePath = path.join(userServiceDir, fileName);
790
+ // CRITICAL: Check if file exists. If so, DO NOT OVERWRITE.
791
+ if (fs.existsSync(filePath)) {
792
+ // If it exists, we assume user might have custom constructor.
793
+ // However, since we changed the base class constructor signature (added repo),
794
+ // the user class will inherit it. If the user overrides constructor, they need to update it.
795
+ // For now, we assume simple extension.
796
+ return;
797
+ }
798
+ const sf = project.createSourceFile(filePath, "", { overwrite: false });
799
+ // Import Base Service from the generated directory
800
+ // services/foo.service.ts -> ../base/services/foo.service.base.ts
801
+ sf.addImportDeclaration({
802
+ moduleSpecifier: `../base/services/${entity.name.toLowerCase()}.service.base`,
803
+ namedImports: [`Base${entity.name}Service`],
804
+ });
805
+ const className = `${entity.name}Service`;
806
+ const cls = sf.addClass({
807
+ name: className,
808
+ isExported: true,
809
+ extends: `Base${entity.name}Service`,
810
+ });
811
+ // Helper comments
812
+ cls.addJsDoc({
813
+ description: `User Implementation of ${entity.name}Service.\nThis file is generated once and will not be overwritten.\nAdd your business logic here.`
814
+ });
815
+ }
816
+ function emitServiceTest(project, outDir, entity) {
817
+ const createOp = entity.operations.find(op => op.kind === "CREATE");
818
+ if (!createOp)
819
+ return; // skip test if no create operation
820
+ const hasIdField = !!(entity.model && Object.prototype.hasOwnProperty.call(entity.model, "id"));
821
+ const fileName = `${entity.name.toLowerCase()}.service.spec.ts`;
822
+ // Place tests next to user services
823
+ const userServiceDir = path.join(outDir, "services");
824
+ ensureDir(userServiceDir);
825
+ const sf = project.createSourceFile(path.join(userServiceDir, fileName), "", { overwrite: true });
826
+ sf.addImportDeclaration({
827
+ moduleSpecifier: "vitest",
828
+ namedImports: ["describe", "it", "expect", "beforeEach"],
829
+ });
830
+ sf.addImportDeclaration({
831
+ moduleSpecifier: `./${entity.name.toLowerCase()}.service`,
832
+ namedImports: [`${entity.name}Service`],
833
+ });
834
+ // Import InMemory Repo to inject
835
+ sf.addImportDeclaration({
836
+ moduleSpecifier: `../base/repositories/${entity.name.toLowerCase()}.memory-repository`,
837
+ namedImports: [`InMemory${entity.name}Repository`],
838
+ });
839
+ sf.addStatements([
840
+ `describe("${entity.name}Service", () => {`,
841
+ ` let service: ${entity.name}Service;`,
842
+ ` let repo: InMemory${entity.name}Repository;`,
843
+ ``,
844
+ ` beforeEach(() => {`,
845
+ ` repo = new InMemory${entity.name}Repository();`,
846
+ ` service = new ${entity.name}Service(repo);`,
847
+ ` });`,
848
+ ``,
849
+ ` it("should create a ${entity.name.toLowerCase()}", async () => {`,
850
+ ` const data = { /* mock data */ } as any;`,
851
+ ` const result = await service.${createOp.methodName}(data);`,
852
+ ` expect(result).toBeDefined();`,
853
+ ...(hasIdField ? [` expect((result as any).id).toBeDefined();`] : []),
854
+ ` });`,
855
+ `});`
856
+ ]);
857
+ }
858
+ //# sourceMappingURL=backend-tsmorph.js.map