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.
- package/LICENSE +21 -0
- package/QUICK_REFERENCE.md +204 -0
- package/README.md +912 -0
- package/USAGE.md +349 -0
- package/bin/eva4j.js +234 -0
- package/config/defaults.json +46 -0
- package/package.json +57 -0
- package/src/commands/add-kafka-client.js +193 -0
- package/src/commands/add-module.js +221 -0
- package/src/commands/create.js +92 -0
- package/src/commands/detach.js +495 -0
- package/src/commands/generate-http-exchange.js +309 -0
- package/src/commands/generate-kafka-event.js +453 -0
- package/src/commands/generate-kafka-listener.js +267 -0
- package/src/commands/generate-resource.js +265 -0
- package/src/commands/generate-usecase.js +198 -0
- package/src/commands/info.js +63 -0
- package/src/generators/base-generator.js +150 -0
- package/src/generators/module-generator.js +48 -0
- package/src/generators/shared-generator.js +153 -0
- package/src/utils/config-manager.js +156 -0
- package/src/utils/context-builder.js +149 -0
- package/src/utils/naming.js +137 -0
- package/src/utils/template-engine.js +55 -0
- package/src/utils/validator.js +159 -0
- package/templates/base/application/Application.java.ejs +27 -0
- package/templates/base/docker/docker-compose.yml.ejs +41 -0
- package/templates/base/gradle/build.gradle.ejs +70 -0
- package/templates/base/gradle/settings.gradle.ejs +1 -0
- package/templates/base/resources/application-develop.yml.ejs +5 -0
- package/templates/base/resources/application-local.yml.ejs +5 -0
- package/templates/base/resources/application-production.yml.ejs +9 -0
- package/templates/base/resources/application-test.yml.ejs +5 -0
- package/templates/base/resources/application.yml.ejs +31 -0
- package/templates/base/resources/parameters/develop/cors.yml.ejs +4 -0
- package/templates/base/resources/parameters/develop/db.yaml.ejs +21 -0
- package/templates/base/resources/parameters/develop/kafka.yml.ejs +26 -0
- package/templates/base/resources/parameters/local/cors.yml.ejs +4 -0
- package/templates/base/resources/parameters/local/db.yaml.ejs +21 -0
- package/templates/base/resources/parameters/local/kafka.yml.ejs +26 -0
- package/templates/base/resources/parameters/production/cors.yml.ejs +4 -0
- package/templates/base/resources/parameters/production/db.yaml.ejs +21 -0
- package/templates/base/resources/parameters/production/kafka.yml.ejs +26 -0
- package/templates/base/root/README.md.ejs +126 -0
- package/templates/base/root/gitignore.ejs +42 -0
- package/templates/http-exchange/Adapter.java.ejs +39 -0
- package/templates/http-exchange/Config.java.ejs +24 -0
- package/templates/http-exchange/FeignClient.java.ejs +23 -0
- package/templates/http-exchange/Port.java.ejs +14 -0
- package/templates/kafka-event/Event.java.ejs +10 -0
- package/templates/kafka-event/KafkaConfigBean.java.ejs +7 -0
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +32 -0
- package/templates/kafka-event/MessageBroker.java.ejs +8 -0
- package/templates/kafka-event/MessageBrokerImplMethod.java.ejs +9 -0
- package/templates/kafka-event/MessageBrokerMethod.java.ejs +1 -0
- package/templates/kafka-listener/KafkaController.java.ejs +34 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +9 -0
- package/templates/kafka-listener/ValueField.java.ejs +4 -0
- package/templates/module/controller.java.ejs +96 -0
- package/templates/module/exception.java.ejs +18 -0
- package/templates/module/mapper.java.ejs +67 -0
- package/templates/module/model.java.ejs +29 -0
- package/templates/module/package-info.java.ejs +7 -0
- package/templates/module/repository.java.ejs +26 -0
- package/templates/module/request-dto.java.ejs +24 -0
- package/templates/module/response-dto.java.ejs +26 -0
- package/templates/module/service-impl.java.ejs +112 -0
- package/templates/module/service.java.ejs +45 -0
- package/templates/module/update-dto.java.ejs +25 -0
- package/templates/resource/Command.java.ejs +9 -0
- package/templates/resource/CommandHandler.java.ejs +22 -0
- package/templates/resource/Controller.java.ejs +73 -0
- package/templates/resource/Query.java.ejs +12 -0
- package/templates/resource/QueryHandler.java.ejs +31 -0
- package/templates/resource/ResponseDto.java.ejs +6 -0
- package/templates/shared/annotations/ApplicationComponent.java.ejs +9 -0
- package/templates/shared/annotations/DomainComponent.java.ejs +9 -0
- package/templates/shared/annotations/LogAfter.java.ejs +11 -0
- package/templates/shared/annotations/LogBefore.java.ejs +11 -0
- package/templates/shared/annotations/LogExceptions.java.ejs +11 -0
- package/templates/shared/annotations/LogTimer.java.ejs +11 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +49 -0
- package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +56 -0
- package/templates/shared/configurations/securityConfig/SecurityConfig.java.ejs +57 -0
- package/templates/shared/configurations/swaggerConfig/SwaggerConfig.java.ejs +31 -0
- package/templates/shared/configurations/useCaseConfig/UseCaseAutoRegister.java.ejs +51 -0
- package/templates/shared/configurations/useCaseConfig/UseCaseConfig.java.ejs +25 -0
- package/templates/shared/configurations/useCaseConfig/UseCaseContainer.java.ejs +29 -0
- package/templates/shared/configurations/useCaseConfig/UseCaseMediator.java.ejs +38 -0
- package/templates/shared/customExceptions/BadRequestException.java.ejs +11 -0
- package/templates/shared/customExceptions/ConflictException.java.ejs +8 -0
- package/templates/shared/customExceptions/ForbiddenException.java.ejs +8 -0
- package/templates/shared/customExceptions/ImportFileException.java.ejs +6 -0
- package/templates/shared/customExceptions/NotFoundException.java.ejs +8 -0
- package/templates/shared/customExceptions/UnauthorizedException.java.ejs +9 -0
- package/templates/shared/customExceptions/ValidationException.java.ejs +17 -0
- package/templates/shared/errorMessage/ErrorMessage.java.ejs +5 -0
- package/templates/shared/errorMessage/FullErrorMessage.java.ejs +9 -0
- package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +6 -0
- package/templates/shared/eventEnvelope/EventEnvelope.java.ejs +13 -0
- package/templates/shared/eventEnvelope/EventMetadata.java.ejs +24 -0
- package/templates/shared/filters/CorrelationIdFilter.java.ejs +45 -0
- package/templates/shared/handlerException/HandlerExceptions.java.ejs +148 -0
- package/templates/shared/interfaces/Command.java.ejs +4 -0
- package/templates/shared/interfaces/CommandHandler.java.ejs +5 -0
- package/templates/shared/interfaces/Dispatchable.java.ejs +4 -0
- package/templates/shared/interfaces/Handler.java.ejs +4 -0
- package/templates/shared/interfaces/Query.java.ejs +4 -0
- package/templates/shared/interfaces/QueryHandler.java.ejs +5 -0
- package/templates/shared/package-info.java.ejs +8 -0
- package/templates/usecase/command/Command.java.ejs +7 -0
- package/templates/usecase/command/CommandHandler.java.ejs +21 -0
- package/templates/usecase/query/Query.java.ejs +10 -0
- package/templates/usecase/query/QueryHandler.java.ejs +22 -0
- 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;
|