@soapjs/cli 1.0.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 (149) hide show
  1. package/.nvmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +360 -0
  4. package/build/cli.d.ts +3 -0
  5. package/build/cli.js +50 -0
  6. package/build/commands/add/add.command.d.ts +2 -0
  7. package/build/commands/add/add.command.js +709 -0
  8. package/build/commands/add/command-plan.d.ts +15 -0
  9. package/build/commands/add/command-plan.js +182 -0
  10. package/build/commands/add/entity-plan.d.ts +7 -0
  11. package/build/commands/add/entity-plan.js +106 -0
  12. package/build/commands/add/event-plan.d.ts +8 -0
  13. package/build/commands/add/event-plan.js +59 -0
  14. package/build/commands/add/query-plan.d.ts +10 -0
  15. package/build/commands/add/query-plan.js +156 -0
  16. package/build/commands/add/repository-plan.d.ts +11 -0
  17. package/build/commands/add/repository-plan.js +252 -0
  18. package/build/commands/add/resource-plan.d.ts +52 -0
  19. package/build/commands/add/resource-plan.js +2031 -0
  20. package/build/commands/add/route-plan.d.ts +24 -0
  21. package/build/commands/add/route-plan.js +256 -0
  22. package/build/commands/add/socket-plan.d.ts +9 -0
  23. package/build/commands/add/socket-plan.js +81 -0
  24. package/build/commands/add/use-case-plan.d.ts +7 -0
  25. package/build/commands/add/use-case-plan.js +86 -0
  26. package/build/commands/check/check.command.d.ts +2 -0
  27. package/build/commands/check/check.command.js +113 -0
  28. package/build/commands/create/create.command.d.ts +2 -0
  29. package/build/commands/create/create.command.js +234 -0
  30. package/build/commands/create/project-plan.d.ts +44 -0
  31. package/build/commands/create/project-plan.js +1430 -0
  32. package/build/commands/doctor/doctor.command.d.ts +2 -0
  33. package/build/commands/doctor/doctor.command.js +38 -0
  34. package/build/commands/generate/bruno-analysis.d.ts +19 -0
  35. package/build/commands/generate/bruno-analysis.js +51 -0
  36. package/build/commands/generate/bruno-plan.d.ts +6 -0
  37. package/build/commands/generate/bruno-plan.js +326 -0
  38. package/build/commands/generate/generate.command.d.ts +2 -0
  39. package/build/commands/generate/generate.command.js +130 -0
  40. package/build/commands/info/info.command.d.ts +2 -0
  41. package/build/commands/info/info.command.js +26 -0
  42. package/build/commands/remove/remove.command.d.ts +2 -0
  43. package/build/commands/remove/remove.command.js +328 -0
  44. package/build/commands/shared/common-options.d.ts +10 -0
  45. package/build/commands/shared/common-options.js +23 -0
  46. package/build/commands/update/update.command.d.ts +2 -0
  47. package/build/commands/update/update.command.js +155 -0
  48. package/build/config/auth-policy.d.ts +4 -0
  49. package/build/config/auth-policy.js +54 -0
  50. package/build/config/find-soap-root.d.ts +1 -0
  51. package/build/config/find-soap-root.js +22 -0
  52. package/build/config/load-soap-config.d.ts +2 -0
  53. package/build/config/load-soap-config.js +30 -0
  54. package/build/config/schemas/types.d.ts +127 -0
  55. package/build/config/schemas/types.js +2 -0
  56. package/build/config/schemas/validation.d.ts +5 -0
  57. package/build/config/schemas/validation.js +130 -0
  58. package/build/config/soap-config.service.d.ts +4 -0
  59. package/build/config/soap-config.service.js +24 -0
  60. package/build/config/write-soap-config.d.ts +8 -0
  61. package/build/config/write-soap-config.js +25 -0
  62. package/build/core/command-context.d.ts +20 -0
  63. package/build/core/command-context.js +30 -0
  64. package/build/core/errors.d.ts +6 -0
  65. package/build/core/errors.js +23 -0
  66. package/build/core/output.d.ts +12 -0
  67. package/build/core/output.js +30 -0
  68. package/build/core/result.d.ts +9 -0
  69. package/build/core/result.js +11 -0
  70. package/build/dependencies/dependency-resolver.d.ts +6 -0
  71. package/build/dependencies/dependency-resolver.js +68 -0
  72. package/build/dependencies/package-manager.d.ts +7 -0
  73. package/build/dependencies/package-manager.js +54 -0
  74. package/build/index.d.ts +2 -0
  75. package/build/index.js +9 -0
  76. package/build/io/conflict-policy.d.ts +10 -0
  77. package/build/io/conflict-policy.js +32 -0
  78. package/build/io/file-writer.d.ts +19 -0
  79. package/build/io/file-writer.js +65 -0
  80. package/build/io/format-file.d.ts +1 -0
  81. package/build/io/format-file.js +13 -0
  82. package/build/presets/create-presets.d.ts +4 -0
  83. package/build/presets/create-presets.js +97 -0
  84. package/build/presets/index.d.ts +2 -0
  85. package/build/presets/index.js +18 -0
  86. package/build/presets/preset.types.d.ts +6 -0
  87. package/build/presets/preset.types.js +2 -0
  88. package/build/prompts/add-resource.prompt.d.ts +13 -0
  89. package/build/prompts/add-resource.prompt.js +80 -0
  90. package/build/prompts/add-route.prompt.d.ts +16 -0
  91. package/build/prompts/add-route.prompt.js +140 -0
  92. package/build/prompts/create-project.prompt.d.ts +11 -0
  93. package/build/prompts/create-project.prompt.js +156 -0
  94. package/build/prompts/generate-bruno.prompt.d.ts +7 -0
  95. package/build/prompts/generate-bruno.prompt.js +21 -0
  96. package/build/prompts/index.d.ts +8 -0
  97. package/build/prompts/index.js +24 -0
  98. package/build/prompts/inquirer-prompt-adapter.d.ts +8 -0
  99. package/build/prompts/inquirer-prompt-adapter.js +52 -0
  100. package/build/prompts/mock-prompt-adapter.d.ts +13 -0
  101. package/build/prompts/mock-prompt-adapter.js +60 -0
  102. package/build/prompts/prompt-adapter.d.ts +7 -0
  103. package/build/prompts/prompt-adapter.js +2 -0
  104. package/build/prompts/prompt.types.d.ts +26 -0
  105. package/build/prompts/prompt.types.js +2 -0
  106. package/build/registry/registry.service.d.ts +19 -0
  107. package/build/registry/registry.service.js +68 -0
  108. package/build/resolvers/add-resource.resolver.d.ts +23 -0
  109. package/build/resolvers/add-resource.resolver.js +73 -0
  110. package/build/resolvers/add-route.resolver.d.ts +34 -0
  111. package/build/resolvers/add-route.resolver.js +83 -0
  112. package/build/resolvers/create-config.resolver.d.ts +32 -0
  113. package/build/resolvers/create-config.resolver.js +57 -0
  114. package/build/resolvers/generate-bruno.resolver.d.ts +17 -0
  115. package/build/resolvers/generate-bruno.resolver.js +23 -0
  116. package/build/resolvers/index.d.ts +5 -0
  117. package/build/resolvers/index.js +21 -0
  118. package/build/resolvers/resolver.types.d.ts +8 -0
  119. package/build/resolvers/resolver.types.js +2 -0
  120. package/build/summary/create-summary.d.ts +2 -0
  121. package/build/summary/create-summary.js +24 -0
  122. package/build/summary/index.d.ts +1 -0
  123. package/build/summary/index.js +17 -0
  124. package/build/templates/naming.d.ts +11 -0
  125. package/build/templates/naming.js +30 -0
  126. package/build/templates/template-context.d.ts +6 -0
  127. package/build/templates/template-context.js +2 -0
  128. package/build/templates/template-engine.d.ts +1 -0
  129. package/build/templates/template-engine.js +10 -0
  130. package/build/templates/template-resolver.d.ts +2 -0
  131. package/build/templates/template-resolver.js +17 -0
  132. package/build/terminal/terminal-capabilities.d.ts +6 -0
  133. package/build/terminal/terminal-capabilities.js +14 -0
  134. package/docs/adr/0001-soap-cli-project-aware-generator.md +108 -0
  135. package/docs/cli/add-resource.md +127 -0
  136. package/docs/cli/add-route.md +79 -0
  137. package/docs/cli/bruno.md +58 -0
  138. package/docs/cli/create.md +73 -0
  139. package/docs/cli/index.md +92 -0
  140. package/docs/cli/interactive-mode.md +61 -0
  141. package/docs/cli/remove.md +45 -0
  142. package/docs/guides/auth.md +90 -0
  143. package/docs/guides/cqrs-events-realtime.md +100 -0
  144. package/docs/guides/index.md +24 -0
  145. package/docs/guides/quality-and-safety.md +88 -0
  146. package/docs/guides/regular-api.md +119 -0
  147. package/docs/guides/storage.md +101 -0
  148. package/docs/plans/interactive-mode-plan.md +601 -0
  149. package/package.json +44 -0
@@ -0,0 +1,2031 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRouteApiDocOptions = exports.createAuthDecorator = exports.parseResourceFieldDefinitions = exports.parseCrudRouteMatrix = exports.createControllersFile = exports.createFeaturesIndexFile = exports.createResourcesFile = exports.createResourceFiles = exports.formatResourceAddPlanningSummary = exports.createResourceAddPlanningSummary = exports.createResourceEntry = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const errors_1 = require("../../core/errors");
9
+ const auth_policy_1 = require("../../config/auth-policy");
10
+ const project_plan_1 = require("../create/project-plan");
11
+ const naming_1 = require("../../templates/naming");
12
+ function createResourceEntry(plan) {
13
+ const names = (0, naming_1.createNameVariants)(plan.name);
14
+ return {
15
+ name: names.kebabName,
16
+ path: `/${names.pluralName}`,
17
+ crud: plan.crud,
18
+ db: plan.db,
19
+ auth: plan.auth,
20
+ zone: plan.zone,
21
+ fields: normalizeResourceFields(plan.fields),
22
+ policy: plan.policy,
23
+ generatedAt: new Date().toISOString(),
24
+ };
25
+ }
26
+ exports.createResourceEntry = createResourceEntry;
27
+ function createResourceAddPlanningSummary(plan) {
28
+ const names = (0, naming_1.createNameVariants)(plan.name);
29
+ const itemName = singularizeResourceName(names.kebabName);
30
+ const collectionName = (0, naming_1.createNameVariants)(itemName).pluralName;
31
+ const steps = [
32
+ { type: "entity", name: itemName },
33
+ { type: "repository", name: `${itemName} port` },
34
+ ];
35
+ if (plan.db !== "none") {
36
+ steps.push({ type: "repository", name: `${itemName} ${plan.db} implementation` });
37
+ }
38
+ else {
39
+ steps.push({ type: "repository", name: `${itemName} in-memory implementation` });
40
+ }
41
+ if (plan.architecture === "cqrs") {
42
+ steps.push({ type: "command", name: `create-${itemName}` }, { type: "query", name: `get-${itemName}` }, { type: "query", name: `list-${collectionName}` });
43
+ if (plan.crud) {
44
+ steps.push({ type: "command", name: `update-${itemName}` }, { type: "command", name: `delete-${itemName}` });
45
+ }
46
+ }
47
+ else {
48
+ steps.push({ type: "use-case", name: `create-${itemName}` }, { type: "use-case", name: `get-${itemName}` }, { type: "use-case", name: `list-${collectionName}` });
49
+ if (plan.crud) {
50
+ steps.push({ type: "use-case", name: `update-${itemName}` }, { type: "use-case", name: `delete-${itemName}` });
51
+ }
52
+ }
53
+ const itemNames = (0, naming_1.createNameVariants)(itemName);
54
+ const collectionNames = (0, naming_1.createNameVariants)(collectionName);
55
+ const routeActions = createRegularCrudActions(itemNames, collectionNames, plan.crudRoutes)
56
+ .filter((action) => plan.crud || action.kind === "list" || action.kind === "get");
57
+ steps.push(...routeActions.map((action) => ({
58
+ type: "route",
59
+ name: `${action.method.toUpperCase()} ${resolveCrudSummaryPath(`/${names.pluralName}`, action.path)}`,
60
+ })));
61
+ steps.push({ type: "contract", name: `${names.kebabName} route contracts` }, { type: "bruno", name: `${names.kebabName} requests` });
62
+ return {
63
+ resource: names.kebabName,
64
+ architecture: plan.architecture,
65
+ steps,
66
+ };
67
+ }
68
+ exports.createResourceAddPlanningSummary = createResourceAddPlanningSummary;
69
+ function formatResourceAddPlanningSummary(summary) {
70
+ const lines = [
71
+ `Resource plan: ${summary.resource} (${summary.architecture})`,
72
+ ...summary.steps.map((step) => `- ${step.type}: ${step.name}`),
73
+ ];
74
+ return lines.join("\n");
75
+ }
76
+ exports.formatResourceAddPlanningSummary = formatResourceAddPlanningSummary;
77
+ function singularizeResourceName(name) {
78
+ if (name.endsWith("ies")) {
79
+ return `${name.slice(0, -3)}y`;
80
+ }
81
+ if (name.endsWith("ses") || name.endsWith("xes") || name.endsWith("zes") || name.endsWith("ches") || name.endsWith("shes")) {
82
+ return name.slice(0, -2);
83
+ }
84
+ if (name.endsWith("s") && name.length > 1) {
85
+ return name.slice(0, -1);
86
+ }
87
+ return name;
88
+ }
89
+ function createResourceFiles(plan) {
90
+ if (plan.architecture === "cqrs" && plan.crud) {
91
+ return createCqrsCrudResourceFiles(plan);
92
+ }
93
+ if (plan.architecture === "regular" && plan.crud) {
94
+ return createRegularCrudResourceFiles(plan);
95
+ }
96
+ const names = (0, naming_1.createNameVariants)(plan.name);
97
+ const root = path_1.default.posix.join(plan.featuresRoot, names.kebabName);
98
+ const controllerPath = `${root}/api/${names.kebabName}.controller.ts`;
99
+ const files = [
100
+ {
101
+ path: `${root}/domain/${names.kebabName}.entity.ts`,
102
+ type: "entity",
103
+ owner: names.kebabName,
104
+ content: createEntityTs(plan.name, plan.fields),
105
+ },
106
+ {
107
+ path: `${root}/application/ports/${names.kebabName}.repository.ts`,
108
+ type: "repository",
109
+ owner: names.kebabName,
110
+ content: createRepositoryPortTs(plan.name),
111
+ },
112
+ {
113
+ path: `${root}/data/${names.kebabName}.memory-repository.ts`,
114
+ type: "repository",
115
+ owner: names.kebabName,
116
+ content: createMemoryRepositoryTs(plan.name),
117
+ },
118
+ ...(plan.db === "mongo"
119
+ ? [
120
+ {
121
+ path: `${root}/data/${names.kebabName}.mongo-repository.ts`,
122
+ type: "repository",
123
+ owner: names.kebabName,
124
+ content: createMongoRepositoryTs(plan.name, plan.fields),
125
+ },
126
+ ]
127
+ : []),
128
+ ...(isSqlDatabase(plan.db)
129
+ ? [
130
+ {
131
+ path: `${root}/data/${names.kebabName}.sql-repository.ts`,
132
+ type: "repository",
133
+ owner: names.kebabName,
134
+ content: createSqlRepositoryTs(plan.name, plan.db, plan.fields),
135
+ },
136
+ ]
137
+ : []),
138
+ {
139
+ path: `${root}/application/use-cases/${names.kebabName}.use-cases.ts`,
140
+ type: "use-case",
141
+ owner: names.kebabName,
142
+ content: createUseCasesTs(plan.name),
143
+ },
144
+ {
145
+ path: `${root}/setup.ts`,
146
+ type: "resource",
147
+ owner: names.kebabName,
148
+ content: createSetupTs(plan.name, plan.db),
149
+ },
150
+ {
151
+ path: controllerPath,
152
+ type: "resource",
153
+ owner: names.kebabName,
154
+ content: createControllerTs(plan),
155
+ },
156
+ {
157
+ path: `${root}/index.ts`,
158
+ type: "resource",
159
+ owner: names.kebabName,
160
+ content: createFeatureIndexTs(plan.name),
161
+ },
162
+ ];
163
+ return files;
164
+ }
165
+ exports.createResourceFiles = createResourceFiles;
166
+ function createCqrsCrudResourceFiles(plan) {
167
+ const featureNames = (0, naming_1.createNameVariants)(plan.name);
168
+ const itemNames = (0, naming_1.createNameVariants)(singularizeResourceName(featureNames.kebabName));
169
+ const collectionNames = (0, naming_1.createNameVariants)(itemNames.pluralName);
170
+ const root = path_1.default.posix.join(plan.featuresRoot, featureNames.kebabName);
171
+ const actions = createRegularCrudActions(itemNames, collectionNames, plan.crudRoutes);
172
+ const commands = actions.filter((action) => action.kind === "create" || action.kind === "update" || action.kind === "delete");
173
+ const queries = actions.filter((action) => action.kind === "list" || action.kind === "get");
174
+ const resource = createResourceEntry(plan);
175
+ return [
176
+ {
177
+ path: `${root}/domain/${itemNames.kebabName}.entity.ts`,
178
+ type: "entity",
179
+ owner: featureNames.kebabName,
180
+ content: createEntityTs(itemNames.kebabName, plan.fields),
181
+ },
182
+ {
183
+ path: `${root}/domain/${itemNames.kebabName}.entity.spec.ts`,
184
+ type: "entity",
185
+ owner: featureNames.kebabName,
186
+ content: createEntitySpecTs(itemNames, plan.fields),
187
+ },
188
+ {
189
+ path: `${root}/application/ports/${itemNames.kebabName}-repository.port.ts`,
190
+ type: "repository",
191
+ owner: featureNames.kebabName,
192
+ content: createRegularCrudRepositoryPortTs(itemNames),
193
+ },
194
+ {
195
+ path: `${root}/data/${itemNames.kebabName}.memory-repository.ts`,
196
+ type: "repository",
197
+ owner: featureNames.kebabName,
198
+ content: createRegularCrudMemoryRepositoryTs(itemNames),
199
+ },
200
+ {
201
+ path: `${root}/data/${itemNames.kebabName}.memory-repository.spec.ts`,
202
+ type: "repository",
203
+ owner: featureNames.kebabName,
204
+ content: createRegularCrudMemoryRepositorySpecTs(itemNames, plan.fields),
205
+ },
206
+ ...(plan.db === "mongo"
207
+ ? [
208
+ {
209
+ path: `${root}/data/${itemNames.kebabName}.repository.mongo.ts`,
210
+ type: "repository",
211
+ owner: featureNames.kebabName,
212
+ content: createRegularCrudMongoRepositoryTs(itemNames, plan.fields),
213
+ },
214
+ ]
215
+ : []),
216
+ ...(isSqlDatabase(plan.db)
217
+ ? [
218
+ {
219
+ path: `${root}/data/${itemNames.kebabName}.repository.sql.ts`,
220
+ type: "repository",
221
+ owner: featureNames.kebabName,
222
+ content: createRegularCrudSqlRepositoryTs(itemNames, plan.db, plan.fields),
223
+ },
224
+ ]
225
+ : []),
226
+ ...commands.flatMap((action) => createCqrsCrudCommandFiles(root, featureNames, itemNames, action)),
227
+ ...queries.flatMap((action) => createCqrsCrudQueryFiles(root, featureNames, itemNames, action)),
228
+ {
229
+ path: `${root}/application/commands/index.ts`,
230
+ type: "config",
231
+ owner: featureNames.kebabName,
232
+ content: createCqrsCrudCommandIndexTs(commands),
233
+ },
234
+ {
235
+ path: `${root}/application/queries/index.ts`,
236
+ type: "config",
237
+ owner: featureNames.kebabName,
238
+ content: createCqrsCrudQueryIndexTs(queries),
239
+ },
240
+ ...actions.map((action) => ({
241
+ path: `${root}/contracts/${action.name}.contract.ts`,
242
+ type: "route",
243
+ owner: featureNames.kebabName,
244
+ content: createRegularCrudContractTs(action, plan),
245
+ })),
246
+ ...actions.map((action) => ({
247
+ path: `${root}/api/${action.name}.controller.ts`,
248
+ type: "route",
249
+ owner: featureNames.kebabName,
250
+ content: createCqrsCrudControllerTs(action, resource, plan),
251
+ })),
252
+ {
253
+ path: `${root}/api/${featureNames.kebabName}.controllers.ts`,
254
+ type: "config",
255
+ owner: featureNames.kebabName,
256
+ content: createRegularCrudControllersIndexTs(featureNames, actions),
257
+ },
258
+ {
259
+ path: `${root}/setup.ts`,
260
+ type: "resource",
261
+ owner: featureNames.kebabName,
262
+ content: createCqrsCrudSetupTs(featureNames, itemNames, plan.db),
263
+ },
264
+ {
265
+ path: `${root}/index.ts`,
266
+ type: "resource",
267
+ owner: featureNames.kebabName,
268
+ content: createCqrsCrudFeatureIndexTs(featureNames, itemNames),
269
+ },
270
+ ];
271
+ }
272
+ function createRegularCrudResourceFiles(plan) {
273
+ const featureNames = (0, naming_1.createNameVariants)(plan.name);
274
+ const itemNames = (0, naming_1.createNameVariants)(singularizeResourceName(featureNames.kebabName));
275
+ const collectionNames = (0, naming_1.createNameVariants)(itemNames.pluralName);
276
+ const root = path_1.default.posix.join(plan.featuresRoot, featureNames.kebabName);
277
+ const actions = createRegularCrudActions(itemNames, collectionNames, plan.crudRoutes);
278
+ const files = [
279
+ {
280
+ path: `${root}/domain/${itemNames.kebabName}.entity.ts`,
281
+ type: "entity",
282
+ owner: featureNames.kebabName,
283
+ content: createEntityTs(itemNames.kebabName, plan.fields),
284
+ },
285
+ {
286
+ path: `${root}/domain/${itemNames.kebabName}.entity.spec.ts`,
287
+ type: "entity",
288
+ owner: featureNames.kebabName,
289
+ content: createEntitySpecTs(itemNames, plan.fields),
290
+ },
291
+ {
292
+ path: `${root}/application/ports/${itemNames.kebabName}-repository.port.ts`,
293
+ type: "repository",
294
+ owner: featureNames.kebabName,
295
+ content: createRegularCrudRepositoryPortTs(itemNames),
296
+ },
297
+ {
298
+ path: `${root}/data/${itemNames.kebabName}.memory-repository.ts`,
299
+ type: "repository",
300
+ owner: featureNames.kebabName,
301
+ content: createRegularCrudMemoryRepositoryTs(itemNames),
302
+ },
303
+ {
304
+ path: `${root}/data/${itemNames.kebabName}.memory-repository.spec.ts`,
305
+ type: "repository",
306
+ owner: featureNames.kebabName,
307
+ content: createRegularCrudMemoryRepositorySpecTs(itemNames, plan.fields),
308
+ },
309
+ ...(plan.db === "mongo"
310
+ ? [
311
+ {
312
+ path: `${root}/data/${itemNames.kebabName}.repository.mongo.ts`,
313
+ type: "repository",
314
+ owner: featureNames.kebabName,
315
+ content: createRegularCrudMongoRepositoryTs(itemNames, plan.fields),
316
+ },
317
+ ]
318
+ : []),
319
+ ...(isSqlDatabase(plan.db)
320
+ ? [
321
+ {
322
+ path: `${root}/data/${itemNames.kebabName}.repository.sql.ts`,
323
+ type: "repository",
324
+ owner: featureNames.kebabName,
325
+ content: createRegularCrudSqlRepositoryTs(itemNames, plan.db, plan.fields),
326
+ },
327
+ ]
328
+ : []),
329
+ ...actions.map((action) => ({
330
+ path: `${root}/application/use-cases/${action.name}.use-case.ts`,
331
+ type: "use-case",
332
+ owner: featureNames.kebabName,
333
+ content: createRegularCrudUseCaseTs(action, itemNames),
334
+ })),
335
+ ...actions.map((action) => ({
336
+ path: `${root}/application/use-cases/${action.name}.use-case.spec.ts`,
337
+ type: "use-case",
338
+ owner: featureNames.kebabName,
339
+ content: createRegularCrudUseCaseSpecTs(action, itemNames, plan.fields),
340
+ })),
341
+ ...actions.map((action) => ({
342
+ path: `${root}/contracts/${action.name}.contract.ts`,
343
+ type: "route",
344
+ owner: featureNames.kebabName,
345
+ content: createRegularCrudContractTs(action, plan),
346
+ })),
347
+ ...actions.map((action) => ({
348
+ path: `${root}/api/${action.name}.controller.ts`,
349
+ type: "route",
350
+ owner: featureNames.kebabName,
351
+ content: createRegularCrudControllerTs(action, itemNames, featureNames, plan),
352
+ })),
353
+ {
354
+ path: `${root}/api/${featureNames.kebabName}.controllers.ts`,
355
+ type: "config",
356
+ owner: featureNames.kebabName,
357
+ content: createRegularCrudControllersIndexTs(featureNames, actions),
358
+ },
359
+ {
360
+ path: `${root}/setup.ts`,
361
+ type: "resource",
362
+ owner: featureNames.kebabName,
363
+ content: createRegularCrudSetupTs(featureNames, itemNames, actions, plan.db),
364
+ },
365
+ {
366
+ path: `${root}/index.ts`,
367
+ type: "resource",
368
+ owner: featureNames.kebabName,
369
+ content: createRegularCrudFeatureIndexTs(featureNames, itemNames),
370
+ },
371
+ ];
372
+ return files;
373
+ }
374
+ function createCqrsCrudCommandFiles(root, featureNames, itemNames, action) {
375
+ return [
376
+ {
377
+ path: `${root}/application/commands/${action.name}.command.ts`,
378
+ type: "use-case",
379
+ owner: featureNames.kebabName,
380
+ content: createCqrsCrudCommandTs(action),
381
+ },
382
+ {
383
+ path: `${root}/application/commands/${action.name}.handler.ts`,
384
+ type: "use-case",
385
+ owner: featureNames.kebabName,
386
+ content: createCqrsCrudCommandHandlerTs(action, itemNames),
387
+ },
388
+ {
389
+ path: `${root}/application/commands/${action.name}.handler.spec.ts`,
390
+ type: "use-case",
391
+ owner: featureNames.kebabName,
392
+ content: createCqrsCrudCommandSpecTs(action),
393
+ },
394
+ ];
395
+ }
396
+ function createCqrsCrudQueryFiles(root, featureNames, itemNames, action) {
397
+ return [
398
+ {
399
+ path: `${root}/application/queries/${action.name}.query.ts`,
400
+ type: "use-case",
401
+ owner: featureNames.kebabName,
402
+ content: createCqrsCrudQueryTs(action),
403
+ },
404
+ {
405
+ path: `${root}/application/queries/${action.name}.handler.ts`,
406
+ type: "use-case",
407
+ owner: featureNames.kebabName,
408
+ content: createCqrsCrudQueryHandlerTs(action, itemNames),
409
+ },
410
+ {
411
+ path: `${root}/application/queries/${action.name}.handler.spec.ts`,
412
+ type: "use-case",
413
+ owner: featureNames.kebabName,
414
+ content: createCqrsCrudQuerySpecTs(action),
415
+ },
416
+ ];
417
+ }
418
+ function createCqrsCrudCommandTs(action) {
419
+ return `import { BaseCommand } from '@soapjs/soap/cqrs';
420
+
421
+ export interface ${action.classBase}Payload {
422
+ [key: string]: unknown;
423
+ }
424
+
425
+ export interface ${action.classBase}Result {
426
+ [key: string]: unknown;
427
+ }
428
+
429
+ export class ${action.classBase}Command extends BaseCommand<${action.classBase}Result> {
430
+ constructor(
431
+ public readonly payload: ${action.classBase}Payload = {},
432
+ initiatedBy?: string,
433
+ correlationId?: string
434
+ ) {
435
+ super(initiatedBy, correlationId);
436
+ }
437
+ }
438
+ `;
439
+ }
440
+ function createCqrsCrudQueryTs(action) {
441
+ return `import { BaseQuery } from '@soapjs/soap/cqrs';
442
+
443
+ export interface ${action.classBase}Criteria {
444
+ [key: string]: unknown;
445
+ }
446
+
447
+ export interface ${action.classBase}Result {
448
+ [key: string]: unknown;
449
+ }
450
+
451
+ export class ${action.classBase}Query extends BaseQuery<${action.classBase}Result> {
452
+ constructor(
453
+ public readonly criteria: ${action.classBase}Criteria = {},
454
+ initiatedBy?: string,
455
+ correlationId?: string
456
+ ) {
457
+ super(initiatedBy, correlationId);
458
+ }
459
+ }
460
+ `;
461
+ }
462
+ function createCqrsCrudCommandHandlerTs(action, itemNames) {
463
+ const repositoryCall = createCqrsCrudCommandRepositoryCall(action, itemNames);
464
+ return `import { Result } from '@soapjs/soap';
465
+ import { CommandHandler as SoapCommandHandler } from '@soapjs/soap/cqrs';
466
+ import { CommandHandler } from '@soapjs/soap-express/cqrs';
467
+ import { ${itemNames.pascalName}Repository } from '../ports/${itemNames.kebabName}-repository.port';
468
+ import { ${itemNames.pascalName}Props } from '../../domain/${itemNames.kebabName}.entity';
469
+ import { ${action.classBase}Command, ${action.classBase}Result } from './${action.name}.command';
470
+
471
+ @CommandHandler(${action.classBase}Command)
472
+ export class ${action.classBase}Handler implements SoapCommandHandler<${action.classBase}Command, ${action.classBase}Result> {
473
+ constructor(private readonly repository?: ${itemNames.pascalName}Repository) {}
474
+
475
+ async handle(command: ${action.classBase}Command): Promise<Result<${action.classBase}Result>> {
476
+ try {
477
+ ${repositoryCall}
478
+ } catch (error) {
479
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
480
+ }
481
+ }
482
+ }
483
+ `;
484
+ }
485
+ function createCqrsCrudQueryHandlerTs(action, itemNames) {
486
+ const repositoryCall = createCqrsCrudQueryRepositoryCall(action);
487
+ return `import { Result } from '@soapjs/soap';
488
+ import { QueryHandler as SoapQueryHandler } from '@soapjs/soap/cqrs';
489
+ import { QueryHandler } from '@soapjs/soap-express/cqrs';
490
+ import { ${itemNames.pascalName}Repository } from '../ports/${itemNames.kebabName}-repository.port';
491
+ import { ${action.classBase}Query, ${action.classBase}Result } from './${action.name}.query';
492
+
493
+ @QueryHandler(${action.classBase}Query)
494
+ export class ${action.classBase}Handler implements SoapQueryHandler<${action.classBase}Query, ${action.classBase}Result> {
495
+ constructor(private readonly repository?: ${itemNames.pascalName}Repository) {}
496
+
497
+ async handle(query: ${action.classBase}Query): Promise<Result<${action.classBase}Result>> {
498
+ try {
499
+ ${repositoryCall}
500
+ } catch (error) {
501
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
502
+ }
503
+ }
504
+ }
505
+ `;
506
+ }
507
+ function createCqrsCrudCommandRepositoryCall(action, itemNames) {
508
+ const kind = action.kind;
509
+ if (kind === "create") {
510
+ return ` if (!this.repository) return Result.withSuccess({ ...command.payload } as ${action.classBase}Result);
511
+ const item = await this.repository.create(command.payload as Omit<${itemNames.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>);
512
+ return Result.withSuccess(item.props as unknown as ${action.classBase}Result);`;
513
+ }
514
+ if (kind === "update") {
515
+ return ` if (!this.repository) return Result.withSuccess({ ...command.payload } as ${action.classBase}Result);
516
+ const { id, ...changes } = command.payload;
517
+ const item = await this.repository.update(String(id), changes);
518
+ return Result.withSuccess((item?.props ?? { id, ...changes }) as unknown as ${action.classBase}Result);`;
519
+ }
520
+ return ` if (!this.repository) return Result.withSuccess({ deleted: true, ...command.payload } as ${action.classBase}Result);
521
+ const deleted = await this.repository.delete(String(command.payload.id));
522
+ return Result.withSuccess({ deleted });`;
523
+ }
524
+ function createCqrsCrudQueryRepositoryCall(action) {
525
+ const kind = action.kind;
526
+ if (kind === "list") {
527
+ return ` if (!this.repository) return Result.withSuccess({ items: [] } as ${action.classBase}Result);
528
+ const items = await this.repository.list();
529
+ return Result.withSuccess({ items: items.map((item) => item.props) });`;
530
+ }
531
+ return ` if (!this.repository) return Result.withSuccess({ ...query.criteria } as ${action.classBase}Result);
532
+ const item = await this.repository.findById(String(query.criteria.id));
533
+ return Result.withSuccess((item?.props ?? { ...query.criteria }) as unknown as ${action.classBase}Result);`;
534
+ }
535
+ function createCqrsCrudCommandSpecTs(action) {
536
+ return `import assert from 'node:assert/strict';
537
+ import test from 'node:test';
538
+ import { Result } from '@soapjs/soap';
539
+ import { InMemoryCommandBus } from '@soapjs/soap/cqrs';
540
+ import { ${action.classBase}Command, ${action.classBase}Result } from './${action.name}.command';
541
+ import { ${action.classBase}Handler } from './${action.name}.handler';
542
+
543
+ test('${action.classBase}Command can be dispatched through a CQRS command bus', async () => {
544
+ const bus = new InMemoryCommandBus();
545
+ bus.register(${action.classBase}Command, new ${action.classBase}Handler());
546
+
547
+ const result: Result<${action.classBase}Result> = await bus.dispatch(new ${action.classBase}Command({ id: 'existing-id', name: 'Ada' }));
548
+
549
+ assert.equal(result.isSuccess(), true);
550
+ });
551
+ `;
552
+ }
553
+ function createCqrsCrudQuerySpecTs(action) {
554
+ return `import assert from 'node:assert/strict';
555
+ import test from 'node:test';
556
+ import { Result } from '@soapjs/soap';
557
+ import { InMemoryQueryBus } from '@soapjs/soap/cqrs';
558
+ import { ${action.classBase}Query, ${action.classBase}Result } from './${action.name}.query';
559
+ import { ${action.classBase}Handler } from './${action.name}.handler';
560
+
561
+ test('${action.classBase}Query can be dispatched through a CQRS query bus', async () => {
562
+ const bus = new InMemoryQueryBus();
563
+ bus.register(${action.classBase}Query, new ${action.classBase}Handler());
564
+
565
+ const result: Result<${action.classBase}Result> = await bus.dispatch(new ${action.classBase}Query({ id: 'existing-id' }));
566
+
567
+ assert.equal(result.isSuccess(), true);
568
+ });
569
+ `;
570
+ }
571
+ function createCqrsCrudCommandIndexTs(actions) {
572
+ return `${actions
573
+ .flatMap((action) => [`export * from './${action.name}.command';`, `export * from './${action.name}.handler';`])
574
+ .join("\n")}
575
+ `;
576
+ }
577
+ function createCqrsCrudQueryIndexTs(actions) {
578
+ return `${actions
579
+ .flatMap((action) => [`export * from './${action.name}.query';`, `export * from './${action.name}.handler';`])
580
+ .join("\n")}
581
+ `;
582
+ }
583
+ function createCqrsCrudControllerTs(action, resource, plan) {
584
+ const isCommand = action.kind === "create" || action.kind === "update" || action.kind === "delete";
585
+ const busType = isCommand ? "CommandBus" : "QueryBus";
586
+ const busProperty = isCommand ? "commandBus" : "queryBus";
587
+ const messageType = isCommand ? "Command" : "Query";
588
+ const folder = isCommand ? "commands" : "queries";
589
+ const routeAuth = action.auth ?? plan.auth;
590
+ const routeZone = action.zone ?? plan.zone;
591
+ const routePolicy = action.policy ?? plan.policy;
592
+ const authDecorator = createCqrsCrudAuthDecorator(routeAuth, routeZone, routePolicy);
593
+ const authLine = authDecorator ? ` ${authDecorator}\n` : "";
594
+ const imports = ["Controller", "Inject", action.method];
595
+ if (authDecorator?.startsWith("@Public"))
596
+ imports.push("Public");
597
+ if (authDecorator?.startsWith("@Auth"))
598
+ imports.push("Auth");
599
+ if (authDecorator?.startsWith("@AdminOnly"))
600
+ imports.push("AdminOnly");
601
+ return `import { Request } from 'express';
602
+ import { ${Array.from(new Set(imports)).sort().join(", ")} } from '@soapjs/soap-express';
603
+ import { ${busType} } from '@soapjs/soap/cqrs';
604
+ import { ${action.classBase}${messageType} } from '../application/${folder}/${action.name}.${messageType.toLowerCase()}';
605
+ import { ${action.operationId}BodyContract } from '../contracts/${action.name}.contract';
606
+
607
+ @Controller('${resource.path}', {
608
+ apiDoc: {
609
+ tags: ['${(0, naming_1.createNameVariants)(resource.name).pascalName}'],
610
+ description: '${action.classBase} route generated by SoapJS CLI',
611
+ },
612
+ })
613
+ export class ${action.classBase}Controller {
614
+ constructor(@Inject('${busType}') private readonly ${busProperty}: ${busType}) {}
615
+
616
+ ${authLine} @${action.method}('${action.path}', ${createRouteApiDocOptions({
617
+ summary: `${action.classBase} ${resource.name}`,
618
+ operationId: action.operationId,
619
+ auth: routeAuth,
620
+ })})
621
+ async ${action.operationId}(req: Request): Promise<unknown> {
622
+ return this.${busProperty}.dispatch(new ${action.classBase}${messageType}(${action.operationId}BodyContract(req)));
623
+ }
624
+ }
625
+ `;
626
+ }
627
+ function createCqrsCrudAuthDecorator(auth, zone, policy) {
628
+ if (zone === "public") {
629
+ return "@Public()";
630
+ }
631
+ return createAuthDecorator(auth, zone, policy);
632
+ }
633
+ function createCqrsCrudSetupTs(featureNames, itemNames, db) {
634
+ return createRegularCrudSetupTs(featureNames, itemNames, [], db);
635
+ }
636
+ function createCqrsCrudFeatureIndexTs(featureNames, itemNames) {
637
+ return `export * from './api/${featureNames.kebabName}.controllers';
638
+ export * from './domain/${itemNames.kebabName}.entity';
639
+ export * from './application/ports/${itemNames.kebabName}-repository.port';
640
+ export * from './application/commands';
641
+ export * from './application/queries';
642
+ export * from './data/${itemNames.kebabName}.memory-repository';
643
+ export * from './setup';
644
+ `;
645
+ }
646
+ function createResourcesFile(resources, featuresRoot) {
647
+ const registrations = resources.map((resource) => {
648
+ const names = (0, naming_1.createNameVariants)(resource.name);
649
+ return {
650
+ functionName: `register${names.pascalName}Dependencies`,
651
+ importPath: `../${featuresRoot.replace(/^src\//, "")}/${names.kebabName}/setup`,
652
+ };
653
+ });
654
+ return {
655
+ path: "src/config/resources.ts",
656
+ type: "config",
657
+ content: (0, project_plan_1.createResourcesTs)(registrations),
658
+ };
659
+ }
660
+ exports.createResourcesFile = createResourcesFile;
661
+ function createFeaturesIndexFile(resources, authCapabilities = []) {
662
+ const exports = [];
663
+ if (authCapabilities.includes("jwt") || authCapabilities.includes("local") || authCapabilities.includes("api-key")) {
664
+ exports.push("export * from './auth';");
665
+ }
666
+ exports.push(...resources
667
+ .map((resource) => (0, naming_1.createNameVariants)(resource.name).kebabName)
668
+ .sort()
669
+ .map((feature) => `export * from './${feature}';`));
670
+ return {
671
+ path: "src/features/index.ts",
672
+ type: "config",
673
+ content: exports.length > 0 ? `${exports.join("\n")}\n` : "export {};\n",
674
+ };
675
+ }
676
+ exports.createFeaturesIndexFile = createFeaturesIndexFile;
677
+ function createControllersFile(resources, featuresRoot, authCapabilities = [], routeControllerIndexes = [], mainControllerResources = resources.map((resource) => resource.name)) {
678
+ const controllers = [];
679
+ const routeControllerIndexSet = new Set(routeControllerIndexes);
680
+ const mainControllerResourceSet = new Set(mainControllerResources);
681
+ if (authCapabilities.includes("jwt") || authCapabilities.includes("local")) {
682
+ controllers.push({
683
+ className: "AuthController",
684
+ importPath: "../features/auth/api/auth.controller",
685
+ });
686
+ }
687
+ controllers.push(...resources
688
+ .filter((resource) => mainControllerResourceSet.has(resource.name))
689
+ .map((resource) => {
690
+ const names = (0, naming_1.createNameVariants)(resource.name);
691
+ return {
692
+ className: `${names.pascalName}Controller`,
693
+ importPath: `../${featuresRoot.replace(/^src\//, "")}/${names.kebabName}/api/${names.kebabName}.controller`,
694
+ };
695
+ }));
696
+ controllers.push(...resources
697
+ .filter((resource) => routeControllerIndexSet.has(resource.name))
698
+ .map((resource) => {
699
+ const names = (0, naming_1.createNameVariants)(resource.name);
700
+ return {
701
+ className: `${names.pascalName}Controllers`,
702
+ importPath: `../${featuresRoot.replace(/^src\//, "")}/${names.kebabName}/api/${names.kebabName}.controllers`,
703
+ spread: true,
704
+ };
705
+ }));
706
+ return {
707
+ path: "src/config/controllers.ts",
708
+ type: "config",
709
+ content: (0, project_plan_1.createControllersTs)(controllers),
710
+ };
711
+ }
712
+ exports.createControllersFile = createControllersFile;
713
+ function createRegularCrudActions(itemNames, collectionNames, matrix = {}) {
714
+ return [
715
+ {
716
+ name: `list-${collectionNames.kebabName}`,
717
+ classBase: `List${collectionNames.pascalName}`,
718
+ method: toControllerMethod(matrix.list?.method, "Get"),
719
+ path: matrix.list?.path ?? "/",
720
+ operationId: `list${collectionNames.pascalName}`,
721
+ kind: "list",
722
+ auth: matrix.list?.auth,
723
+ zone: matrix.list?.zone,
724
+ bruno: matrix.list?.bruno,
725
+ policy: matrix.list?.policy,
726
+ },
727
+ {
728
+ name: `get-${itemNames.kebabName}`,
729
+ classBase: `Get${itemNames.pascalName}`,
730
+ method: toControllerMethod(matrix.get?.method, "Get"),
731
+ path: matrix.get?.path ?? "/:id",
732
+ operationId: `get${itemNames.pascalName}`,
733
+ kind: "get",
734
+ auth: matrix.get?.auth,
735
+ zone: matrix.get?.zone,
736
+ bruno: matrix.get?.bruno,
737
+ policy: matrix.get?.policy,
738
+ },
739
+ {
740
+ name: `create-${itemNames.kebabName}`,
741
+ classBase: `Create${itemNames.pascalName}`,
742
+ method: toControllerMethod(matrix.create?.method, "Post"),
743
+ path: matrix.create?.path ?? "/",
744
+ operationId: `create${itemNames.pascalName}`,
745
+ kind: "create",
746
+ auth: matrix.create?.auth,
747
+ zone: matrix.create?.zone,
748
+ bruno: matrix.create?.bruno,
749
+ policy: matrix.create?.policy,
750
+ },
751
+ {
752
+ name: `update-${itemNames.kebabName}`,
753
+ classBase: `Update${itemNames.pascalName}`,
754
+ method: toControllerMethod(matrix.update?.method, "Put"),
755
+ path: matrix.update?.path ?? "/:id",
756
+ operationId: `update${itemNames.pascalName}`,
757
+ kind: "update",
758
+ auth: matrix.update?.auth,
759
+ zone: matrix.update?.zone,
760
+ bruno: matrix.update?.bruno,
761
+ policy: matrix.update?.policy,
762
+ },
763
+ {
764
+ name: `delete-${itemNames.kebabName}`,
765
+ classBase: `Delete${itemNames.pascalName}`,
766
+ method: toControllerMethod(matrix.delete?.method, "Delete"),
767
+ path: matrix.delete?.path ?? "/:id",
768
+ operationId: `delete${itemNames.pascalName}`,
769
+ kind: "delete",
770
+ auth: matrix.delete?.auth,
771
+ zone: matrix.delete?.zone,
772
+ bruno: matrix.delete?.bruno,
773
+ policy: matrix.delete?.policy,
774
+ },
775
+ ];
776
+ }
777
+ function parseCrudRouteMatrix(values) {
778
+ const matrix = {};
779
+ for (const value of values ?? []) {
780
+ const [operation, method, ...rest] = value.split(":");
781
+ const bruno = isCrudRouteBrunoToken(rest[rest.length - 1]) ? rest.pop() : undefined;
782
+ const policy = isCrudRoutePolicyToken(rest[rest.length - 1]) ? rest.pop() : undefined;
783
+ const zone = isCrudRouteZoneToken(rest[rest.length - 1]) ? rest.pop() : undefined;
784
+ const auth = isCrudRouteAuthToken(rest[rest.length - 1]) ? rest.pop() : undefined;
785
+ const routePath = rest.join(":");
786
+ if (!isCrudOperation(operation)) {
787
+ throw new errors_1.CliError(`Invalid CRUD operation "${operation}". Allowed values: list, get, create, update, delete.`);
788
+ }
789
+ if (!isCrudMatrixMethod(method)) {
790
+ throw new errors_1.CliError(`Invalid CRUD route method "${method}". Allowed values: get, post, put, patch, delete.`);
791
+ }
792
+ if (!routePath || !routePath.startsWith("/")) {
793
+ throw new errors_1.CliError(`Invalid CRUD route path "${routePath}". Use a path starting with "/".`);
794
+ }
795
+ matrix[operation] = {
796
+ method,
797
+ path: routePath,
798
+ auth: parseCrudRouteAuth(auth),
799
+ zone: parseCrudRouteZone(zone),
800
+ policy: parseCrudRoutePolicy(policy),
801
+ bruno: parseCrudRouteBruno(bruno),
802
+ };
803
+ }
804
+ return matrix;
805
+ }
806
+ exports.parseCrudRouteMatrix = parseCrudRouteMatrix;
807
+ function isCrudOperation(value) {
808
+ return value === "list" || value === "get" || value === "create" || value === "update" || value === "delete";
809
+ }
810
+ function isCrudMatrixMethod(value) {
811
+ return value === "get" || value === "post" || value === "put" || value === "patch" || value === "delete";
812
+ }
813
+ function parseCrudRouteAuth(value) {
814
+ if (!value)
815
+ return undefined;
816
+ if (isCrudRouteAuthToken(value))
817
+ return value;
818
+ throw new errors_1.CliError(`Invalid CRUD route auth "${value}". Allowed values: none, jwt, api-key, local.`);
819
+ }
820
+ function parseCrudRouteZone(value) {
821
+ if (!value)
822
+ return undefined;
823
+ if (isCrudRouteZoneToken(value))
824
+ return value;
825
+ throw new errors_1.CliError(`Invalid CRUD route zone "${value}". Allowed values: public, private, admin.`);
826
+ }
827
+ function parseCrudRouteBruno(value) {
828
+ if (!value)
829
+ return undefined;
830
+ if (value === "bruno")
831
+ return true;
832
+ if (value === "no-bruno")
833
+ return false;
834
+ throw new errors_1.CliError(`Invalid CRUD route Bruno option "${value}". Allowed values: bruno, no-bruno.`);
835
+ }
836
+ function isCrudRouteAuthToken(value) {
837
+ return value === "none" || value === "jwt" || value === "api-key" || value === "local";
838
+ }
839
+ function isCrudRouteZoneToken(value) {
840
+ return value === "public" || value === "private" || value === "admin";
841
+ }
842
+ function isCrudRouteBrunoToken(value) {
843
+ return value === "bruno" || value === "no-bruno";
844
+ }
845
+ function isCrudRoutePolicyToken(value) {
846
+ return value === "admin" || value === "none" || Boolean(value?.startsWith("roles=") || value?.startsWith("custom="));
847
+ }
848
+ function parseCrudRoutePolicy(value) {
849
+ if (!value) {
850
+ return undefined;
851
+ }
852
+ if (value.startsWith("roles=")) {
853
+ return (0, auth_policy_1.parseAuthPolicy)(`roles:${value.slice("roles=".length)}`);
854
+ }
855
+ if (value.startsWith("custom=")) {
856
+ return (0, auth_policy_1.parseAuthPolicy)(`custom:${value.slice("custom=".length)}`);
857
+ }
858
+ return (0, auth_policy_1.parseAuthPolicy)(value);
859
+ }
860
+ function resolveCrudSummaryPath(resourcePath, routePath) {
861
+ if (routePath === resourcePath || routePath.startsWith(`${resourcePath}/`)) {
862
+ return routePath;
863
+ }
864
+ if (routePath === "/") {
865
+ return resourcePath;
866
+ }
867
+ return `${resourcePath}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
868
+ }
869
+ function toControllerMethod(method, fallback) {
870
+ if (!method)
871
+ return fallback;
872
+ const normalized = method[0].toUpperCase() + method.slice(1);
873
+ return normalized;
874
+ }
875
+ function isSqlDatabase(value) {
876
+ return value === "postgres" || value === "mysql" || value === "sqlite";
877
+ }
878
+ function formatDatabaseName(value) {
879
+ if (value === "postgres")
880
+ return "PostgreSQL";
881
+ if (value === "mysql")
882
+ return "MySQL";
883
+ if (value === "sqlite")
884
+ return "SQLite";
885
+ if (value === "mongo")
886
+ return "Mongo";
887
+ return value;
888
+ }
889
+ function createRegularCrudRepositoryPortTs(names) {
890
+ return `import { ${names.pascalName}, ${names.pascalName}Props } from '../../domain/${names.kebabName}.entity';
891
+
892
+ export const ${names.constantName}_REPOSITORY = '${names.pascalName}Repository';
893
+
894
+ export interface ${names.pascalName}Repository {
895
+ list(): Promise<${names.pascalName}[]>;
896
+ findById(id: string): Promise<${names.pascalName} | undefined>;
897
+ create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}>;
898
+ update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined>;
899
+ delete(id: string): Promise<boolean>;
900
+ }
901
+ `;
902
+ }
903
+ function createRegularCrudMemoryRepositoryTs(names) {
904
+ return `import { randomUUID } from 'crypto';
905
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
906
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}-repository.port';
907
+
908
+ export class InMemory${names.pascalName}Repository implements ${names.pascalName}Repository {
909
+ private readonly items = new Map<string, ${names.pascalName}>();
910
+
911
+ async list(): Promise<${names.pascalName}[]> {
912
+ return Array.from(this.items.values());
913
+ }
914
+
915
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
916
+ return this.items.get(id);
917
+ }
918
+
919
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
920
+ const now = new Date().toISOString();
921
+ const item = new ${names.pascalName}({
922
+ ...input,
923
+ id: randomUUID(),
924
+ createdAt: now,
925
+ updatedAt: now,
926
+ });
927
+ this.items.set(item.id, item);
928
+ return item;
929
+ }
930
+
931
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
932
+ const current = this.items.get(id);
933
+ if (!current) return undefined;
934
+
935
+ const updated = new ${names.pascalName}({
936
+ ...current.props,
937
+ ...input,
938
+ id,
939
+ updatedAt: new Date().toISOString(),
940
+ });
941
+ this.items.set(id, updated);
942
+ return updated;
943
+ }
944
+
945
+ async delete(id: string): Promise<boolean> {
946
+ return this.items.delete(id);
947
+ }
948
+ }
949
+ `;
950
+ }
951
+ function createRegularCrudMemoryRepositorySpecTs(names, fields = []) {
952
+ const input = createSampleInputObject(fields, "create", " ");
953
+ const updateInput = createSampleInputObject(fields, "update", " ");
954
+ const assertionField = normalizeResourceFields(fields)[0].name;
955
+ const expectedUpdatedValue = createSampleFieldValue(normalizeResourceFields(fields)[0], "update");
956
+ return `import assert from 'node:assert/strict';
957
+ import test from 'node:test';
958
+ import { ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
959
+ import { InMemory${names.pascalName}Repository } from './${names.kebabName}.memory-repository';
960
+
961
+ test('InMemory${names.pascalName}Repository supports CRUD operations', async () => {
962
+ const repository = new InMemory${names.pascalName}Repository();
963
+ const input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'> = {
964
+ ${input}
965
+ };
966
+
967
+ const created = await repository.create(input);
968
+ assert.equal(Boolean(created.id), true);
969
+
970
+ const listed = await repository.list();
971
+ assert.equal(listed.length, 1);
972
+
973
+ const found = await repository.findById(created.id);
974
+ assert.equal(found?.id, created.id);
975
+
976
+ const updated = await repository.update(created.id, {
977
+ ${updateInput}
978
+ });
979
+ assert.equal(updated?.props.${assertionField}, ${expectedUpdatedValue});
980
+
981
+ const deleted = await repository.delete(created.id);
982
+ assert.equal(deleted, true);
983
+ assert.equal(await repository.findById(created.id), undefined);
984
+ });
985
+ `;
986
+ }
987
+ function createRegularCrudMongoRepositoryTs(names, fields = []) {
988
+ const documentFields = createInterfaceFields(fields);
989
+ return `import { randomUUID } from 'crypto';
990
+ import { MongoSource } from '@soapjs/soap-node-mongo';
991
+ import { Document } from 'mongodb';
992
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
993
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}-repository.port';
994
+
995
+ export interface ${names.pascalName}Document extends Document {
996
+ id: string;
997
+ ${documentFields}
998
+ createdAt: string;
999
+ updatedAt: string;
1000
+ }
1001
+
1002
+ export class Mongo${names.pascalName}Repository implements ${names.pascalName}Repository {
1003
+ constructor(private readonly source: MongoSource<${names.pascalName}Document>) {}
1004
+
1005
+ async list(): Promise<${names.pascalName}[]> {
1006
+ const documents = await this.source.find({});
1007
+ return documents.map((document) => new ${names.pascalName}(document));
1008
+ }
1009
+
1010
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
1011
+ const [document] = await this.source.find({ where: { id }, limit: 1 });
1012
+ return document ? new ${names.pascalName}(document) : undefined;
1013
+ }
1014
+
1015
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
1016
+ const now = new Date().toISOString();
1017
+ const document: ${names.pascalName}Document = {
1018
+ ...input,
1019
+ id: randomUUID(),
1020
+ createdAt: now,
1021
+ updatedAt: now,
1022
+ };
1023
+ const [created] = await this.source.insert(document);
1024
+ return new ${names.pascalName}(created ?? document);
1025
+ }
1026
+
1027
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
1028
+ const current = await this.findById(id);
1029
+ if (!current) return undefined;
1030
+
1031
+ const next = {
1032
+ ...current.props,
1033
+ ...input,
1034
+ id,
1035
+ updatedAt: new Date().toISOString(),
1036
+ };
1037
+ await this.source.update({ where: { id }, update: next });
1038
+ return new ${names.pascalName}(next);
1039
+ }
1040
+
1041
+ async delete(id: string): Promise<boolean> {
1042
+ const result = await this.source.remove({ where: { id } });
1043
+ return (result.deletedCount ?? 0) > 0;
1044
+ }
1045
+ }
1046
+ `;
1047
+ }
1048
+ function createRegularCrudSqlRepositoryTs(names, db, fields = []) {
1049
+ const sql = createSqlFieldMetadata(fields, db);
1050
+ return `import { randomUUID } from 'crypto';
1051
+ import { SqlDataSource } from '@soapjs/soap-node-sql';
1052
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
1053
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}-repository.port';
1054
+
1055
+ export interface ${names.pascalName}Row {
1056
+ id: string;
1057
+ ${sql.interfaceFields}
1058
+ createdAt: string;
1059
+ updatedAt: string;
1060
+ }
1061
+
1062
+ export async function ensure${names.pascalName}Schema(source: SqlDataSource<${names.pascalName}Row>): Promise<void> {
1063
+ await source.query(\`
1064
+ CREATE TABLE IF NOT EXISTS ${names.snakeName} (
1065
+ id TEXT PRIMARY KEY,
1066
+ ${sql.columnDefinitions}
1067
+ ${sql.createdAtColumn} TEXT NOT NULL,
1068
+ ${quoteSqlIdentifier("updatedAt", db)} TEXT NOT NULL
1069
+ )
1070
+ \`);
1071
+ }
1072
+
1073
+ export class Sql${names.pascalName}Repository implements ${names.pascalName}Repository {
1074
+ constructor(private readonly source: SqlDataSource<${names.pascalName}Row>) {}
1075
+
1076
+ async list(): Promise<${names.pascalName}[]> {
1077
+ const result = await this.source.query('SELECT ${sql.selectColumns} FROM ${names.snakeName} ORDER BY ${sql.createdAtColumn} DESC');
1078
+ return result.data.map((row) => new ${names.pascalName}(row as ${names.pascalName}Row));
1079
+ }
1080
+
1081
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
1082
+ const result = await this.source.query('SELECT ${sql.selectColumns} FROM ${names.snakeName} WHERE id = ${sql.firstPlaceholder} LIMIT 1', [id]);
1083
+ const row = result.data[0] as ${names.pascalName}Row | undefined;
1084
+ return row ? new ${names.pascalName}(row) : undefined;
1085
+ }
1086
+
1087
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
1088
+ const now = new Date().toISOString();
1089
+ const row: ${names.pascalName}Row = {
1090
+ ...input,
1091
+ id: randomUUID(),
1092
+ createdAt: now,
1093
+ updatedAt: now,
1094
+ };
1095
+ const result = await this.source.query(
1096
+ 'INSERT INTO ${names.snakeName} (${sql.insertColumns}) VALUES (${sql.insertPlaceholders})',
1097
+ [${sql.insertValues}]
1098
+ );
1099
+ return new ${names.pascalName}(row);
1100
+ }
1101
+
1102
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
1103
+ const current = await this.findById(id);
1104
+ if (!current) return undefined;
1105
+
1106
+ const next = {
1107
+ ...current.props,
1108
+ ...input,
1109
+ id,
1110
+ updatedAt: new Date().toISOString(),
1111
+ };
1112
+ const result = await this.source.query(
1113
+ 'UPDATE ${names.snakeName} SET ${sql.updateAssignments} WHERE id = ${sql.idPlaceholder}',
1114
+ [${sql.updateValues}]
1115
+ );
1116
+ if (result.count === 0) return undefined;
1117
+ return this.findById(id);
1118
+ }
1119
+
1120
+ async delete(id: string): Promise<boolean> {
1121
+ const result = await this.source.query('DELETE FROM ${names.snakeName} WHERE id = ${sql.firstPlaceholder}', [id]);
1122
+ return result.count > 0;
1123
+ }
1124
+ }
1125
+ `;
1126
+ }
1127
+ function createRegularCrudUseCaseTs(action, itemNames) {
1128
+ const inputInterface = `${action.classBase}Input`;
1129
+ const outputInterface = `${action.classBase}Output`;
1130
+ const inputShape = createRegularCrudUseCaseInputShape(action);
1131
+ const outputShape = action.kind === "list"
1132
+ ? `export interface ${outputInterface} {
1133
+ items: ${itemNames.pascalName}Props[];
1134
+ }
1135
+ `
1136
+ : action.kind === "delete"
1137
+ ? `export interface ${outputInterface} {
1138
+ deleted: boolean;
1139
+ }
1140
+ `
1141
+ : `export type ${outputInterface} = ${itemNames.pascalName}Props | undefined;
1142
+ `;
1143
+ const executeInput = action.kind === "list" ? "" : `input: ${inputInterface}`;
1144
+ const repositoryCall = createRegularCrudRepositoryCall(action.kind, itemNames);
1145
+ return `import { Injectable, Result, UseCase } from '@soapjs/soap';
1146
+ import { ${itemNames.pascalName}Props } from '../../domain/${itemNames.kebabName}.entity';
1147
+ import { ${itemNames.pascalName}Repository } from '../ports/${itemNames.kebabName}-repository.port';
1148
+
1149
+ ${inputShape}
1150
+ ${outputShape}
1151
+ @Injectable()
1152
+ export class ${action.classBase}UseCase implements UseCase<${outputInterface}> {
1153
+ constructor(private readonly repository: ${itemNames.pascalName}Repository) {}
1154
+
1155
+ async execute(${executeInput}): Promise<Result<${outputInterface}>> {
1156
+ try {
1157
+ ${repositoryCall}
1158
+ } catch (error) {
1159
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
1160
+ }
1161
+ }
1162
+ }
1163
+ `;
1164
+ }
1165
+ function createRegularCrudUseCaseSpecTs(action, itemNames, fields = []) {
1166
+ const props = createSamplePropsObject(fields, "create", " ");
1167
+ const createInput = createSampleInputObject(fields, "create", " ");
1168
+ const updateInput = createSampleInputObject(fields, "update", " ");
1169
+ const execute = createRegularCrudUseCaseSpecExecute(action, createInput, updateInput);
1170
+ return `import assert from 'node:assert/strict';
1171
+ import test from 'node:test';
1172
+ import { ${itemNames.pascalName}, ${itemNames.pascalName}Props } from '../../domain/${itemNames.kebabName}.entity';
1173
+ import { ${itemNames.pascalName}Repository } from '../ports/${itemNames.kebabName}-repository.port';
1174
+ import { ${action.classBase}UseCase } from './${action.name}.use-case';
1175
+
1176
+ test('${action.classBase}UseCase returns a successful Result', async () => {
1177
+ const props: ${itemNames.pascalName}Props = {
1178
+ ${props}
1179
+ };
1180
+ const repository: ${itemNames.pascalName}Repository = {
1181
+ async list() {
1182
+ return [new ${itemNames.pascalName}(props)];
1183
+ },
1184
+ async findById(id) {
1185
+ return id === props.id ? new ${itemNames.pascalName}(props) : undefined;
1186
+ },
1187
+ async create(input) {
1188
+ return new ${itemNames.pascalName}({
1189
+ ...props,
1190
+ ...input,
1191
+ id: 'created-id',
1192
+ });
1193
+ },
1194
+ async update(id, input) {
1195
+ return id === props.id
1196
+ ? new ${itemNames.pascalName}({
1197
+ ...props,
1198
+ ...input,
1199
+ id,
1200
+ updatedAt: '2026-01-02T00:00:00.000Z',
1201
+ })
1202
+ : undefined;
1203
+ },
1204
+ async delete(id) {
1205
+ return id === props.id;
1206
+ },
1207
+ };
1208
+ const useCase = new ${action.classBase}UseCase(repository);
1209
+
1210
+ ${execute}
1211
+ });
1212
+ `;
1213
+ }
1214
+ function createRegularCrudUseCaseSpecExecute(action, createInput, updateInput) {
1215
+ if (action.kind === "list") {
1216
+ return ` const result = await useCase.execute();
1217
+
1218
+ assert.equal(result.isSuccess(), true);
1219
+ assert.deepEqual(result.content, { items: [props] });`;
1220
+ }
1221
+ if (action.kind === "get") {
1222
+ return ` const result = await useCase.execute({ id: props.id });
1223
+
1224
+ assert.equal(result.isSuccess(), true);
1225
+ assert.deepEqual(result.content, props);`;
1226
+ }
1227
+ if (action.kind === "create") {
1228
+ return ` const result = await useCase.execute({
1229
+ ${createInput}
1230
+ });
1231
+
1232
+ assert.equal(result.isSuccess(), true);
1233
+ assert.equal(result.content?.id, 'created-id');`;
1234
+ }
1235
+ if (action.kind === "update") {
1236
+ return ` const result = await useCase.execute({
1237
+ id: props.id,
1238
+ ${updateInput}
1239
+ });
1240
+
1241
+ assert.equal(result.isSuccess(), true);
1242
+ assert.equal(result.content?.id, props.id);`;
1243
+ }
1244
+ return ` const result = await useCase.execute({ id: props.id });
1245
+
1246
+ assert.equal(result.isSuccess(), true);
1247
+ assert.deepEqual(result.content, { deleted: true });`;
1248
+ }
1249
+ function createRegularCrudUseCaseInputShape(action) {
1250
+ const kind = action.kind;
1251
+ if (kind === "list") {
1252
+ return "";
1253
+ }
1254
+ if (kind === "create") {
1255
+ return `export interface ${action.classBase}Input {
1256
+ [key: string]: unknown;
1257
+ }
1258
+ `;
1259
+ }
1260
+ if (kind === "update") {
1261
+ return `export interface ${action.classBase}Input {
1262
+ id: string;
1263
+ [key: string]: unknown;
1264
+ }
1265
+ `;
1266
+ }
1267
+ return `export interface ${action.classBase}Input {
1268
+ id: string;
1269
+ }
1270
+ `;
1271
+ }
1272
+ function createRegularCrudRepositoryCall(kind, itemNames) {
1273
+ if (kind === "list") {
1274
+ return ` const items = await this.repository.list();
1275
+ return Result.withSuccess({ items: items.map((item) => item.props) });`;
1276
+ }
1277
+ if (kind === "get") {
1278
+ return ` const item = await this.repository.findById(input.id);
1279
+ return Result.withSuccess(item?.props);`;
1280
+ }
1281
+ if (kind === "create") {
1282
+ return ` const item = await this.repository.create(input as Omit<${itemNames.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>);
1283
+ return Result.withSuccess(item.props);`;
1284
+ }
1285
+ if (kind === "update") {
1286
+ return ` const { id, ...changes } = input;
1287
+ const item = await this.repository.update(id, changes);
1288
+ return Result.withSuccess(item?.props);`;
1289
+ }
1290
+ return ` const deleted = await this.repository.delete(input.id);
1291
+ return Result.withSuccess({ deleted });`;
1292
+ }
1293
+ function createRegularCrudContractTs(action, plan) {
1294
+ const source = action.kind === "list"
1295
+ ? "{ ...req.query }"
1296
+ : action.kind === "create"
1297
+ ? "{ ...req.body }"
1298
+ : action.kind === "update"
1299
+ ? "{ ...req.params, ...req.body }"
1300
+ : "{ ...req.params, ...req.query }";
1301
+ if (plan.contracts === "zod") {
1302
+ const schemaFields = createZodSchemaFields(plan.fields, action.kind);
1303
+ return `import { Request } from 'express';
1304
+ import { z } from 'zod';
1305
+
1306
+ export const ${action.operationId}BodySchema = z.object({
1307
+ ${schemaFields}
1308
+ }).passthrough();
1309
+
1310
+ export type ${action.classBase}RouteInput = z.infer<typeof ${action.operationId}BodySchema>;
1311
+
1312
+ export function ${action.operationId}BodyContract(req: Request): ${action.classBase}RouteInput {
1313
+ return ${action.operationId}BodySchema.parse(${source});
1314
+ }
1315
+ `;
1316
+ }
1317
+ return `import { Request } from 'express';
1318
+
1319
+ export interface ${action.classBase}RouteInput {
1320
+ [key: string]: unknown;
1321
+ }
1322
+
1323
+ export function ${action.operationId}BodyContract(req: Request): ${action.classBase}RouteInput {
1324
+ return ${source};
1325
+ }
1326
+ `;
1327
+ }
1328
+ function createRegularCrudControllerTs(action, itemNames, featureNames, plan) {
1329
+ const routeAuth = action.auth ?? plan.auth;
1330
+ const routeZone = action.zone ?? plan.zone;
1331
+ const routePolicy = action.policy ?? plan.policy;
1332
+ const authDecorator = createAuthDecorator(routeAuth, routeZone, routePolicy);
1333
+ const authLine = authDecorator ? ` ${authDecorator}\n` : "";
1334
+ const decoratorImports = ["CallUseCase", "Controller", action.method, "RouteIO"];
1335
+ if (authDecorator?.startsWith("@Auth"))
1336
+ decoratorImports.push("Auth");
1337
+ if (authDecorator?.startsWith("@AdminOnly"))
1338
+ decoratorImports.push("AdminOnly");
1339
+ return `import { ${Array.from(new Set(decoratorImports)).sort().join(", ")} } from '@soapjs/soap-express';
1340
+ import { ${action.classBase}UseCase } from '../application/use-cases/${action.name}.use-case';
1341
+ import { ${action.operationId}BodyContract } from '../contracts/${action.name}.contract';
1342
+
1343
+ @Controller('/${featureNames.kebabName}', {
1344
+ apiDoc: {
1345
+ tags: ['${featureNames.pascalName}'],
1346
+ description: '${action.classBase} route generated by SoapJS CLI',
1347
+ },
1348
+ })
1349
+ export class ${action.classBase}Controller {
1350
+ ${authLine} @CallUseCase(${action.classBase}UseCase)
1351
+ @RouteIO({ from: ${action.operationId}BodyContract })
1352
+ @${action.method}('${action.path}', ${createRouteApiDocOptions({
1353
+ summary: `${action.classBase} ${featureNames.kebabName}`,
1354
+ operationId: action.operationId,
1355
+ auth: routeAuth,
1356
+ })})
1357
+ async ${action.operationId}(): Promise<void> {}
1358
+ }
1359
+ `;
1360
+ }
1361
+ function createRegularCrudControllersIndexTs(featureNames, actions) {
1362
+ const imports = actions
1363
+ .map((action) => `import { ${action.classBase}Controller } from './${action.name}.controller';`)
1364
+ .join("\n");
1365
+ const controllers = actions.map((action) => ` ${action.classBase}Controller,`).join("\n");
1366
+ return `${imports}
1367
+
1368
+ export const ${featureNames.pascalName}Controllers = [
1369
+ ${controllers}
1370
+ ];
1371
+ `;
1372
+ }
1373
+ function createRegularCrudSetupTs(featureNames, itemNames, actions, db) {
1374
+ const repositoryImport = db === "mongo"
1375
+ ? `import { createMongoSource } from '../../common/data/mongo/mongo.source-factory';
1376
+ import { ResourceContext } from '../../config/dependencies';
1377
+ import { Mongo${itemNames.pascalName}Repository, ${itemNames.pascalName}Document } from './data/${itemNames.kebabName}.repository.mongo';`
1378
+ : isSqlDatabase(db)
1379
+ ? `import { createSqlSource } from '../../common/data/sql/sql.source-factory';
1380
+ import { ResourceContext } from '../../config/dependencies';
1381
+ import { ensure${itemNames.pascalName}Schema, Sql${itemNames.pascalName}Repository, ${itemNames.pascalName}Row } from './data/${itemNames.kebabName}.repository.sql';`
1382
+ : `import { ResourceContext } from '../../config/dependencies';
1383
+ import { InMemory${itemNames.pascalName}Repository } from './data/${itemNames.kebabName}.memory-repository';`;
1384
+ const repositoryFactory = db === "mongo"
1385
+ ? ` if (!resources.mongo) {
1386
+ throw new Error('Mongo is not configured for ${featureNames.kebabName}. Enable --db mongo for the project.');
1387
+ }
1388
+
1389
+ const source = createMongoSource<${itemNames.pascalName}Document>(resources.mongo, '${featureNames.kebabName}');
1390
+ const repository = new Mongo${itemNames.pascalName}Repository(source);`
1391
+ : isSqlDatabase(db)
1392
+ ? ` if (!resources.sql) {
1393
+ throw new Error('${formatDatabaseName(db)} is not configured for ${featureNames.kebabName}. Enable --db ${db} for the project.');
1394
+ }
1395
+
1396
+ const sql = resources.sql.${db};
1397
+ if (!sql) {
1398
+ throw new Error('${formatDatabaseName(db)} is not configured for ${featureNames.kebabName}. Enable --db ${db} for the project.');
1399
+ }
1400
+
1401
+ const source = createSqlSource<${itemNames.pascalName}Row>(sql, '${featureNames.snakeName}');
1402
+ await ensure${itemNames.pascalName}Schema(source);
1403
+ const repository = new Sql${itemNames.pascalName}Repository(source);`
1404
+ : ` const repository = new InMemory${itemNames.pascalName}Repository();`;
1405
+ const useCaseImports = actions
1406
+ .map((action) => `import { ${action.classBase}UseCase } from './application/use-cases/${action.name}.use-case';`)
1407
+ .join("\n");
1408
+ const useCaseBindings = actions
1409
+ .map((action) => ` container.bindFactory(${action.classBase}UseCase.name, () => new ${action.classBase}UseCase(repository));`)
1410
+ .join("\n");
1411
+ return `import { DIContainer } from '@soapjs/soap-express';
1412
+ ${repositoryImport}
1413
+ import { ${itemNames.constantName}_REPOSITORY } from './application/ports/${itemNames.kebabName}-repository.port';
1414
+ ${useCaseImports}
1415
+
1416
+ export async function register${featureNames.pascalName}Dependencies(container: DIContainer, resources: ResourceContext): Promise<void> {
1417
+ ${repositoryFactory}
1418
+
1419
+ container.bindValue(${itemNames.constantName}_REPOSITORY, repository);
1420
+ ${useCaseBindings}
1421
+ }
1422
+ `;
1423
+ }
1424
+ function createRegularCrudFeatureIndexTs(featureNames, itemNames) {
1425
+ return `export * from './api/${featureNames.kebabName}.controllers';
1426
+ export * from './domain/${itemNames.kebabName}.entity';
1427
+ export * from './application/ports/${itemNames.kebabName}-repository.port';
1428
+ export * from './data/${itemNames.kebabName}.memory-repository';
1429
+ export * from './setup';
1430
+ `;
1431
+ }
1432
+ function createEntityTs(name, fields = []) {
1433
+ const names = (0, naming_1.createNameVariants)(name);
1434
+ const props = normalizeResourceFields(fields)
1435
+ .map((field) => ` ${field.name}${field.required ? "" : "?"}: ${toTypescriptType(field.type)};`)
1436
+ .join("\n");
1437
+ return `export interface ${names.pascalName}Props {
1438
+ id: string;
1439
+ ${props}
1440
+ createdAt: string;
1441
+ updatedAt: string;
1442
+ }
1443
+
1444
+ export class ${names.pascalName} {
1445
+ constructor(public readonly props: ${names.pascalName}Props) {}
1446
+
1447
+ get id(): string {
1448
+ return this.props.id;
1449
+ }
1450
+ }
1451
+ `;
1452
+ }
1453
+ function createEntitySpecTs(names, fields = []) {
1454
+ const props = createSamplePropsObject(fields, "create", " ");
1455
+ return `import assert from 'node:assert/strict';
1456
+ import test from 'node:test';
1457
+ import { ${names.pascalName}, ${names.pascalName}Props } from './${names.kebabName}.entity';
1458
+
1459
+ test('${names.pascalName} exposes immutable props and id', () => {
1460
+ const props: ${names.pascalName}Props = {
1461
+ ${props}
1462
+ };
1463
+ const entity = new ${names.pascalName}(props);
1464
+
1465
+ assert.equal(entity.id, props.id);
1466
+ assert.deepEqual(entity.props, props);
1467
+ });
1468
+ `;
1469
+ }
1470
+ function parseResourceFieldDefinitions(values) {
1471
+ return normalizeResourceFields((values ?? []).map(parseResourceFieldDefinition));
1472
+ }
1473
+ exports.parseResourceFieldDefinitions = parseResourceFieldDefinitions;
1474
+ function parseResourceFieldDefinition(value) {
1475
+ const [name, type, requiredToken] = value.split(":");
1476
+ if (!name || !/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
1477
+ throw new errors_1.CliError(`Invalid field definition "${value}". Use name:type or name:type:optional.`);
1478
+ }
1479
+ if (!isResourceFieldType(type)) {
1480
+ throw new errors_1.CliError(`Invalid field type "${type}" for "${name}". Allowed values: string, number, boolean, date.`);
1481
+ }
1482
+ if (requiredToken && requiredToken !== "required" && requiredToken !== "optional") {
1483
+ throw new errors_1.CliError(`Invalid field modifier "${requiredToken}" for "${name}". Use required or optional.`);
1484
+ }
1485
+ return {
1486
+ name,
1487
+ type,
1488
+ required: requiredToken !== "optional",
1489
+ };
1490
+ }
1491
+ function normalizeResourceFields(fields) {
1492
+ const normalized = fields && fields.length > 0
1493
+ ? fields
1494
+ : [{ name: "name", type: "string", required: true }];
1495
+ const byName = new Map();
1496
+ for (const field of normalized) {
1497
+ byName.set(field.name, field);
1498
+ }
1499
+ return Array.from(byName.values());
1500
+ }
1501
+ function isResourceFieldType(value) {
1502
+ return value === "string" || value === "number" || value === "boolean" || value === "date";
1503
+ }
1504
+ function toTypescriptType(type) {
1505
+ if (type === "number")
1506
+ return "number";
1507
+ if (type === "boolean")
1508
+ return "boolean";
1509
+ return "string";
1510
+ }
1511
+ function createInterfaceFields(fields) {
1512
+ return normalizeResourceFields(fields)
1513
+ .map((field) => ` ${field.name}${field.required ? "" : "?"}: ${toTypescriptType(field.type)};`)
1514
+ .join("\n");
1515
+ }
1516
+ function createSamplePropsObject(fields, variant, indent) {
1517
+ return [
1518
+ `${indent}id: 'entity-id',`,
1519
+ createSampleInputObject(fields, variant, indent),
1520
+ `${indent}createdAt: '2026-01-01T00:00:00.000Z',`,
1521
+ `${indent}updatedAt: '2026-01-01T00:00:00.000Z',`,
1522
+ ].join("\n");
1523
+ }
1524
+ function createSampleInputObject(fields, variant, indent) {
1525
+ return normalizeResourceFields(fields)
1526
+ .map((field) => `${indent}${field.name}: ${createSampleFieldValue(field, variant)},`)
1527
+ .join("\n");
1528
+ }
1529
+ function createSampleFieldValue(field, variant) {
1530
+ if (field.type === "number") {
1531
+ return variant === "create" ? "100" : "200";
1532
+ }
1533
+ if (field.type === "boolean") {
1534
+ return variant === "create" ? "true" : "false";
1535
+ }
1536
+ const prefix = variant === "create" ? "Sample" : "Updated";
1537
+ return JSON.stringify(`${prefix} ${field.name}`);
1538
+ }
1539
+ function createSqlFieldMetadata(fields, db) {
1540
+ const normalized = normalizeResourceFields(fields);
1541
+ const quote = (value) => quoteSqlIdentifier(value, db);
1542
+ const placeholder = (index) => createSqlPlaceholder(index, db);
1543
+ const selectColumns = ["id", ...normalized.map((field) => quote(field.name)), quote("createdAt"), quote("updatedAt")];
1544
+ const insertColumns = ["id", ...normalized.map((field) => quote(field.name)), quote("createdAt"), quote("updatedAt")];
1545
+ const insertValues = ["row.id", ...normalized.map((field) => `row.${field.name}`), "row.createdAt", "row.updatedAt"];
1546
+ const updateAssignments = [
1547
+ ...normalized.map((field, index) => `${quote(field.name)} = ${placeholder(index + 2)}`),
1548
+ `${quote("updatedAt")} = ${placeholder(normalized.length + 2)}`,
1549
+ ];
1550
+ const updateValues = ["id", ...normalized.map((field) => `next.${field.name}`), "next.updatedAt"];
1551
+ return {
1552
+ interfaceFields: createInterfaceFields(normalized),
1553
+ columnDefinitions: normalized
1554
+ .map((field) => ` ${quote(field.name)} ${toSqlColumnType(field.type, db)}${field.required ? " NOT NULL" : ""},`)
1555
+ .join("\n"),
1556
+ selectColumns: selectColumns.join(", "),
1557
+ insertColumns: insertColumns.join(", "),
1558
+ insertPlaceholders: insertColumns.map((_, index) => placeholder(index + 1)).join(", "),
1559
+ insertValues: insertValues.join(", "),
1560
+ updateAssignments: updateAssignments.join(", "),
1561
+ updateValues: updateValues.join(", "),
1562
+ firstPlaceholder: placeholder(1),
1563
+ idPlaceholder: placeholder(1),
1564
+ createdAtColumn: quote("createdAt"),
1565
+ };
1566
+ }
1567
+ function quoteSqlIdentifier(value, db) {
1568
+ if (/^[a-z_][a-z0-9_]*$/.test(value)) {
1569
+ return value;
1570
+ }
1571
+ return db === "mysql" ? `\`${value}\`` : `"${value}"`;
1572
+ }
1573
+ function createSqlPlaceholder(index, db) {
1574
+ return db === "postgres" ? `$${index}` : "?";
1575
+ }
1576
+ function toSqlColumnType(type, db) {
1577
+ if (type === "number")
1578
+ return db === "postgres" ? "DOUBLE PRECISION" : "REAL";
1579
+ if (type === "boolean")
1580
+ return "BOOLEAN";
1581
+ return "TEXT";
1582
+ }
1583
+ function createZodSchemaFields(fields, kind) {
1584
+ const resourceFields = kind === "create" || kind === "update"
1585
+ ? normalizeResourceFields(fields).map((field) => ({
1586
+ ...field,
1587
+ required: kind === "create" ? field.required : false,
1588
+ }))
1589
+ : [];
1590
+ const idFields = kind === "get" || kind === "update" || kind === "delete"
1591
+ ? [{ name: "id", type: "string", required: true }]
1592
+ : [];
1593
+ const allFields = [...idFields, ...resourceFields];
1594
+ return allFields
1595
+ .map((field) => ` ${field.name}: ${toZodExpression(field)},`)
1596
+ .join("\n");
1597
+ }
1598
+ function toZodExpression(field) {
1599
+ const base = field.type === "number"
1600
+ ? "z.coerce.number()"
1601
+ : field.type === "boolean"
1602
+ ? "z.coerce.boolean()"
1603
+ : "z.string()";
1604
+ return field.required ? base : `${base}.optional()`;
1605
+ }
1606
+ function createRepositoryPortTs(name) {
1607
+ const names = (0, naming_1.createNameVariants)(name);
1608
+ return `import { ${names.pascalName}, ${names.pascalName}Props } from '../../domain/${names.kebabName}.entity';
1609
+
1610
+ export interface ${names.pascalName}Repository {
1611
+ list(): Promise<${names.pascalName}[]>;
1612
+ findById(id: string): Promise<${names.pascalName} | undefined>;
1613
+ create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}>;
1614
+ update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined>;
1615
+ delete(id: string): Promise<boolean>;
1616
+ }
1617
+ `;
1618
+ }
1619
+ function createMemoryRepositoryTs(name) {
1620
+ const names = (0, naming_1.createNameVariants)(name);
1621
+ return `import { randomUUID } from 'crypto';
1622
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
1623
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}.repository';
1624
+
1625
+ export class InMemory${names.pascalName}Repository implements ${names.pascalName}Repository {
1626
+ private readonly items = new Map<string, ${names.pascalName}>();
1627
+
1628
+ async list(): Promise<${names.pascalName}[]> {
1629
+ return Array.from(this.items.values());
1630
+ }
1631
+
1632
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
1633
+ return this.items.get(id);
1634
+ }
1635
+
1636
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
1637
+ const now = new Date().toISOString();
1638
+ const item = new ${names.pascalName}({
1639
+ ...input,
1640
+ id: randomUUID(),
1641
+ createdAt: now,
1642
+ updatedAt: now,
1643
+ });
1644
+ this.items.set(item.id, item);
1645
+ return item;
1646
+ }
1647
+
1648
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
1649
+ const current = this.items.get(id);
1650
+ if (!current) return undefined;
1651
+
1652
+ const updated = new ${names.pascalName}({
1653
+ ...current.props,
1654
+ ...input,
1655
+ id,
1656
+ updatedAt: new Date().toISOString(),
1657
+ });
1658
+ this.items.set(id, updated);
1659
+ return updated;
1660
+ }
1661
+
1662
+ async delete(id: string): Promise<boolean> {
1663
+ return this.items.delete(id);
1664
+ }
1665
+ }
1666
+ `;
1667
+ }
1668
+ function createUseCasesTs(name) {
1669
+ const names = (0, naming_1.createNameVariants)(name);
1670
+ return `import { ${names.pascalName}Repository } from '../ports/${names.kebabName}.repository';
1671
+
1672
+ export class List${names.pascalName}UseCase {
1673
+ constructor(private readonly repository: ${names.pascalName}Repository) {}
1674
+
1675
+ execute() {
1676
+ return this.repository.list();
1677
+ }
1678
+ }
1679
+
1680
+ export class Get${names.pascalName}UseCase {
1681
+ constructor(private readonly repository: ${names.pascalName}Repository) {}
1682
+
1683
+ execute(id: string) {
1684
+ return this.repository.findById(id);
1685
+ }
1686
+ }
1687
+
1688
+ export class Create${names.pascalName}UseCase {
1689
+ constructor(private readonly repository: ${names.pascalName}Repository) {}
1690
+
1691
+ execute(input: { name: string }) {
1692
+ return this.repository.create(input);
1693
+ }
1694
+ }
1695
+
1696
+ export class Update${names.pascalName}UseCase {
1697
+ constructor(private readonly repository: ${names.pascalName}Repository) {}
1698
+
1699
+ execute(id: string, input: { name?: string }) {
1700
+ return this.repository.update(id, input);
1701
+ }
1702
+ }
1703
+
1704
+ export class Delete${names.pascalName}UseCase {
1705
+ constructor(private readonly repository: ${names.pascalName}Repository) {}
1706
+
1707
+ execute(id: string) {
1708
+ return this.repository.delete(id);
1709
+ }
1710
+ }
1711
+ `;
1712
+ }
1713
+ function createMongoRepositoryTs(name, fields = []) {
1714
+ const names = (0, naming_1.createNameVariants)(name);
1715
+ const documentFields = createInterfaceFields(fields);
1716
+ return `import { randomUUID } from 'crypto';
1717
+ import { MongoSource } from '@soapjs/soap-node-mongo';
1718
+ import { Document } from 'mongodb';
1719
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
1720
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}.repository';
1721
+
1722
+ export interface ${names.pascalName}Document extends Document {
1723
+ id: string;
1724
+ ${documentFields}
1725
+ createdAt: string;
1726
+ updatedAt: string;
1727
+ }
1728
+
1729
+ export class Mongo${names.pascalName}Repository implements ${names.pascalName}Repository {
1730
+ constructor(private readonly source: MongoSource<${names.pascalName}Document>) {}
1731
+
1732
+ async list(): Promise<${names.pascalName}[]> {
1733
+ const documents = await this.source.find({});
1734
+ return documents.map((document) => new ${names.pascalName}(document));
1735
+ }
1736
+
1737
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
1738
+ const [document] = await this.source.find({ where: { id }, limit: 1 });
1739
+ return document ? new ${names.pascalName}(document) : undefined;
1740
+ }
1741
+
1742
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
1743
+ const now = new Date().toISOString();
1744
+ const document: ${names.pascalName}Document = {
1745
+ ...input,
1746
+ id: randomUUID(),
1747
+ createdAt: now,
1748
+ updatedAt: now,
1749
+ };
1750
+ const [created] = await this.source.insert(document);
1751
+ return new ${names.pascalName}(created ?? document);
1752
+ }
1753
+
1754
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
1755
+ const current = await this.findById(id);
1756
+ if (!current) return undefined;
1757
+
1758
+ const next = {
1759
+ ...current.props,
1760
+ ...input,
1761
+ id,
1762
+ updatedAt: new Date().toISOString(),
1763
+ };
1764
+ await this.source.update({ where: { id }, update: next });
1765
+ return new ${names.pascalName}(next);
1766
+ }
1767
+
1768
+ async delete(id: string): Promise<boolean> {
1769
+ const result = await this.source.remove({ where: { id } });
1770
+ return (result.deletedCount ?? 0) > 0;
1771
+ }
1772
+ }
1773
+ `;
1774
+ }
1775
+ function createSqlRepositoryTs(name, db, fields = []) {
1776
+ const names = (0, naming_1.createNameVariants)(name);
1777
+ const sql = createSqlFieldMetadata(fields, db);
1778
+ return `import { randomUUID } from 'crypto';
1779
+ import { SqlDataSource } from '@soapjs/soap-node-sql';
1780
+ import { ${names.pascalName}, ${names.pascalName}Props } from '../domain/${names.kebabName}.entity';
1781
+ import { ${names.pascalName}Repository } from '../application/ports/${names.kebabName}.repository';
1782
+
1783
+ export interface ${names.pascalName}Row {
1784
+ id: string;
1785
+ ${sql.interfaceFields}
1786
+ createdAt: string;
1787
+ updatedAt: string;
1788
+ }
1789
+
1790
+ export async function ensure${names.pascalName}Schema(source: SqlDataSource<${names.pascalName}Row>): Promise<void> {
1791
+ await source.query(\`
1792
+ CREATE TABLE IF NOT EXISTS ${names.snakeName} (
1793
+ id TEXT PRIMARY KEY,
1794
+ ${sql.columnDefinitions}
1795
+ ${sql.createdAtColumn} TEXT NOT NULL,
1796
+ ${quoteSqlIdentifier("updatedAt", db)} TEXT NOT NULL
1797
+ )
1798
+ \`);
1799
+ }
1800
+
1801
+ export class Sql${names.pascalName}Repository implements ${names.pascalName}Repository {
1802
+ constructor(private readonly source: SqlDataSource<${names.pascalName}Row>) {}
1803
+
1804
+ async list(): Promise<${names.pascalName}[]> {
1805
+ const result = await this.source.query('SELECT ${sql.selectColumns} FROM ${names.snakeName} ORDER BY ${sql.createdAtColumn} DESC');
1806
+ return result.data.map((row) => new ${names.pascalName}(row as ${names.pascalName}Row));
1807
+ }
1808
+
1809
+ async findById(id: string): Promise<${names.pascalName} | undefined> {
1810
+ const result = await this.source.query('SELECT ${sql.selectColumns} FROM ${names.snakeName} WHERE id = ${sql.firstPlaceholder} LIMIT 1', [id]);
1811
+ const row = result.data[0] as ${names.pascalName}Row | undefined;
1812
+ return row ? new ${names.pascalName}(row) : undefined;
1813
+ }
1814
+
1815
+ async create(input: Omit<${names.pascalName}Props, 'id' | 'createdAt' | 'updatedAt'>): Promise<${names.pascalName}> {
1816
+ const now = new Date().toISOString();
1817
+ const row: ${names.pascalName}Row = {
1818
+ ...input,
1819
+ id: randomUUID(),
1820
+ createdAt: now,
1821
+ updatedAt: now,
1822
+ };
1823
+ const result = await this.source.query(
1824
+ 'INSERT INTO ${names.snakeName} (${sql.insertColumns}) VALUES (${sql.insertPlaceholders})',
1825
+ [${sql.insertValues}]
1826
+ );
1827
+ return new ${names.pascalName}(row);
1828
+ }
1829
+
1830
+ async update(id: string, input: Partial<${names.pascalName}Props>): Promise<${names.pascalName} | undefined> {
1831
+ const current = await this.findById(id);
1832
+ if (!current) return undefined;
1833
+
1834
+ const next = {
1835
+ ...current.props,
1836
+ ...input,
1837
+ id,
1838
+ updatedAt: new Date().toISOString(),
1839
+ };
1840
+ const result = await this.source.query(
1841
+ 'UPDATE ${names.snakeName} SET ${sql.updateAssignments} WHERE id = ${sql.idPlaceholder}',
1842
+ [${sql.updateValues}]
1843
+ );
1844
+ if (result.count === 0) return undefined;
1845
+ return this.findById(id);
1846
+ }
1847
+
1848
+ async delete(id: string): Promise<boolean> {
1849
+ const result = await this.source.query('DELETE FROM ${names.snakeName} WHERE id = ${sql.firstPlaceholder}', [id]);
1850
+ return result.count > 0;
1851
+ }
1852
+ }
1853
+ `;
1854
+ }
1855
+ function createSetupTs(name, db) {
1856
+ const names = (0, naming_1.createNameVariants)(name);
1857
+ const repositoryImport = db === "mongo"
1858
+ ? `import { createMongoSource } from '../../common/data/mongo/mongo.source-factory';
1859
+ import { ResourceContext } from '../../config/dependencies';
1860
+ import { Mongo${names.pascalName}Repository, ${names.pascalName}Document } from './data/${names.kebabName}.mongo-repository';`
1861
+ : isSqlDatabase(db)
1862
+ ? `import { createSqlSource } from '../../common/data/sql/sql.source-factory';
1863
+ import { ResourceContext } from '../../config/dependencies';
1864
+ import { ensure${names.pascalName}Schema, Sql${names.pascalName}Repository, ${names.pascalName}Row } from './data/${names.kebabName}.sql-repository';`
1865
+ : `import { ResourceContext } from '../../config/dependencies';
1866
+ import { InMemory${names.pascalName}Repository } from './data/${names.kebabName}.memory-repository';`;
1867
+ const repositoryFactory = db === "mongo"
1868
+ ? ` if (!resources.mongo) {
1869
+ throw new Error('Mongo is not configured for ${names.kebabName}. Enable --db mongo for the project.');
1870
+ }
1871
+
1872
+ const source = createMongoSource<${names.pascalName}Document>(resources.mongo, '${names.kebabName}');
1873
+ const repository = new Mongo${names.pascalName}Repository(source);`
1874
+ : isSqlDatabase(db)
1875
+ ? ` if (!resources.sql) {
1876
+ throw new Error('${formatDatabaseName(db)} is not configured for ${names.kebabName}. Enable --db ${db} for the project.');
1877
+ }
1878
+
1879
+ const sql = resources.sql.${db};
1880
+ if (!sql) {
1881
+ throw new Error('${formatDatabaseName(db)} is not configured for ${names.kebabName}. Enable --db ${db} for the project.');
1882
+ }
1883
+
1884
+ const source = createSqlSource<${names.pascalName}Row>(sql, '${names.snakeName}');
1885
+ await ensure${names.pascalName}Schema(source);
1886
+ const repository = new Sql${names.pascalName}Repository(source);`
1887
+ : ` const repository = new InMemory${names.pascalName}Repository();`;
1888
+ return `import { DIContainer } from '@soapjs/soap-express';
1889
+ ${repositoryImport}
1890
+ import {
1891
+ Create${names.pascalName}UseCase,
1892
+ Delete${names.pascalName}UseCase,
1893
+ Get${names.pascalName}UseCase,
1894
+ List${names.pascalName}UseCase,
1895
+ Update${names.pascalName}UseCase,
1896
+ } from './application/use-cases/${names.kebabName}.use-cases';
1897
+
1898
+ export const ${names.constantName}_REPOSITORY = '${names.pascalName}Repository';
1899
+
1900
+ export async function register${names.pascalName}Dependencies(container: DIContainer, resources: ResourceContext): Promise<void> {
1901
+ ${repositoryFactory}
1902
+
1903
+ container.bindValue(${names.constantName}_REPOSITORY, repository);
1904
+ container.bindFactory(List${names.pascalName}UseCase.name, () => new List${names.pascalName}UseCase(repository));
1905
+ container.bindFactory(Get${names.pascalName}UseCase.name, () => new Get${names.pascalName}UseCase(repository));
1906
+ container.bindFactory(Create${names.pascalName}UseCase.name, () => new Create${names.pascalName}UseCase(repository));
1907
+ container.bindFactory(Update${names.pascalName}UseCase.name, () => new Update${names.pascalName}UseCase(repository));
1908
+ container.bindFactory(Delete${names.pascalName}UseCase.name, () => new Delete${names.pascalName}UseCase(repository));
1909
+ }
1910
+ `;
1911
+ }
1912
+ function createControllerTs(plan) {
1913
+ const names = (0, naming_1.createNameVariants)(plan.name);
1914
+ const routeBase = `/${names.pluralName}`;
1915
+ const authDecorator = createAuthDecorator(plan.auth, plan.zone, plan.policy);
1916
+ const authLine = authDecorator ? `\n ${authDecorator}` : "";
1917
+ const crudMethods = plan.crud
1918
+ ? `
1919
+ ${authLine}
1920
+ @Post('/', ${createRouteApiDocOptions({
1921
+ summary: `Create ${names.pascalName}`,
1922
+ operationId: `create${names.pascalName}`,
1923
+ auth: plan.auth,
1924
+ })})
1925
+ async create(req: Request): Promise<unknown> {
1926
+ return this.create${names.pascalName}.execute(req.body);
1927
+ }
1928
+
1929
+ ${authLine}
1930
+ @Put('/:id', ${createRouteApiDocOptions({
1931
+ summary: `Update ${names.pascalName}`,
1932
+ operationId: `update${names.pascalName}`,
1933
+ auth: plan.auth,
1934
+ })})
1935
+ async update(req: Request): Promise<unknown> {
1936
+ return this.update${names.pascalName}.execute(req.params.id, req.body);
1937
+ }
1938
+
1939
+ ${authLine}
1940
+ @Delete('/:id', ${createRouteApiDocOptions({
1941
+ summary: `Delete ${names.pascalName}`,
1942
+ operationId: `delete${names.pascalName}`,
1943
+ auth: plan.auth,
1944
+ })})
1945
+ async delete(req: Request): Promise<unknown> {
1946
+ return this.delete${names.pascalName}.execute(req.params.id);
1947
+ }
1948
+ `
1949
+ : "";
1950
+ return `import { Request } from 'express';
1951
+ import { AdminOnly, Auth, Controller, Delete, Get, Post, Put } from '@soapjs/soap-express';
1952
+ import {
1953
+ Create${names.pascalName}UseCase,
1954
+ Delete${names.pascalName}UseCase,
1955
+ Get${names.pascalName}UseCase,
1956
+ List${names.pascalName}UseCase,
1957
+ Update${names.pascalName}UseCase,
1958
+ } from '../application/use-cases/${names.kebabName}.use-cases';
1959
+
1960
+ @Controller('${routeBase}', {
1961
+ apiDoc: {
1962
+ tags: ['${names.pascalName}'],
1963
+ description: '${names.pascalName} resource generated by SoapJS CLI',
1964
+ },
1965
+ })
1966
+ export class ${names.pascalName}Controller {
1967
+ constructor(
1968
+ private readonly list${names.pascalName}: List${names.pascalName}UseCase,
1969
+ private readonly get${names.pascalName}: Get${names.pascalName}UseCase,
1970
+ private readonly create${names.pascalName}: Create${names.pascalName}UseCase,
1971
+ private readonly update${names.pascalName}: Update${names.pascalName}UseCase,
1972
+ private readonly delete${names.pascalName}: Delete${names.pascalName}UseCase,
1973
+ ) {}${authLine}
1974
+ @Get('/', ${createRouteApiDocOptions({
1975
+ summary: `List ${names.pascalName}`,
1976
+ operationId: `list${names.pascalName}`,
1977
+ auth: plan.auth,
1978
+ })})
1979
+ async list(): Promise<unknown> {
1980
+ return this.list${names.pascalName}.execute();
1981
+ }
1982
+
1983
+ ${authLine}
1984
+ @Get('/:id', ${createRouteApiDocOptions({
1985
+ summary: `Get ${names.pascalName}`,
1986
+ operationId: `get${names.pascalName}`,
1987
+ auth: plan.auth,
1988
+ })})
1989
+ async get(req: Request): Promise<unknown> {
1990
+ return this.get${names.pascalName}.execute(req.params.id);
1991
+ }
1992
+ ${crudMethods}}
1993
+ `;
1994
+ }
1995
+ function createAuthDecorator(auth, zone, policy) {
1996
+ if (auth === "none") {
1997
+ return undefined;
1998
+ }
1999
+ const strategy = auth === "local" ? "jwt" : auth;
2000
+ if (zone === "admin" || policy?.type === "admin") {
2001
+ return `@AdminOnly('${strategy}')`;
2002
+ }
2003
+ const policyArgument = (0, auth_policy_1.createAuthPolicyArgument)(policy);
2004
+ return policyArgument ? `@Auth('${strategy}', ${policyArgument})` : `@Auth('${strategy}')`;
2005
+ }
2006
+ exports.createAuthDecorator = createAuthDecorator;
2007
+ function createRouteApiDocOptions(input) {
2008
+ const security = input.auth === "none"
2009
+ ? ""
2010
+ : `,\n security: [{ name: '${input.auth === "local" ? "jwt" : input.auth}' }]`;
2011
+ return `{
2012
+ apiDoc: {
2013
+ summary: '${input.summary}',
2014
+ operationId: '${input.operationId}',
2015
+ responses: {
2016
+ '200': { description: 'Success' },
2017
+ }${security},
2018
+ },
2019
+ }`;
2020
+ }
2021
+ exports.createRouteApiDocOptions = createRouteApiDocOptions;
2022
+ function createFeatureIndexTs(name) {
2023
+ const names = (0, naming_1.createNameVariants)(name);
2024
+ return `export * from './api/${names.kebabName}.controller';
2025
+ export * from './domain/${names.kebabName}.entity';
2026
+ export * from './application/ports/${names.kebabName}.repository';
2027
+ export * from './application/use-cases/${names.kebabName}.use-cases';
2028
+ export * from './data/${names.kebabName}.memory-repository';
2029
+ export * from './setup';
2030
+ `;
2031
+ }