@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,15 @@
1
+ import { PlannedFile } from "../../io/file-writer";
2
+ export interface AddCommandPlan {
3
+ name: string;
4
+ feature: string;
5
+ featuresRoot: string;
6
+ }
7
+ export declare function createCommandFiles(plan: AddCommandPlan): PlannedFile[];
8
+ export declare function createCommandIndexFile(feature: string, featuresRoot: string, commandNames: string[]): PlannedFile;
9
+ export interface CqrsFeatureImports {
10
+ commands: string[];
11
+ queries?: string[];
12
+ }
13
+ export declare function createCqrsConfigFile(featuresRoot: string, importsByKind: CqrsFeatureImports | string[]): PlannedFile;
14
+ export declare function commandNameFromPath(filePath: string): string | undefined;
15
+ export declare function commandFeatureFromPath(filePath: string, featuresRoot: string): string | undefined;
@@ -0,0 +1,182 @@
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.commandFeatureFromPath = exports.commandNameFromPath = exports.createCqrsConfigFile = exports.createCommandIndexFile = exports.createCommandFiles = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const naming_1 = require("../../templates/naming");
9
+ function createCommandFiles(plan) {
10
+ const commandNames = (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", "commands");
13
+ return [
14
+ {
15
+ path: `${root}/${commandNames.kebabName}.command.ts`,
16
+ type: "use-case",
17
+ owner: featureNames.kebabName,
18
+ content: createCommandTs(commandNames.pascalName),
19
+ },
20
+ {
21
+ path: `${root}/${commandNames.kebabName}.handler.ts`,
22
+ type: "use-case",
23
+ owner: featureNames.kebabName,
24
+ content: createCommandHandlerTs(commandNames.pascalName, commandNames.kebabName),
25
+ },
26
+ {
27
+ path: `${root}/${commandNames.kebabName}.handler.spec.ts`,
28
+ type: "use-case",
29
+ owner: featureNames.kebabName,
30
+ content: createCommandHandlerSpecTs(commandNames.pascalName, commandNames.kebabName),
31
+ },
32
+ ];
33
+ }
34
+ exports.createCommandFiles = createCommandFiles;
35
+ function createCommandIndexFile(feature, featuresRoot, commandNames) {
36
+ const featureNames = (0, naming_1.createNameVariants)(feature);
37
+ const uniqueCommandNames = Array.from(new Set(commandNames)).sort();
38
+ const exports = uniqueCommandNames
39
+ .flatMap((commandName) => {
40
+ const names = (0, naming_1.createNameVariants)(commandName);
41
+ return [
42
+ `export * from './${names.kebabName}.command';`,
43
+ `export * from './${names.kebabName}.handler';`,
44
+ ];
45
+ })
46
+ .join("\n");
47
+ return {
48
+ path: path_1.default.posix.join(featuresRoot, featureNames.kebabName, "application", "commands", "index.ts"),
49
+ type: "config",
50
+ owner: featureNames.kebabName,
51
+ content: `${exports}
52
+ `,
53
+ };
54
+ }
55
+ exports.createCommandIndexFile = createCommandIndexFile;
56
+ function createCqrsConfigFile(featuresRoot, importsByKind) {
57
+ const normalized = Array.isArray(importsByKind)
58
+ ? { commands: importsByKind, queries: [] }
59
+ : { commands: importsByKind.commands, queries: importsByKind.queries ?? [] };
60
+ const importLines = [
61
+ ...Array.from(new Set(normalized.commands)).sort()
62
+ .map((feature) => `import '../${featuresRoot.replace(/^src\//, "")}/${feature}/application/commands';`),
63
+ ...Array.from(new Set(normalized.queries)).sort()
64
+ .map((feature) => `import '../${featuresRoot.replace(/^src\//, "")}/${feature}/application/queries';`),
65
+ ]
66
+ .join("\n");
67
+ return {
68
+ path: "src/config/cqrs.ts",
69
+ type: "config",
70
+ content: importLines
71
+ ? `${importLines}
72
+
73
+ export {};
74
+ `
75
+ : `// Generated CQRS handler imports. Updated by soap add command/query.
76
+ export {};
77
+ `,
78
+ };
79
+ }
80
+ exports.createCqrsConfigFile = createCqrsConfigFile;
81
+ function commandNameFromPath(filePath) {
82
+ const match = filePath.match(/\/application\/commands\/([^/]+)\.command\.ts$/);
83
+ return match?.[1];
84
+ }
85
+ exports.commandNameFromPath = commandNameFromPath;
86
+ function commandFeatureFromPath(filePath, featuresRoot) {
87
+ const escapedRoot = featuresRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
+ const match = filePath.match(new RegExp(`^${escapedRoot}/([^/]+)/application/commands/index\\.ts$`));
89
+ return match?.[1];
90
+ }
91
+ exports.commandFeatureFromPath = commandFeatureFromPath;
92
+ function createCommandTs(className) {
93
+ return `import { BaseCommand } from '@soapjs/soap/cqrs';
94
+
95
+ export interface ${className}Payload {
96
+ [key: string]: unknown;
97
+ }
98
+
99
+ export interface ${className}Result {
100
+ [key: string]: unknown;
101
+ }
102
+
103
+ export class ${className}Command extends BaseCommand<${className}Result> {
104
+ constructor(
105
+ public readonly payload: ${className}Payload = {},
106
+ initiatedBy?: string,
107
+ correlationId?: string
108
+ ) {
109
+ super(initiatedBy, correlationId);
110
+ }
111
+ }
112
+ `;
113
+ }
114
+ function createCommandHandlerTs(className, kebabName) {
115
+ return `import { Result } from '@soapjs/soap';
116
+ import { CommandHandler as SoapCommandHandler } from '@soapjs/soap/cqrs';
117
+ import { CommandHandler } from '@soapjs/soap-express/cqrs';
118
+ import { ${className}Command, ${className}Result } from './${kebabName}.command';
119
+
120
+ export interface ${className}Repository {
121
+ save(command: ${className}Command): Promise<${className}Result> | ${className}Result;
122
+ }
123
+
124
+ @CommandHandler(${className}Command)
125
+ export class ${className}Handler implements SoapCommandHandler<${className}Command, ${className}Result> {
126
+ constructor(private readonly repository?: ${className}Repository) {}
127
+
128
+ async handle(command: ${className}Command): Promise<Result<${className}Result>> {
129
+ try {
130
+ const output = this.repository
131
+ ? await this.repository.save(command)
132
+ : ({ ...command.payload } as ${className}Result);
133
+
134
+ return Result.withSuccess(output);
135
+ } catch (error) {
136
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
137
+ }
138
+ }
139
+ }
140
+ `;
141
+ }
142
+ function createCommandHandlerSpecTs(className, kebabName) {
143
+ return `import assert from 'node:assert/strict';
144
+ import test from 'node:test';
145
+ import { Result } from '@soapjs/soap';
146
+ import { InMemoryCommandBus } from '@soapjs/soap/cqrs';
147
+ import { ${className}Command, ${className}Result } from './${kebabName}.command';
148
+ import { ${className}Handler, ${className}Repository } from './${kebabName}.handler';
149
+
150
+ test('${className}Handler returns a successful Result from a repository port', async () => {
151
+ const repository: ${className}Repository = {
152
+ async save(command) {
153
+ return {
154
+ id: 'generated-id',
155
+ ...command.payload,
156
+ };
157
+ },
158
+ };
159
+ const handler = new ${className}Handler(repository);
160
+
161
+ const result = await handler.handle(new ${className}Command({ name: 'Ada' }));
162
+
163
+ assert.equal(result.isSuccess(), true);
164
+ assert.deepEqual(result.content, {
165
+ id: 'generated-id',
166
+ name: 'Ada',
167
+ });
168
+ });
169
+
170
+ test('${className}Command can be dispatched through a CQRS command bus', async () => {
171
+ const bus = new InMemoryCommandBus();
172
+ bus.register(${className}Command, new ${className}Handler());
173
+
174
+ const result: Result<${className}Result> = await bus.dispatch(new ${className}Command({ name: 'Ada' }));
175
+
176
+ assert.equal(result.isSuccess(), true);
177
+ assert.deepEqual(result.content, {
178
+ name: 'Ada',
179
+ });
180
+ });
181
+ `;
182
+ }
@@ -0,0 +1,7 @@
1
+ import { PlannedFile } from "../../io/file-writer";
2
+ export interface AddEntityPlan {
3
+ name: string;
4
+ feature: string;
5
+ featuresRoot: string;
6
+ }
7
+ export declare function createEntityFiles(plan: AddEntityPlan): PlannedFile[];
@@ -0,0 +1,106 @@
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.createEntityFiles = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const naming_1 = require("../../templates/naming");
9
+ function createEntityFiles(plan) {
10
+ const entityNames = (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, "domain");
13
+ return [
14
+ {
15
+ path: `${root}/${entityNames.kebabName}.entity.ts`,
16
+ type: "entity",
17
+ owner: featureNames.kebabName,
18
+ content: createEntityTs(entityNames.pascalName),
19
+ },
20
+ {
21
+ path: `${root}/${entityNames.kebabName}.entity.spec.ts`,
22
+ type: "entity",
23
+ owner: featureNames.kebabName,
24
+ content: createEntitySpecTs(entityNames.pascalName, entityNames.kebabName),
25
+ },
26
+ ];
27
+ }
28
+ exports.createEntityFiles = createEntityFiles;
29
+ function createEntityTs(className) {
30
+ return `import { randomUUID } from 'crypto';
31
+
32
+ export interface ${className}Props {
33
+ id: string;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+
38
+ export class Invalid${className}Error extends Error {
39
+ constructor(message: string) {
40
+ super(message);
41
+ this.name = 'Invalid${className}Error';
42
+ }
43
+ }
44
+
45
+ export class ${className} {
46
+ private constructor(public readonly props: ${className}Props) {}
47
+
48
+ static create(input: Partial<Pick<${className}Props, 'id' | 'createdAt' | 'updatedAt'>> = {}): ${className} {
49
+ const now = new Date().toISOString();
50
+ const props: ${className}Props = {
51
+ id: input.id ?? randomUUID(),
52
+ createdAt: input.createdAt ?? now,
53
+ updatedAt: input.updatedAt ?? now,
54
+ };
55
+
56
+ ${className}.validate(props);
57
+ return new ${className}(props);
58
+ }
59
+
60
+ static rehydrate(props: ${className}Props): ${className} {
61
+ ${className}.validate(props);
62
+ return new ${className}(props);
63
+ }
64
+
65
+ get id(): string {
66
+ return this.props.id;
67
+ }
68
+
69
+ private static validate(props: ${className}Props): void {
70
+ if (!props.id) {
71
+ throw new Invalid${className}Error('${className} id is required.');
72
+ }
73
+
74
+ if (!props.createdAt || !props.updatedAt) {
75
+ throw new Invalid${className}Error('${className} timestamps are required.');
76
+ }
77
+ }
78
+ }
79
+ `;
80
+ }
81
+ function createEntitySpecTs(className, kebabName) {
82
+ return `import assert from 'node:assert/strict';
83
+ import test from 'node:test';
84
+ import { Invalid${className}Error, ${className} } from './${kebabName}.entity';
85
+
86
+ test('${className}.create generates identity and timestamps', () => {
87
+ const entity = ${className}.create();
88
+
89
+ assert.ok(entity.id);
90
+ assert.ok(entity.props.createdAt);
91
+ assert.ok(entity.props.updatedAt);
92
+ });
93
+
94
+ test('${className}.rehydrate rejects invalid props', () => {
95
+ assert.throws(
96
+ () =>
97
+ ${className}.rehydrate({
98
+ id: '',
99
+ createdAt: '',
100
+ updatedAt: '',
101
+ }),
102
+ Invalid${className}Error
103
+ );
104
+ });
105
+ `;
106
+ }
@@ -0,0 +1,8 @@
1
+ import { PlannedFile } from "../../io/file-writer";
2
+ export interface AddEventPlan {
3
+ name: string;
4
+ feature: string;
5
+ architecture: "regular" | "cqrs";
6
+ featuresRoot: string;
7
+ }
8
+ export declare function createEventFiles(plan: AddEventPlan): PlannedFile[];
@@ -0,0 +1,59 @@
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.createEventFiles = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const naming_1 = require("../../templates/naming");
9
+ function createEventFiles(plan) {
10
+ const eventNames = (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);
13
+ const files = [
14
+ {
15
+ path: `${root}/domain/events/${eventNames.kebabName}.event.ts`,
16
+ type: "resource",
17
+ owner: featureNames.kebabName,
18
+ content: createEventTs(eventNames.pascalName, eventNames.kebabName),
19
+ },
20
+ ];
21
+ if (plan.architecture === "cqrs") {
22
+ files.push({
23
+ path: `${root}/api/events/${eventNames.kebabName}.handler.ts`,
24
+ type: "resource",
25
+ owner: featureNames.kebabName,
26
+ content: createEventHandlerTs(eventNames.pascalName, eventNames.kebabName),
27
+ });
28
+ }
29
+ return files;
30
+ }
31
+ exports.createEventFiles = createEventFiles;
32
+ function createEventTs(className, eventName) {
33
+ return `import { BaseDomainEvent } from '../../../../common/events/domain-event';
34
+
35
+ export interface ${className}Data {
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export class ${className}Event extends BaseDomainEvent<${className}Data> {
40
+ static readonly type = '${eventName}';
41
+
42
+ constructor(aggregateId: string, data: ${className}Data = {}, version = 1) {
43
+ super(${className}Event.type, aggregateId, data, version);
44
+ }
45
+ }
46
+ `;
47
+ }
48
+ function createEventHandlerTs(className, eventName) {
49
+ return `import { EventHandler } from '@soapjs/soap-express/build/decorators/event';
50
+ import { ${className}Event } from '../../domain/events/${eventName}.event';
51
+
52
+ @EventHandler(${className}Event)
53
+ export class ${className}Handler {
54
+ async handle(event: ${className}Event): Promise<void> {
55
+ void event;
56
+ }
57
+ }
58
+ `;
59
+ }
@@ -0,0 +1,10 @@
1
+ import { PlannedFile } from "../../io/file-writer";
2
+ export interface AddQueryPlan {
3
+ name: string;
4
+ feature: string;
5
+ featuresRoot: string;
6
+ }
7
+ export declare function createQueryFiles(plan: AddQueryPlan): PlannedFile[];
8
+ export declare function createQueryIndexFile(feature: string, featuresRoot: string, queryNames: string[]): PlannedFile;
9
+ export declare function queryNameFromPath(filePath: string): string | undefined;
10
+ export declare function queryFeatureFromPath(filePath: string, featuresRoot: string): string | undefined;
@@ -0,0 +1,156 @@
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.queryFeatureFromPath = exports.queryNameFromPath = exports.createQueryIndexFile = exports.createQueryFiles = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const naming_1 = require("../../templates/naming");
9
+ function createQueryFiles(plan) {
10
+ const queryNames = (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", "queries");
13
+ return [
14
+ {
15
+ path: `${root}/${queryNames.kebabName}.query.ts`,
16
+ type: "use-case",
17
+ owner: featureNames.kebabName,
18
+ content: createQueryTs(queryNames.pascalName),
19
+ },
20
+ {
21
+ path: `${root}/${queryNames.kebabName}.handler.ts`,
22
+ type: "use-case",
23
+ owner: featureNames.kebabName,
24
+ content: createQueryHandlerTs(queryNames.pascalName, queryNames.kebabName),
25
+ },
26
+ {
27
+ path: `${root}/${queryNames.kebabName}.handler.spec.ts`,
28
+ type: "use-case",
29
+ owner: featureNames.kebabName,
30
+ content: createQueryHandlerSpecTs(queryNames.pascalName, queryNames.kebabName),
31
+ },
32
+ ];
33
+ }
34
+ exports.createQueryFiles = createQueryFiles;
35
+ function createQueryIndexFile(feature, featuresRoot, queryNames) {
36
+ const featureNames = (0, naming_1.createNameVariants)(feature);
37
+ const uniqueQueryNames = Array.from(new Set(queryNames)).sort();
38
+ const exports = uniqueQueryNames
39
+ .flatMap((queryName) => {
40
+ const names = (0, naming_1.createNameVariants)(queryName);
41
+ return [
42
+ `export * from './${names.kebabName}.query';`,
43
+ `export * from './${names.kebabName}.handler';`,
44
+ ];
45
+ })
46
+ .join("\n");
47
+ return {
48
+ path: path_1.default.posix.join(featuresRoot, featureNames.kebabName, "application", "queries", "index.ts"),
49
+ type: "config",
50
+ owner: featureNames.kebabName,
51
+ content: `${exports}
52
+ `,
53
+ };
54
+ }
55
+ exports.createQueryIndexFile = createQueryIndexFile;
56
+ function queryNameFromPath(filePath) {
57
+ const match = filePath.match(/\/application\/queries\/([^/]+)\.query\.ts$/);
58
+ return match?.[1];
59
+ }
60
+ exports.queryNameFromPath = queryNameFromPath;
61
+ function queryFeatureFromPath(filePath, featuresRoot) {
62
+ const escapedRoot = featuresRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ const match = filePath.match(new RegExp(`^${escapedRoot}/([^/]+)/application/queries/index\\.ts$`));
64
+ return match?.[1];
65
+ }
66
+ exports.queryFeatureFromPath = queryFeatureFromPath;
67
+ function createQueryTs(className) {
68
+ return `import { BaseQuery } from '@soapjs/soap/cqrs';
69
+
70
+ export interface ${className}Criteria {
71
+ [key: string]: unknown;
72
+ }
73
+
74
+ export interface ${className}Result {
75
+ [key: string]: unknown;
76
+ }
77
+
78
+ export class ${className}Query extends BaseQuery<${className}Result> {
79
+ constructor(
80
+ public readonly criteria: ${className}Criteria = {},
81
+ initiatedBy?: string,
82
+ correlationId?: string
83
+ ) {
84
+ super(initiatedBy, correlationId);
85
+ }
86
+ }
87
+ `;
88
+ }
89
+ function createQueryHandlerTs(className, kebabName) {
90
+ return `import { Result } from '@soapjs/soap';
91
+ import { QueryHandler as SoapQueryHandler } from '@soapjs/soap/cqrs';
92
+ import { QueryHandler } from '@soapjs/soap-express/cqrs';
93
+ import { ${className}Query, ${className}Result } from './${kebabName}.query';
94
+
95
+ export interface ${className}Repository {
96
+ find(query: ${className}Query): Promise<${className}Result> | ${className}Result;
97
+ }
98
+
99
+ @QueryHandler(${className}Query)
100
+ export class ${className}Handler implements SoapQueryHandler<${className}Query, ${className}Result> {
101
+ constructor(private readonly repository?: ${className}Repository) {}
102
+
103
+ async handle(query: ${className}Query): Promise<Result<${className}Result>> {
104
+ try {
105
+ const output = this.repository
106
+ ? await this.repository.find(query)
107
+ : ({ ...query.criteria } as ${className}Result);
108
+
109
+ return Result.withSuccess(output);
110
+ } catch (error) {
111
+ return Result.withFailure(error instanceof Error ? error : new Error(String(error)));
112
+ }
113
+ }
114
+ }
115
+ `;
116
+ }
117
+ function createQueryHandlerSpecTs(className, kebabName) {
118
+ return `import assert from 'node:assert/strict';
119
+ import test from 'node:test';
120
+ import { Result } from '@soapjs/soap';
121
+ import { InMemoryQueryBus } from '@soapjs/soap/cqrs';
122
+ import { ${className}Query, ${className}Result } from './${kebabName}.query';
123
+ import { ${className}Handler, ${className}Repository } from './${kebabName}.handler';
124
+
125
+ test('${className}Handler returns a successful Result from a repository port', async () => {
126
+ const repository: ${className}Repository = {
127
+ async find(query) {
128
+ return {
129
+ id: 'existing-id',
130
+ ...query.criteria,
131
+ };
132
+ },
133
+ };
134
+ const handler = new ${className}Handler(repository);
135
+
136
+ const result = await handler.handle(new ${className}Query({ id: 'existing-id' }));
137
+
138
+ assert.equal(result.isSuccess(), true);
139
+ assert.deepEqual(result.content, {
140
+ id: 'existing-id',
141
+ });
142
+ });
143
+
144
+ test('${className}Query can be dispatched through a CQRS query bus', async () => {
145
+ const bus = new InMemoryQueryBus();
146
+ bus.register(${className}Query, new ${className}Handler());
147
+
148
+ const result: Result<${className}Result> = await bus.dispatch(new ${className}Query({ id: 'existing-id' }));
149
+
150
+ assert.equal(result.isSuccess(), true);
151
+ assert.deepEqual(result.content, {
152
+ id: 'existing-id',
153
+ });
154
+ });
155
+ `;
156
+ }
@@ -0,0 +1,11 @@
1
+ import { DatabaseCapability } from "../../config/schemas/types";
2
+ import { PlannedFile } from "../../io/file-writer";
3
+ type RepositoryDb = Extract<DatabaseCapability, "mongo" | "postgres" | "mysql" | "sqlite">;
4
+ export interface AddRepositoryPlan {
5
+ name: string;
6
+ feature: string;
7
+ db: RepositoryDb;
8
+ featuresRoot: string;
9
+ }
10
+ export declare function createRepositoryFiles(plan: AddRepositoryPlan): PlannedFile[];
11
+ export {};