@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,24 @@
1
+ import { ApiZone, AuthCapability, AuthPolicy, ResourceRegistryEntry, RouteRegistryEntry } from "../../config/schemas/types";
2
+ import { PlannedFile } from "../../io/file-writer";
3
+ export type RouteMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options";
4
+ export interface AddRoutePlan {
5
+ resource: ResourceRegistryEntry;
6
+ name: string;
7
+ method: RouteMethod;
8
+ path?: string;
9
+ useCase?: string;
10
+ command?: string;
11
+ query?: string;
12
+ auth: "none" | AuthCapability;
13
+ zone: ApiZone;
14
+ policy?: AuthPolicy;
15
+ featuresRoot: string;
16
+ contracts?: "plain" | "zod";
17
+ }
18
+ export declare const routeMethods: RouteMethod[];
19
+ export declare function createRouteEntry(plan: AddRoutePlan): RouteRegistryEntry;
20
+ export declare function createRouteControllerFile(plan: AddRoutePlan): PlannedFile;
21
+ export declare function createRouteControllersIndexFile(resource: ResourceRegistryEntry, featuresRoot: string, routeNames: string[]): PlannedFile;
22
+ export declare function createRouteContractFile(plan: AddRoutePlan): PlannedFile;
23
+ export declare function routeControllerNameFromPath(filePath: string): string | undefined;
24
+ export declare function routeControllerIndexResourceFromPath(filePath: string, featuresRoot: string): string | undefined;
@@ -0,0 +1,256 @@
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.routeControllerIndexResourceFromPath = exports.routeControllerNameFromPath = exports.createRouteContractFile = exports.createRouteControllersIndexFile = exports.createRouteControllerFile = exports.createRouteEntry = exports.routeMethods = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const errors_1 = require("../../core/errors");
9
+ const naming_1 = require("../../templates/naming");
10
+ const resource_plan_1 = require("./resource-plan");
11
+ const auth_policy_1 = require("../../config/auth-policy");
12
+ const decoratorByMethod = {
13
+ get: "Get",
14
+ post: "Post",
15
+ put: "Put",
16
+ patch: "Patch",
17
+ delete: "Delete",
18
+ head: "Head",
19
+ options: "Options",
20
+ };
21
+ exports.routeMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
22
+ function createRouteEntry(plan) {
23
+ const names = (0, naming_1.createNameVariants)(plan.name);
24
+ const absolutePath = resolveRoutePath(plan.resource.path, names.kebabName, plan.path);
25
+ return {
26
+ resource: plan.resource.name,
27
+ name: names.kebabName,
28
+ method: plan.method.toUpperCase(),
29
+ path: absolutePath,
30
+ auth: plan.auth,
31
+ zone: plan.zone,
32
+ policy: plan.policy,
33
+ generatedAt: new Date().toISOString(),
34
+ };
35
+ }
36
+ exports.createRouteEntry = createRouteEntry;
37
+ function createRouteControllerFile(plan) {
38
+ const resourceNames = (0, naming_1.createNameVariants)(plan.resource.name);
39
+ const routeNames = (0, naming_1.createNameVariants)(plan.name);
40
+ const controllerPath = path_1.default.posix.join(plan.featuresRoot, resourceNames.kebabName, "api", `${routeNames.kebabName}.controller.ts`);
41
+ return {
42
+ path: controllerPath,
43
+ type: "route",
44
+ owner: resourceNames.kebabName,
45
+ content: createRouteControllerTs(plan),
46
+ };
47
+ }
48
+ exports.createRouteControllerFile = createRouteControllerFile;
49
+ function createRouteControllersIndexFile(resource, featuresRoot, routeNames) {
50
+ const resourceNames = (0, naming_1.createNameVariants)(resource.name);
51
+ const uniqueRouteNames = Array.from(new Set(routeNames)).sort();
52
+ const imports = uniqueRouteNames
53
+ .map((routeName) => {
54
+ const names = (0, naming_1.createNameVariants)(routeName);
55
+ return `import { ${names.pascalName}Controller } from './${names.kebabName}.controller';`;
56
+ })
57
+ .join("\n");
58
+ const controllers = uniqueRouteNames
59
+ .map((routeName) => ` ${(0, naming_1.createNameVariants)(routeName).pascalName}Controller,`)
60
+ .join("\n");
61
+ return {
62
+ path: path_1.default.posix.join(planFeatureRoot(featuresRoot, resourceNames.kebabName), "api", `${resourceNames.kebabName}.controllers.ts`),
63
+ type: "config",
64
+ owner: resourceNames.kebabName,
65
+ content: `${imports}
66
+
67
+ export const ${resourceNames.pascalName}Controllers = [
68
+ ${controllers}
69
+ ];
70
+ `,
71
+ };
72
+ }
73
+ exports.createRouteControllersIndexFile = createRouteControllersIndexFile;
74
+ function createRouteContractFile(plan) {
75
+ const resourceNames = (0, naming_1.createNameVariants)(plan.resource.name);
76
+ const routeNames = (0, naming_1.createNameVariants)(plan.name);
77
+ const contractPath = path_1.default.posix.join(plan.featuresRoot, resourceNames.kebabName, "contracts", `${routeNames.kebabName}.contract.ts`);
78
+ return {
79
+ path: contractPath,
80
+ type: "route",
81
+ owner: resourceNames.kebabName,
82
+ content: createRouteContractTs(routeNames.pascalName, routeNames.camelName, plan.method, plan.contracts),
83
+ };
84
+ }
85
+ exports.createRouteContractFile = createRouteContractFile;
86
+ function routeControllerNameFromPath(filePath) {
87
+ const match = filePath.match(/\/api\/([^/]+)\.controller\.ts$/);
88
+ const name = match?.[1];
89
+ if (!name || name.endsWith(".controllers")) {
90
+ return undefined;
91
+ }
92
+ return name;
93
+ }
94
+ exports.routeControllerNameFromPath = routeControllerNameFromPath;
95
+ function routeControllerIndexResourceFromPath(filePath, featuresRoot) {
96
+ const escapedRoot = featuresRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
97
+ const match = filePath.match(new RegExp(`^${escapedRoot}/([^/]+)/api/\\1\\.controllers\\.ts$`));
98
+ return match?.[1];
99
+ }
100
+ exports.routeControllerIndexResourceFromPath = routeControllerIndexResourceFromPath;
101
+ function createRouteControllerTs(plan) {
102
+ const resourceNames = (0, naming_1.createNameVariants)(plan.resource.name);
103
+ const routeNames = (0, naming_1.createNameVariants)(plan.name);
104
+ const useCaseNames = plan.useCase ? (0, naming_1.createNameVariants)(plan.useCase) : undefined;
105
+ const commandNames = plan.command ? (0, naming_1.createNameVariants)(plan.command) : undefined;
106
+ const queryNames = plan.query ? (0, naming_1.createNameVariants)(plan.query) : undefined;
107
+ const cqrsTarget = commandNames
108
+ ? {
109
+ kind: "command",
110
+ names: commandNames,
111
+ className: `${commandNames.pascalName}Command`,
112
+ busType: "CommandBus",
113
+ busProperty: "commandBus",
114
+ importPath: `../application/commands/${commandNames.kebabName}.command`,
115
+ }
116
+ : queryNames
117
+ ? {
118
+ kind: "query",
119
+ names: queryNames,
120
+ className: `${queryNames.pascalName}Query`,
121
+ busType: "QueryBus",
122
+ busProperty: "queryBus",
123
+ importPath: `../application/queries/${queryNames.kebabName}.query`,
124
+ }
125
+ : undefined;
126
+ const decorator = decoratorByMethod[plan.method];
127
+ const routePath = toControllerRoutePath(plan.resource.path, resolveRoutePath(plan.resource.path, routeNames.kebabName, plan.path));
128
+ const soapImports = createSoapExpressImports(plan, decorator);
129
+ const expressImport = cqrsTarget ? "import { Request } from 'express';\n" : "";
130
+ const cqrsImport = cqrsTarget ? `import { ${cqrsTarget.busType} } from '@soapjs/soap/cqrs';\n` : "";
131
+ const cqrsTargetImport = cqrsTarget ? `import { ${cqrsTarget.className} } from '${cqrsTarget.importPath}';\n` : "";
132
+ const useCaseImport = useCaseNames
133
+ ? `import { ${useCaseNames.pascalName}UseCase } from '../application/use-cases/${useCaseNames.kebabName}.use-case';\n`
134
+ : "";
135
+ const contractImport = useCaseNames || cqrsTarget
136
+ ? `import { ${routeNames.camelName}BodyContract } from '../contracts/${routeNames.kebabName}.contract';\n`
137
+ : "";
138
+ const authDecorator = createRouteAuthDecorator(plan.auth, plan.zone, plan.policy);
139
+ const authLine = authDecorator ? ` ${authDecorator}\n` : "";
140
+ const useCaseDecorators = useCaseNames
141
+ ? ` @CallUseCase(${useCaseNames.pascalName}UseCase)\n @RouteIO({ from: ${routeNames.camelName}BodyContract })\n`
142
+ : "";
143
+ const constructor = cqrsTarget
144
+ ? ` constructor(@Inject('${cqrsTarget.busType}') private readonly ${cqrsTarget.busProperty}: ${cqrsTarget.busType}) {}\n\n`
145
+ : "";
146
+ const methodArgs = cqrsTarget ? "req: Request" : "";
147
+ const body = cqrsTarget
148
+ ? `\n return this.${cqrsTarget.busProperty}.dispatch(new ${cqrsTarget.className}(${routeNames.camelName}BodyContract(req)));\n `
149
+ : useCaseNames
150
+ ? ""
151
+ : `\n return { resource: '${resourceNames.kebabName}', action: '${routeNames.kebabName}' };\n `;
152
+ const returnType = useCaseNames ? "void" : "unknown";
153
+ return `${expressImport}${soapImports}
154
+ ${cqrsImport}${cqrsTargetImport}${useCaseImport}${contractImport}
155
+ @Controller('${plan.resource.path}', {
156
+ apiDoc: {
157
+ tags: ['${resourceNames.pascalName}'],
158
+ description: '${routeNames.pascalName} route generated by SoapJS CLI',
159
+ },
160
+ })
161
+ export class ${routeNames.pascalName}Controller {
162
+ ${constructor}${authLine}${useCaseDecorators} @${decorator}('${routePath}', ${(0, resource_plan_1.createRouteApiDocOptions)({
163
+ summary: `${routeNames.camelName} ${resourceNames.kebabName}`,
164
+ operationId: routeNames.camelName,
165
+ auth: plan.auth,
166
+ })})
167
+ async ${routeNames.camelName}(${methodArgs}): Promise<${returnType}> {${body}}
168
+ }
169
+ `;
170
+ }
171
+ function createSoapExpressImports(plan, routeDecorator) {
172
+ const imports = new Set(["Controller", routeDecorator]);
173
+ if (plan.useCase) {
174
+ imports.add("CallUseCase");
175
+ imports.add("RouteIO");
176
+ }
177
+ if (plan.command || plan.query) {
178
+ imports.add("Inject");
179
+ }
180
+ const authDecorator = createRouteAuthDecorator(plan.auth, plan.zone, plan.policy);
181
+ if (authDecorator?.startsWith("@Public"))
182
+ imports.add("Public");
183
+ if (authDecorator?.startsWith("@Auth"))
184
+ imports.add("Auth");
185
+ if (authDecorator?.startsWith("@AdminOnly"))
186
+ imports.add("AdminOnly");
187
+ return `import { ${Array.from(imports).sort().join(", ")} } from '@soapjs/soap-express';`;
188
+ }
189
+ function createRouteAuthDecorator(auth, zone, policy) {
190
+ if (zone === "public") {
191
+ return "@Public()";
192
+ }
193
+ if (auth === "none") {
194
+ return undefined;
195
+ }
196
+ const strategy = auth === "local" ? "jwt" : auth;
197
+ if (zone === "admin" || policy?.type === "admin") {
198
+ return `@AdminOnly('${strategy}')`;
199
+ }
200
+ const policyArgument = (0, auth_policy_1.createAuthPolicyArgument)(policy);
201
+ return policyArgument ? `@Auth('${strategy}', ${policyArgument})` : `@Auth('${strategy}')`;
202
+ }
203
+ function createRouteContractTs(className, camelName, method, contracts) {
204
+ const sourceExpression = method === "get" || method === "delete" || method === "head" || method === "options"
205
+ ? "{ ...req.params, ...req.query }"
206
+ : "{ ...req.params, ...req.query, ...req.body }";
207
+ if (contracts === "zod") {
208
+ return `import { Request } from 'express';
209
+ import { z } from 'zod';
210
+
211
+ export const ${camelName}BodySchema = z.record(z.unknown());
212
+
213
+ export type ${className}RouteInput = z.infer<typeof ${camelName}BodySchema>;
214
+
215
+ export function ${camelName}BodyContract(req: Request): ${className}RouteInput {
216
+ return ${camelName}BodySchema.parse(${sourceExpression});
217
+ }
218
+ `;
219
+ }
220
+ return `import { Request } from 'express';
221
+
222
+ export interface ${className}RouteInput {
223
+ [key: string]: unknown;
224
+ }
225
+
226
+ export function ${camelName}BodyContract(req: Request): ${className}RouteInput {
227
+ return ${sourceExpression};
228
+ }
229
+ `;
230
+ }
231
+ function resolveRoutePath(resourcePath, routeName, routePath) {
232
+ if (!routePath) {
233
+ return normalizePath(`${resourcePath}/${routeName}`);
234
+ }
235
+ const normalized = normalizePath(routePath);
236
+ if (normalized === resourcePath || normalized.startsWith(`${resourcePath}/`)) {
237
+ return normalized;
238
+ }
239
+ if (!routePath.startsWith("/")) {
240
+ return normalizePath(`${resourcePath}/${routePath}`);
241
+ }
242
+ throw new errors_1.CliError(`Route path "${routePath}" must be under resource path "${resourcePath}".`);
243
+ }
244
+ function toControllerRoutePath(resourcePath, absoluteRoutePath) {
245
+ if (absoluteRoutePath === resourcePath) {
246
+ return "/";
247
+ }
248
+ return absoluteRoutePath.slice(resourcePath.length) || "/";
249
+ }
250
+ function normalizePath(value) {
251
+ const normalized = value.replace(/\/+/g, "/");
252
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
253
+ }
254
+ function planFeatureRoot(featuresRoot, resourceName) {
255
+ return path_1.default.posix.join(featuresRoot, resourceName);
256
+ }
@@ -0,0 +1,9 @@
1
+ import { AuthCapability } from "../../config/schemas/types";
2
+ import { PlannedFile } from "../../io/file-writer";
3
+ export interface AddSocketPlan {
4
+ name: string;
5
+ feature: string;
6
+ auth: "none" | AuthCapability;
7
+ featuresRoot: string;
8
+ }
9
+ export declare function createSocketFiles(root: string, plan: AddSocketPlan): Promise<PlannedFile[]>;
@@ -0,0 +1,81 @@
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.createSocketFiles = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const errors_1 = require("../../core/errors");
10
+ const naming_1 = require("../../templates/naming");
11
+ async function createSocketFiles(root, plan) {
12
+ const socketNames = (0, naming_1.createNameVariants)(plan.name);
13
+ const featureNames = (0, naming_1.createNameVariants)(plan.feature);
14
+ const featureRoot = path_1.default.posix.join(plan.featuresRoot, featureNames.kebabName);
15
+ const socketPath = `${featureRoot}/api/sockets/${socketNames.kebabName}.socket.ts`;
16
+ return [
17
+ {
18
+ path: socketPath,
19
+ type: "resource",
20
+ owner: featureNames.kebabName,
21
+ content: createSocketTs(`${socketNames.pascalName}Socket`, `${socketNames.camelName}Socket`, socketNames.kebabName, plan.auth),
22
+ },
23
+ await createSocketsConfigFile(root, {
24
+ className: `${socketNames.pascalName}Socket`,
25
+ exportName: `${socketNames.camelName}Socket`,
26
+ importPath: `../${featureRoot.replace(/^src\//, "")}/api/sockets/${socketNames.kebabName}.socket`,
27
+ }),
28
+ ];
29
+ }
30
+ exports.createSocketFiles = createSocketFiles;
31
+ function createSocketTs(className, exportName, eventName, auth) {
32
+ const authComment = auth === "none" ? "" : "\n // TODO: validate socket auth before handling this message.\n";
33
+ return `import { SocketMessage, SocketServer } from '@soapjs/soap-node-socket';
34
+ import { AppSocketHandler } from '../../../../common/sockets/socket.setup';
35
+
36
+ export class ${className} implements AppSocketHandler {
37
+ readonly event = '${eventName}';
38
+
39
+ async handle(clientId: string, message: SocketMessage, server: SocketServer): Promise<void> {${authComment}
40
+ server.sendToClient(clientId, {
41
+ type: '${eventName}:ack',
42
+ payload: {
43
+ received: true,
44
+ input: message.payload,
45
+ },
46
+ });
47
+ }
48
+ }
49
+
50
+ export const ${exportName} = new ${className}();
51
+ `;
52
+ }
53
+ async function createSocketsConfigFile(root, socket) {
54
+ const configPath = "src/config/sockets.ts";
55
+ const absolutePath = path_1.default.join(root, configPath);
56
+ let current;
57
+ try {
58
+ current = await promises_1.default.readFile(absolutePath, "utf8");
59
+ }
60
+ catch {
61
+ throw new errors_1.CliError("WebSocket config is missing. Create the project with `--realtime ws` first.");
62
+ }
63
+ if (current.includes(` ${socket.exportName} `) || current.includes(`{ ${socket.exportName} }`)) {
64
+ throw new errors_1.CliError(`Socket handler "${socket.exportName}" is already registered.`);
65
+ }
66
+ const importLine = `import { ${socket.exportName} } from '${socket.importPath}';`;
67
+ const withImport = `${importLine}\n${current}`;
68
+ const content = withImport.replace(/export const socketHandlers: AppSocketHandler\[] = \[([\s\S]*?)\];/, (_match, body) => {
69
+ const entries = body
70
+ .split("\n")
71
+ .map((line) => line.trim())
72
+ .filter(Boolean);
73
+ entries.push(` ${socket.exportName},`);
74
+ return `export const socketHandlers: AppSocketHandler[] = [\n${entries.join("\n")}\n];`;
75
+ });
76
+ return {
77
+ path: configPath,
78
+ type: "config",
79
+ content,
80
+ };
81
+ }
@@ -0,0 +1,7 @@
1
+ import { PlannedFile } from "../../io/file-writer";
2
+ export interface AddUseCasePlan {
3
+ name: string;
4
+ feature: string;
5
+ featuresRoot: string;
6
+ }
7
+ export declare function createUseCaseFiles(plan: AddUseCasePlan): PlannedFile[];
@@ -0,0 +1,86 @@
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.createUseCaseFiles = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const naming_1 = require("../../templates/naming");
9
+ function createUseCaseFiles(plan) {
10
+ const useCaseNames = (0, naming_1.createNameVariants)(plan.name);
11
+ const featureNames = (0, naming_1.createNameVariants)(plan.feature);
12
+ const root = path_1.default.posix.join(plan.featuresRoot, featureNames.kebabName, "application", "use-cases");
13
+ return [
14
+ {
15
+ path: `${root}/${useCaseNames.kebabName}.use-case.ts`,
16
+ type: "use-case",
17
+ owner: featureNames.kebabName,
18
+ content: createUseCaseTs(useCaseNames.pascalName, useCaseNames.constantName),
19
+ },
20
+ {
21
+ path: `${root}/${useCaseNames.kebabName}.use-case.spec.ts`,
22
+ type: "use-case",
23
+ owner: featureNames.kebabName,
24
+ content: createUseCaseSpecTs(useCaseNames.pascalName, useCaseNames.kebabName),
25
+ },
26
+ ];
27
+ }
28
+ exports.createUseCaseFiles = createUseCaseFiles;
29
+ function createUseCaseTs(className, constantName) {
30
+ return `import { Inject, Injectable, Result, UseCase } from '@soapjs/soap';
31
+
32
+ export interface ${className}Input {
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ export interface ${className}Output {
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ export interface ${className}Repository {
41
+ save(input: ${className}Input): Promise<${className}Output> | ${className}Output;
42
+ }
43
+
44
+ export const ${constantName}_REPOSITORY = '${className}Repository';
45
+
46
+ @Injectable()
47
+ export class ${className}UseCase implements UseCase<${className}Output> {
48
+ constructor(@Inject(${constantName}_REPOSITORY) private readonly repository: ${className}Repository) {}
49
+
50
+ async execute(input: ${className}Input): Promise<Result<${className}Output>> {
51
+ try {
52
+ const output = await this.repository.save(input);
53
+ return Result.withSuccess(output);
54
+ } catch (error) {
55
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
56
+ }
57
+ }
58
+ }
59
+ `;
60
+ }
61
+ function createUseCaseSpecTs(className, kebabName) {
62
+ return `import assert from 'node:assert/strict';
63
+ import test from 'node:test';
64
+ import { ${className}Repository, ${className}UseCase } from './${kebabName}.use-case';
65
+
66
+ test('${className}UseCase returns a successful Result from the repository', async () => {
67
+ const repository: ${className}Repository = {
68
+ async save(input) {
69
+ return {
70
+ id: 'generated-id',
71
+ ...input,
72
+ };
73
+ },
74
+ };
75
+ const useCase = new ${className}UseCase(repository);
76
+
77
+ const result = await useCase.execute({ name: 'Ada' });
78
+
79
+ assert.equal(result.isSuccess(), true);
80
+ assert.deepEqual(result.content, {
81
+ id: 'generated-id',
82
+ name: 'Ada',
83
+ });
84
+ });
85
+ `;
86
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCheckCommand(program: Command): void;
@@ -0,0 +1,113 @@
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.registerCheckCommand = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const load_soap_config_1 = require("../../config/load-soap-config");
10
+ const command_context_1 = require("../../core/command-context");
11
+ const errors_1 = require("../../core/errors");
12
+ const naming_1 = require("../../templates/naming");
13
+ const allowedZones = ["public", "private", "admin"];
14
+ const allowedAuth = ["none", "jwt", "api-key"];
15
+ function registerCheckCommand(program) {
16
+ const check = program.command("check").description("Validate generated SoapJS project metadata.");
17
+ check
18
+ .command("routes")
19
+ .description("Validate route registry consistency.")
20
+ .action(async (_options, command) => {
21
+ const context = (0, command_context_1.getCommandContext)(command);
22
+ const config = await (0, load_soap_config_1.loadSoapConfig)(context.cwd);
23
+ const errors = checkRoutes(config);
24
+ if (errors.length > 0) {
25
+ for (const error of errors) {
26
+ context.output.error(error);
27
+ }
28
+ throw new errors_1.CliError(`Route check failed with ${errors.length} issue${errors.length === 1 ? "" : "s"}.`);
29
+ }
30
+ context.output.success(`Route registry is valid (${config.registry.routes.length} routes).`);
31
+ });
32
+ }
33
+ exports.registerCheckCommand = registerCheckCommand;
34
+ function checkRoutes(config) {
35
+ const errors = [];
36
+ const routeKeys = new Map();
37
+ const resourceNames = new Set(config.registry.resources.map((resource) => resource.name));
38
+ for (const route of config.registry.routes) {
39
+ const label = `${route.method.toUpperCase()} ${route.path}`;
40
+ const key = label.toLowerCase();
41
+ const duplicate = routeKeys.get(key);
42
+ if (duplicate) {
43
+ errors.push(`Duplicate route ${label} used by ${duplicate.resource}/${duplicate.name} and ${route.resource}/${route.name}.`);
44
+ }
45
+ else {
46
+ routeKeys.set(key, route);
47
+ }
48
+ if (!resourceNames.has(route.resource)) {
49
+ errors.push(`Route ${route.resource}/${route.name} references unknown resource "${route.resource}".`);
50
+ }
51
+ if (!allowedZones.includes(route.zone)) {
52
+ errors.push(`Route ${route.resource}/${route.name} uses unknown zone "${route.zone}".`);
53
+ }
54
+ else if (!config.project.zones.includes(route.zone)) {
55
+ errors.push(`Route ${route.resource}/${route.name} uses zone "${route.zone}" that is not enabled for this project.`);
56
+ }
57
+ if (!allowedAuth.includes(route.auth)) {
58
+ errors.push(`Route ${route.resource}/${route.name} uses unknown auth strategy "${route.auth}".`);
59
+ }
60
+ else if (route.auth !== "none" &&
61
+ !enabledRouteAuthValues(config.project.capabilities.auth).includes(route.auth)) {
62
+ errors.push(`Route ${route.resource}/${route.name} uses auth strategy "${route.auth}" that is not enabled for this project.`);
63
+ }
64
+ if (route.policy) {
65
+ errors.push(...checkAuthPolicy(route));
66
+ }
67
+ const contractPaths = expectedContractPaths(config, route);
68
+ if (!contractPaths.some((contractPath) => fs_1.default.existsSync(path_1.default.join(config.root, contractPath)))) {
69
+ errors.push(`Route ${route.resource}/${route.name} is missing contract file (${contractPaths.join(" or ")}).`);
70
+ }
71
+ if ((config.api.bruno.enabled || config.project.capabilities.apiClient.includes("bruno")) && route.bruno !== false) {
72
+ const brunoPath = expectedBrunoPath(config, route);
73
+ if (!fs_1.default.existsSync(path_1.default.join(config.root, brunoPath))) {
74
+ errors.push(`Route ${route.resource}/${route.name} is missing Bruno file ${brunoPath}.`);
75
+ }
76
+ }
77
+ }
78
+ return errors;
79
+ }
80
+ function checkAuthPolicy(route) {
81
+ const errors = [];
82
+ if (route.auth === "none") {
83
+ errors.push(`Route ${route.resource}/${route.name} uses auth policy without route auth.`);
84
+ }
85
+ if (route.policy?.type === "roles" && route.policy.roles.length === 0) {
86
+ errors.push(`Route ${route.resource}/${route.name} uses roles policy without roles.`);
87
+ }
88
+ if (route.policy?.type === "custom" && route.policy.name.trim().length === 0) {
89
+ errors.push(`Route ${route.resource}/${route.name} uses custom policy without a name.`);
90
+ }
91
+ return errors;
92
+ }
93
+ function enabledRouteAuthValues(values) {
94
+ const allowed = ["none"];
95
+ if (values.includes("jwt") || values.includes("local")) {
96
+ allowed.push("jwt");
97
+ }
98
+ if (values.includes("api-key")) {
99
+ allowed.push("api-key");
100
+ }
101
+ return allowed;
102
+ }
103
+ function expectedContractPaths(config, route) {
104
+ const resourceNames = (0, naming_1.createNameVariants)(route.resource);
105
+ const routeNames = (0, naming_1.createNameVariants)(route.name);
106
+ const contractNames = Array.from(new Set([routeNames.kebabName, `${routeNames.kebabName}-${resourceNames.kebabName}`, `${routeNames.kebabName}-${resourceNames.pluralName}`]));
107
+ return contractNames.map((contractName) => path_1.default.posix.join(config.structure.featuresRoot, resourceNames.kebabName, config.structure.paths.contracts, `${contractName}.contract.ts`));
108
+ }
109
+ function expectedBrunoPath(config, route) {
110
+ const resourceNames = (0, naming_1.createNameVariants)(route.resource);
111
+ const routeNames = (0, naming_1.createNameVariants)(route.name);
112
+ return path_1.default.posix.join(config.api.bruno.collectionPath, resourceNames.pascalName, `${routeNames.pascalName}.bru`);
113
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerCreateCommand(program: Command): void;