eva4j 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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/QUICK_REFERENCE.md +204 -0
  3. package/README.md +912 -0
  4. package/USAGE.md +349 -0
  5. package/bin/eva4j.js +234 -0
  6. package/config/defaults.json +46 -0
  7. package/package.json +57 -0
  8. package/src/commands/add-kafka-client.js +193 -0
  9. package/src/commands/add-module.js +221 -0
  10. package/src/commands/create.js +92 -0
  11. package/src/commands/detach.js +495 -0
  12. package/src/commands/generate-http-exchange.js +309 -0
  13. package/src/commands/generate-kafka-event.js +453 -0
  14. package/src/commands/generate-kafka-listener.js +267 -0
  15. package/src/commands/generate-resource.js +265 -0
  16. package/src/commands/generate-usecase.js +198 -0
  17. package/src/commands/info.js +63 -0
  18. package/src/generators/base-generator.js +150 -0
  19. package/src/generators/module-generator.js +48 -0
  20. package/src/generators/shared-generator.js +153 -0
  21. package/src/utils/config-manager.js +156 -0
  22. package/src/utils/context-builder.js +149 -0
  23. package/src/utils/naming.js +137 -0
  24. package/src/utils/template-engine.js +55 -0
  25. package/src/utils/validator.js +159 -0
  26. package/templates/base/application/Application.java.ejs +27 -0
  27. package/templates/base/docker/docker-compose.yml.ejs +41 -0
  28. package/templates/base/gradle/build.gradle.ejs +70 -0
  29. package/templates/base/gradle/settings.gradle.ejs +1 -0
  30. package/templates/base/resources/application-develop.yml.ejs +5 -0
  31. package/templates/base/resources/application-local.yml.ejs +5 -0
  32. package/templates/base/resources/application-production.yml.ejs +9 -0
  33. package/templates/base/resources/application-test.yml.ejs +5 -0
  34. package/templates/base/resources/application.yml.ejs +31 -0
  35. package/templates/base/resources/parameters/develop/cors.yml.ejs +4 -0
  36. package/templates/base/resources/parameters/develop/db.yaml.ejs +21 -0
  37. package/templates/base/resources/parameters/develop/kafka.yml.ejs +26 -0
  38. package/templates/base/resources/parameters/local/cors.yml.ejs +4 -0
  39. package/templates/base/resources/parameters/local/db.yaml.ejs +21 -0
  40. package/templates/base/resources/parameters/local/kafka.yml.ejs +26 -0
  41. package/templates/base/resources/parameters/production/cors.yml.ejs +4 -0
  42. package/templates/base/resources/parameters/production/db.yaml.ejs +21 -0
  43. package/templates/base/resources/parameters/production/kafka.yml.ejs +26 -0
  44. package/templates/base/root/README.md.ejs +126 -0
  45. package/templates/base/root/gitignore.ejs +42 -0
  46. package/templates/http-exchange/Adapter.java.ejs +39 -0
  47. package/templates/http-exchange/Config.java.ejs +24 -0
  48. package/templates/http-exchange/FeignClient.java.ejs +23 -0
  49. package/templates/http-exchange/Port.java.ejs +14 -0
  50. package/templates/kafka-event/Event.java.ejs +10 -0
  51. package/templates/kafka-event/KafkaConfigBean.java.ejs +7 -0
  52. package/templates/kafka-event/KafkaMessageBroker.java.ejs +32 -0
  53. package/templates/kafka-event/MessageBroker.java.ejs +8 -0
  54. package/templates/kafka-event/MessageBrokerImplMethod.java.ejs +9 -0
  55. package/templates/kafka-event/MessageBrokerMethod.java.ejs +1 -0
  56. package/templates/kafka-listener/KafkaController.java.ejs +34 -0
  57. package/templates/kafka-listener/ListenerMethod.java.ejs +9 -0
  58. package/templates/kafka-listener/ValueField.java.ejs +4 -0
  59. package/templates/module/controller.java.ejs +96 -0
  60. package/templates/module/exception.java.ejs +18 -0
  61. package/templates/module/mapper.java.ejs +67 -0
  62. package/templates/module/model.java.ejs +29 -0
  63. package/templates/module/package-info.java.ejs +7 -0
  64. package/templates/module/repository.java.ejs +26 -0
  65. package/templates/module/request-dto.java.ejs +24 -0
  66. package/templates/module/response-dto.java.ejs +26 -0
  67. package/templates/module/service-impl.java.ejs +112 -0
  68. package/templates/module/service.java.ejs +45 -0
  69. package/templates/module/update-dto.java.ejs +25 -0
  70. package/templates/resource/Command.java.ejs +9 -0
  71. package/templates/resource/CommandHandler.java.ejs +22 -0
  72. package/templates/resource/Controller.java.ejs +73 -0
  73. package/templates/resource/Query.java.ejs +12 -0
  74. package/templates/resource/QueryHandler.java.ejs +31 -0
  75. package/templates/resource/ResponseDto.java.ejs +6 -0
  76. package/templates/shared/annotations/ApplicationComponent.java.ejs +9 -0
  77. package/templates/shared/annotations/DomainComponent.java.ejs +9 -0
  78. package/templates/shared/annotations/LogAfter.java.ejs +11 -0
  79. package/templates/shared/annotations/LogBefore.java.ejs +11 -0
  80. package/templates/shared/annotations/LogExceptions.java.ejs +11 -0
  81. package/templates/shared/annotations/LogTimer.java.ejs +11 -0
  82. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +49 -0
  83. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +56 -0
  84. package/templates/shared/configurations/securityConfig/SecurityConfig.java.ejs +57 -0
  85. package/templates/shared/configurations/swaggerConfig/SwaggerConfig.java.ejs +31 -0
  86. package/templates/shared/configurations/useCaseConfig/UseCaseAutoRegister.java.ejs +51 -0
  87. package/templates/shared/configurations/useCaseConfig/UseCaseConfig.java.ejs +25 -0
  88. package/templates/shared/configurations/useCaseConfig/UseCaseContainer.java.ejs +29 -0
  89. package/templates/shared/configurations/useCaseConfig/UseCaseMediator.java.ejs +38 -0
  90. package/templates/shared/customExceptions/BadRequestException.java.ejs +11 -0
  91. package/templates/shared/customExceptions/ConflictException.java.ejs +8 -0
  92. package/templates/shared/customExceptions/ForbiddenException.java.ejs +8 -0
  93. package/templates/shared/customExceptions/ImportFileException.java.ejs +6 -0
  94. package/templates/shared/customExceptions/NotFoundException.java.ejs +8 -0
  95. package/templates/shared/customExceptions/UnauthorizedException.java.ejs +9 -0
  96. package/templates/shared/customExceptions/ValidationException.java.ejs +17 -0
  97. package/templates/shared/errorMessage/ErrorMessage.java.ejs +5 -0
  98. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +9 -0
  99. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +6 -0
  100. package/templates/shared/eventEnvelope/EventEnvelope.java.ejs +13 -0
  101. package/templates/shared/eventEnvelope/EventMetadata.java.ejs +24 -0
  102. package/templates/shared/filters/CorrelationIdFilter.java.ejs +45 -0
  103. package/templates/shared/handlerException/HandlerExceptions.java.ejs +148 -0
  104. package/templates/shared/interfaces/Command.java.ejs +4 -0
  105. package/templates/shared/interfaces/CommandHandler.java.ejs +5 -0
  106. package/templates/shared/interfaces/Dispatchable.java.ejs +4 -0
  107. package/templates/shared/interfaces/Handler.java.ejs +4 -0
  108. package/templates/shared/interfaces/Query.java.ejs +4 -0
  109. package/templates/shared/interfaces/QueryHandler.java.ejs +5 -0
  110. package/templates/shared/package-info.java.ejs +8 -0
  111. package/templates/usecase/command/Command.java.ejs +7 -0
  112. package/templates/usecase/command/CommandHandler.java.ejs +21 -0
  113. package/templates/usecase/query/Query.java.ejs +10 -0
  114. package/templates/usecase/query/QueryHandler.java.ejs +22 -0
  115. package/templates/usecase/query/ResponseDto.java.ejs +5 -0
@@ -0,0 +1,267 @@
1
+ const inquirer = require('inquirer');
2
+ const ora = require('ora');
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('js-yaml');
7
+ const ConfigManager = require('../utils/config-manager');
8
+ const { isEva4jProject } = require('../utils/validator');
9
+ const { toPackagePath, toPascalCase, toCamelCase, toKebabCase } = require('../utils/naming');
10
+ const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
11
+
12
+ /**
13
+ * Generate Kafka listener methods in a module's infrastructure
14
+ * @param {string} moduleName - Name of the module
15
+ */
16
+ async function generateKafkaListenerCommand(moduleName) {
17
+ const projectDir = process.cwd();
18
+
19
+ // Validate eva4j project
20
+ if (!(await isEva4jProject(projectDir))) {
21
+ console.error(chalk.red('āŒ Not in an eva4j project directory'));
22
+ console.error(chalk.gray(' Run this command from the root of an eva4j project'));
23
+ process.exit(1);
24
+ }
25
+
26
+ // Check if Kafka is installed
27
+ const configManager = new ConfigManager(projectDir);
28
+ if (!(await configManager.featureExists('kafka'))) {
29
+ console.error(chalk.red('āŒ Kafka client is not installed in this project'));
30
+ console.error(chalk.gray(' Run: eva4j add kafka-client'));
31
+ process.exit(1);
32
+ }
33
+
34
+ // Load project configuration
35
+ const projectConfig = await configManager.loadProjectConfig();
36
+ const { packageName, projectName } = projectConfig;
37
+ const packagePath = toPackagePath(packageName);
38
+
39
+ // Validate module exists
40
+ if (!(await configManager.moduleExists(moduleName))) {
41
+ console.error(chalk.red(`āŒ Module '${moduleName}' not found in project`));
42
+ console.error(chalk.gray(' Available modules:'));
43
+ const modules = projectConfig.modules || [];
44
+ modules.forEach(mod => console.error(chalk.gray(` - ${mod}`)));
45
+ process.exit(1);
46
+ }
47
+
48
+ // Read available topics from kafka.yml
49
+ const topics = await getAvailableTopics(projectDir);
50
+
51
+ if (topics.length === 0) {
52
+ console.error(chalk.red('āŒ No topics found in kafka.yml'));
53
+ console.error(chalk.gray(' Add topics using: eva4j generate kafka-event <module> <event-name>'));
54
+ process.exit(1);
55
+ }
56
+
57
+ // Prompt for topic selection (multiple)
58
+ const { selectedTopics } = await inquirer.prompt([
59
+ {
60
+ type: 'checkbox',
61
+ name: 'selectedTopics',
62
+ message: 'Select topics to listen to (use space to select, enter to confirm):',
63
+ choices: topics.map(t => ({
64
+ name: `${t.key} (${t.value})`,
65
+ value: t.key,
66
+ checked: false
67
+ })),
68
+ validate: (answer) => {
69
+ if (answer.length === 0) {
70
+ return 'You must select at least one topic';
71
+ }
72
+ return true;
73
+ }
74
+ }
75
+ ]);
76
+
77
+ const spinner = ora('Generating Kafka listener...').start();
78
+
79
+ try {
80
+ const listenerPath = path.join(
81
+ projectDir,
82
+ 'src',
83
+ 'main',
84
+ 'java',
85
+ packagePath,
86
+ moduleName,
87
+ 'infrastructure',
88
+ 'kafkaListener',
89
+ 'KafkaController.java'
90
+ );
91
+
92
+ const listenerExists = await fs.pathExists(listenerPath);
93
+
94
+ if (!listenerExists) {
95
+ // First time: Create new KafkaController class with first topic
96
+ spinner.text = 'Creating KafkaController class...';
97
+
98
+ const firstTopic = selectedTopics[0];
99
+ const firstContext = buildTopicContext(packageName, moduleName, firstTopic, topics);
100
+
101
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'KafkaController.java.ejs');
102
+ await renderAndWrite(templatePath, listenerPath, firstContext);
103
+
104
+ // Add remaining topics as additional methods
105
+ for (let i = 1; i < selectedTopics.length; i++) {
106
+ spinner.text = `Adding listener for ${selectedTopics[i]}...`;
107
+ const topicContext = buildTopicContext(packageName, moduleName, selectedTopics[i], topics);
108
+ await addListenerMethod(listenerPath, topicContext);
109
+ }
110
+
111
+ } else {
112
+ // Update existing KafkaController class
113
+ spinner.text = 'Updating existing KafkaController class...';
114
+
115
+ for (const topicKey of selectedTopics) {
116
+ const topicContext = buildTopicContext(packageName, moduleName, topicKey, topics);
117
+ await addListenerMethod(listenerPath, topicContext);
118
+ }
119
+ }
120
+
121
+ spinner.succeed(chalk.green(`✨ Kafka listener ${listenerExists ? 'updated' : 'generated'} successfully!`));
122
+
123
+ // Display generated components
124
+ console.log(chalk.blue('\nšŸ“¦ Generated/Updated components:'));
125
+ console.log(chalk.gray(` └── ${moduleName}/infrastructure/kafkaListener/KafkaController.java`));
126
+
127
+ console.log(chalk.blue('\nšŸ“ Listener methods added:'));
128
+ selectedTopics.forEach(topic => {
129
+ const methodName = generateMethodName(topic);
130
+ console.log(chalk.gray(` ā”œā”€ā”€ ${methodName}() - listening to topic: ${topic}`));
131
+ });
132
+
133
+ console.log(chalk.yellow('\nāš ļø Next steps:'));
134
+ console.log(chalk.gray(' 1. Implement event processing logic in listener methods'));
135
+ console.log(chalk.gray(' 2. Consider creating use cases to handle events via UseCaseMediator'));
136
+ console.log(chalk.gray(' 3. Test your listeners with Kafka producer'));
137
+
138
+ } catch (error) {
139
+ spinner.fail(chalk.red('Failed to generate Kafka listener'));
140
+ console.error(chalk.red('\nāŒ Error:'), error.message);
141
+ if (process.env.DEBUG) {
142
+ console.error(error.stack);
143
+ }
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Read available topics from kafka.yml
150
+ */
151
+ async function getAvailableTopics(projectDir) {
152
+ const kafkaYmlPath = path.join(projectDir, 'src', 'main', 'resources', 'parameters', 'local', 'kafka.yml');
153
+
154
+ if (!(await fs.pathExists(kafkaYmlPath))) {
155
+ return [];
156
+ }
157
+
158
+ const kafkaContent = await fs.readFile(kafkaYmlPath, 'utf8');
159
+ const kafkaConfig = yaml.load(kafkaContent);
160
+
161
+ if (!kafkaConfig.topics || Object.keys(kafkaConfig.topics).length === 0) {
162
+ return [];
163
+ }
164
+
165
+ return Object.entries(kafkaConfig.topics).map(([key, value]) => ({
166
+ key,
167
+ value
168
+ }));
169
+ }
170
+
171
+ /**
172
+ * Build context object for a topic
173
+ */
174
+ function buildTopicContext(packageName, moduleName, topicKey, allTopics) {
175
+ const topic = allTopics.find(t => t.key === topicKey);
176
+ const topicValue = topic.value;
177
+
178
+ // Generate method name: user-created → handleUserCreatedListener
179
+ const methodName = generateMethodName(topicKey);
180
+
181
+ // Generate variable name: user-created → usercreatedTopic
182
+ const topicVariableName = toCamelCase(topicKey.replace(/-/g, ''));
183
+
184
+ return {
185
+ packageName,
186
+ moduleName,
187
+ topicNameKebab: topicKey,
188
+ topicValue,
189
+ topicSpringProperty: `\${topics.${topicKey}}`,
190
+ topicVariableName,
191
+ methodName
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Generate method name from topic key
197
+ * Example: user-created → handleUserCreatedListener
198
+ */
199
+ function generateMethodName(topicKey) {
200
+ const pascalCase = toPascalCase(topicKey);
201
+ return `handle${pascalCase}Listener`;
202
+ }
203
+
204
+ /**
205
+ * Add a listener method to existing KafkaListener class
206
+ */
207
+ async function addListenerMethod(listenerPath, context) {
208
+ let content = await fs.readFile(listenerPath, 'utf-8');
209
+
210
+ // Check if method already exists
211
+ if (content.includes(`void ${context.methodName}(`)) {
212
+ console.log(chalk.yellow(` ⚠ Method ${context.methodName}() already exists, skipping...`));
213
+ return;
214
+ }
215
+
216
+ // Add @Value field if not exists
217
+ if (!content.includes(`private String ${context.topicVariableName}Topic;`)) {
218
+ content = await addValueField(content, context);
219
+ }
220
+
221
+ // Add listener method before closing brace
222
+ const methodTemplatePath = path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerMethod.java.ejs');
223
+ const methodContent = await renderTemplate(methodTemplatePath, context);
224
+
225
+ // Find last closing brace
226
+ const lastBraceIndex = content.lastIndexOf('}');
227
+ if (lastBraceIndex === -1) {
228
+ throw new Error('Could not find closing brace in KafkaController class');
229
+ }
230
+
231
+ // Insert method before closing brace
232
+ content = content.slice(0, lastBraceIndex) + methodContent + '\n}\n';
233
+
234
+ await fs.writeFile(listenerPath, content, 'utf-8');
235
+ }
236
+
237
+ /**
238
+ * Add @Value field to KafkaController class
239
+ */
240
+ async function addValueField(content, context) {
241
+ const valueFieldTemplatePath = path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ValueField.java.ejs');
242
+ const valueFieldContent = await renderTemplate(valueFieldTemplatePath, context);
243
+
244
+ // Find existing @Value fields
245
+ const valueFieldPattern = /(@Value\([^)]+\)\s*\n\s*private\s+String\s+\w+Topic;\s*\n)/g;
246
+ const valueFields = [...content.matchAll(valueFieldPattern)];
247
+
248
+ if (valueFields.length > 0) {
249
+ // Add after last @Value field
250
+ const lastField = valueFields[valueFields.length - 1];
251
+ const insertPos = lastField.index + lastField[0].length;
252
+ return content.slice(0, insertPos) + valueFieldContent + content.slice(insertPos);
253
+ } else {
254
+ // Add after UseCaseMediator field declaration
255
+ const fieldPattern = /(private\s+final\s+UseCaseMediator\s+useCaseMediator;\s*\n)/;
256
+ const fieldMatch = content.match(fieldPattern);
257
+
258
+ if (fieldMatch) {
259
+ const insertPos = fieldMatch.index + fieldMatch[0].length;
260
+ return content.slice(0, insertPos) + '\n' + valueFieldContent + content.slice(insertPos);
261
+ }
262
+ }
263
+
264
+ return content;
265
+ }
266
+
267
+ module.exports = generateKafkaListenerCommand;
@@ -0,0 +1,265 @@
1
+ const inquirer = require('inquirer');
2
+ const ora = require('ora');
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const ConfigManager = require('../utils/config-manager');
7
+ const { isEva4jProject } = require('../utils/validator');
8
+ const { toPackagePath, toPascalCase, pluralizeWord, toKebabCase } = require('../utils/naming');
9
+ const { renderAndWrite } = require('../utils/template-engine');
10
+
11
+ /**
12
+ * Generate REST resource with CRUD operations and corresponding use cases
13
+ * @param {string} moduleName - Name of the module
14
+ */
15
+ async function generateResourceCommand(moduleName) {
16
+ const projectDir = process.cwd();
17
+
18
+ // Validate eva4j project
19
+ if (!(await isEva4jProject(projectDir))) {
20
+ console.error(chalk.red('āŒ Not in an eva4j project directory'));
21
+ console.error(chalk.gray(' Run this command from the root of an eva4j project'));
22
+ process.exit(1);
23
+ }
24
+
25
+ // Load project configuration
26
+ const configManager = new ConfigManager(projectDir);
27
+ const projectConfig = await configManager.loadProjectConfig();
28
+ const { packageName } = projectConfig;
29
+ const packagePath = toPackagePath(packageName);
30
+
31
+ // Validate module exists
32
+ if (!(await configManager.moduleExists(moduleName))) {
33
+ console.error(chalk.red(`āŒ Module '${moduleName}' not found in project`));
34
+ console.error(chalk.gray(' Available modules:'));
35
+ const modules = projectConfig.modules || [];
36
+ modules.forEach(mod => console.error(chalk.gray(` - ${mod}`)));
37
+ process.exit(1);
38
+ }
39
+
40
+ // Prompt for resource name
41
+ const { resourceName } = await inquirer.prompt([
42
+ {
43
+ type: 'input',
44
+ name: 'resourceName',
45
+ message: 'Enter the resource name (e.g., user, product, order-item):',
46
+ default: moduleName,
47
+ validate: (input) => {
48
+ if (!input || input.trim().length === 0) {
49
+ return 'Resource name is required';
50
+ }
51
+ if (!/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(input.trim())) {
52
+ return 'Resource name must contain only letters, numbers, hyphens, and underscores';
53
+ }
54
+ return true;
55
+ },
56
+ filter: (input) => toPascalCase(input.trim())
57
+ }
58
+ ]);
59
+
60
+ // Prompt for API version
61
+ const { apiVersion } = await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'apiVersion',
65
+ message: 'Enter the API version (e.g., v1, v2):',
66
+ default: 'v1',
67
+ validate: (input) => {
68
+ if (!input || input.trim().length === 0) {
69
+ return 'API version is required';
70
+ }
71
+ if (!/^v\d+$/.test(input.trim())) {
72
+ return 'API version must follow the pattern v1, v2, v3, etc.';
73
+ }
74
+ return true;
75
+ },
76
+ filter: (input) => input.trim().toLowerCase()
77
+ }
78
+ ]);
79
+
80
+ const spinner = ora('Generating resource...').start();
81
+
82
+ try {
83
+ const resourceNameKebab = toKebabCase(resourceName);
84
+ const resourceNameCamel = resourceName.charAt(0).toLowerCase() + resourceName.slice(1);
85
+ const resourceNamePlural = pluralizeWord(resourceName);
86
+
87
+ // Check if controller already exists
88
+ const controllerPath = path.join(
89
+ projectDir,
90
+ 'src',
91
+ 'main',
92
+ 'java',
93
+ packagePath,
94
+ moduleName,
95
+ 'infrastructure',
96
+ 'rest',
97
+ 'controllers',
98
+ resourceNameCamel,
99
+ apiVersion,
100
+ `${resourceName}Controller.java`
101
+ );
102
+
103
+ if (await fs.pathExists(controllerPath)) {
104
+ spinner.fail(chalk.red('Resource already exists'));
105
+ console.error(chalk.red(`\nāŒ Resource already exists at:`));
106
+ console.error(chalk.gray(` ${moduleName}/infrastructure/rest/controllers/${resourceNameCamel}/${apiVersion}/${resourceName}Controller.java`));
107
+ process.exit(1);
108
+ }
109
+
110
+ // Define use cases to generate
111
+ const useCases = [
112
+ {
113
+ name: `Create${resourceName}`,
114
+ type: 'command',
115
+ description: 'Create new resource',
116
+ hasId: false
117
+ },
118
+ {
119
+ name: `Update${resourceName}`,
120
+ type: 'command',
121
+ description: 'Update existing resource',
122
+ hasId: true
123
+ },
124
+ {
125
+ name: `Delete${resourceName}`,
126
+ type: 'command',
127
+ description: 'Delete resource',
128
+ hasId: true
129
+ },
130
+ {
131
+ name: `Find${resourceName}ById`,
132
+ type: 'query',
133
+ description: 'Find resource by ID',
134
+ hasId: true
135
+ },
136
+ {
137
+ name: `FindAll${resourceNamePlural}`,
138
+ type: 'query',
139
+ description: 'Find all resources',
140
+ hasId: false
141
+ }
142
+ ];
143
+
144
+ const moduleBasePath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName);
145
+
146
+ // Generate single ResponseDto for the resource
147
+ spinner.text = 'Generating Response DTO...';
148
+ const dtoPath = path.join(moduleBasePath, 'application', 'dtos', `${resourceName}ResponseDto.java`);
149
+ if (!fs.existsSync(dtoPath)) {
150
+ const dtoContext = {
151
+ packageName,
152
+ moduleName,
153
+ resourceName
154
+ };
155
+ const dtoTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'ResponseDto.java.ejs');
156
+ await renderAndWrite(dtoTemplatePath, dtoPath, dtoContext);
157
+ }
158
+
159
+ // Generate use cases
160
+ for (const useCase of useCases) {
161
+ spinner.text = `Generating ${useCase.name}...`;
162
+
163
+ const context = {
164
+ packageName,
165
+ moduleName,
166
+ resourceName,
167
+ usecaseName: useCase.name,
168
+ hasId: useCase.hasId || false,
169
+ isFindAll: require('../utils/naming').isAllTypeQuery(useCase.name)
170
+ };
171
+
172
+ if (useCase.type === 'command') {
173
+ // Generate Command
174
+ const commandPath = path.join(moduleBasePath, 'application', 'commands', `${useCase.name}Command.java`);
175
+ const commandTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'Command.java.ejs');
176
+ await renderAndWrite(commandTemplatePath, commandPath, context);
177
+
178
+ // Generate CommandHandler
179
+ const handlerPath = path.join(moduleBasePath, 'application', 'usecases', `${useCase.name}CommandHandler.java`);
180
+ const handlerTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'CommandHandler.java.ejs');
181
+ await renderAndWrite(handlerTemplatePath, handlerPath, context);
182
+
183
+ } else if (useCase.type === 'query') {
184
+ // Generate Query
185
+ const queryPath = path.join(moduleBasePath, 'application', 'queries', `${useCase.name}Query.java`);
186
+ const queryTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'Query.java.ejs');
187
+ await renderAndWrite(queryTemplatePath, queryPath, context);
188
+
189
+ // Generate QueryHandler
190
+ const handlerPath = path.join(moduleBasePath, 'application', 'usecases', `${useCase.name}QueryHandler.java`);
191
+ const handlerTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'QueryHandler.java.ejs');
192
+ await renderAndWrite(handlerTemplatePath, handlerPath, context);
193
+ }
194
+ }
195
+
196
+ // Generate Controller
197
+ spinner.text = 'Generating REST controller...';
198
+ const controllerContext = {
199
+ packageName,
200
+ moduleName,
201
+ resourceName,
202
+ resourceNamePlural,
203
+ resourceNameKebab,
204
+ apiVersion,
205
+ resourceNameCamel
206
+ };
207
+
208
+ const controllerTemplatePath = path.join(__dirname, '..', '..', 'templates', 'resource', 'Controller.java.ejs');
209
+ await renderAndWrite(controllerTemplatePath, controllerPath, controllerContext);
210
+
211
+ spinner.succeed(chalk.green('✨ Resource generated successfully!'));
212
+
213
+ // Display generated components
214
+ console.log(chalk.blue('\nšŸ“¦ Generated files:'));
215
+ console.log(chalk.gray(` └── ${moduleName}/`));
216
+ console.log(chalk.gray(' ā”œā”€ā”€ application/'));
217
+ console.log(chalk.gray(' │ ā”œā”€ā”€ commands/'));
218
+ useCases.filter(u => u.type === 'command').forEach(u => {
219
+ console.log(chalk.gray(` │ │ └── ${u.name}Command.java`));
220
+ });
221
+ console.log(chalk.gray(' │ ā”œā”€ā”€ queries/'));
222
+ useCases.filter(u => u.type === 'query').forEach(u => {
223
+ console.log(chalk.gray(` │ │ └── ${u.name}Query.java`));
224
+ });
225
+ console.log(chalk.gray(' │ ā”œā”€ā”€ usecases/'));
226
+ useCases.forEach(u => {
227
+ const suffix = u.type === 'command' ? 'CommandHandler' : 'QueryHandler';
228
+ console.log(chalk.gray(` │ │ └── ${u.name}${suffix}.java`));
229
+ });
230
+ console.log(chalk.gray(' │ └── dtos/'));
231
+ useCases.filter(u => u.type === 'query').forEach(u => {
232
+ console.log(chalk.gray(` │ │ └── ${u.name}ResponseDto.java`));
233
+ });
234
+ console.log(chalk.gray(' └── infrastructure/'));
235
+ console.log(chalk.gray(' └── rest/'));
236
+ console.log(chalk.gray(' └── controllers/'));
237
+ console.log(chalk.gray(` └── ${resourceName}/`));
238
+ console.log(chalk.gray(` └── ${apiVersion}/`));
239
+ console.log(chalk.gray(` └── ${resourceName}Controller.java`));
240
+
241
+ console.log(chalk.blue('\nšŸ“ CRUD Endpoints:'));
242
+ console.log(chalk.gray(` POST /api/${apiVersion}/${resourceNameKebab} - Create ${resourceName}`));
243
+ console.log(chalk.gray(` GET /api/${apiVersion}/${resourceNameKebab}/{id} - Get ${resourceName} by ID`));
244
+ console.log(chalk.gray(` GET /api/${apiVersion}/${resourceNameKebab} - Get all ${resourceNamePlural}`));
245
+ console.log(chalk.gray(` PUT /api/${apiVersion}/${resourceNameKebab}/{id} - Update ${resourceName}`));
246
+ console.log(chalk.gray(` DELETE /api/${apiVersion}/${resourceNameKebab}/{id} - Delete ${resourceName}`));
247
+
248
+ console.log(chalk.yellow('\nāš ļø Next steps:'));
249
+ console.log(chalk.gray(' 1. Add fields to Command and Query records'));
250
+ console.log(chalk.gray(' 2. Implement business logic in handlers'));
251
+ console.log(chalk.gray(' 3. Add validation annotations to Commands'));
252
+ console.log(chalk.gray(' 4. Define response fields in ResponseDtos'));
253
+ console.log(chalk.gray(' 5. Inject dependencies in handlers'));
254
+
255
+ } catch (error) {
256
+ spinner.fail(chalk.red('Failed to generate resource'));
257
+ console.error(chalk.red('\nāŒ Error:'), error.message);
258
+ if (process.env.DEBUG) {
259
+ console.error(error.stack);
260
+ }
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ module.exports = generateResourceCommand;