katax-cli 0.1.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/README.md +0 -0
- package/dist/commands/add-endpoint.d.ts +6 -0
- package/dist/commands/add-endpoint.js +229 -0
- package/dist/commands/generate-crud.d.ts +5 -0
- package/dist/commands/generate-crud.js +282 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +80 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +1315 -0
- package/dist/database/mongodb.ts +46 -0
- package/dist/database/mysql.ts +26 -0
- package/dist/database/postgresql.ts +52 -0
- package/dist/generators/controller-generator.d.ts +2 -0
- package/dist/generators/controller-generator.js +223 -0
- package/dist/generators/handler-generator.d.ts +2 -0
- package/dist/generators/handler-generator.js +84 -0
- package/dist/generators/route-generator.d.ts +2 -0
- package/dist/generators/route-generator.js +50 -0
- package/dist/generators/router-updater.d.ts +2 -0
- package/dist/generators/router-updater.js +48 -0
- package/dist/generators/validator-generator.d.ts +2 -0
- package/dist/generators/validator-generator.js +160 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +101 -0
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/file-utils.d.ts +40 -0
- package/dist/utils/file-utils.js +89 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +38 -0
- package/package.json +68 -0
package/README.md
ADDED
|
Binary file
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { success, error, warning, gray, title, info } from '../utils/logger.js';
|
|
5
|
+
import { fileExists, writeFile } from '../utils/file-utils.js';
|
|
6
|
+
import { generateValidator } from '../generators/validator-generator.js';
|
|
7
|
+
import { generateController } from '../generators/controller-generator.js';
|
|
8
|
+
import { generateHandler } from '../generators/handler-generator.js';
|
|
9
|
+
import { generateRoute } from '../generators/route-generator.js';
|
|
10
|
+
import { updateMainRouter } from '../generators/router-updater.js';
|
|
11
|
+
export async function addEndpointCommand(name, options = {}) {
|
|
12
|
+
title(`🎯 Add Endpoint: ${name}`);
|
|
13
|
+
// Check if we're in a Katax project
|
|
14
|
+
if (!fileExists(path.join(process.cwd(), 'package.json'))) {
|
|
15
|
+
error('Not in a project directory!');
|
|
16
|
+
gray('Run this command from your project root\n');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
// Check if katax-core is installed
|
|
20
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
21
|
+
const packageJson = JSON.parse(await import('fs').then(fs => fs.promises.readFile(packageJsonPath, 'utf-8')));
|
|
22
|
+
const hasKataxCore = packageJson.dependencies?.['katax-core'];
|
|
23
|
+
if (!hasKataxCore) {
|
|
24
|
+
warning('katax-core is not installed in this project');
|
|
25
|
+
const { install } = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'confirm',
|
|
28
|
+
name: 'install',
|
|
29
|
+
message: 'Would you like to install katax-core?',
|
|
30
|
+
default: true
|
|
31
|
+
}
|
|
32
|
+
]);
|
|
33
|
+
if (!install) {
|
|
34
|
+
error('Cannot create endpoint without validation library');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// Install katax-core
|
|
38
|
+
const spinner = ora('Installing katax-core...').start();
|
|
39
|
+
const { execa } = await import('execa');
|
|
40
|
+
await execa('npm', ['install', 'katax-core'], { cwd: process.cwd() });
|
|
41
|
+
spinner.succeed('katax-core installed');
|
|
42
|
+
}
|
|
43
|
+
// Interactive prompts
|
|
44
|
+
const answers = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'list',
|
|
47
|
+
name: 'method',
|
|
48
|
+
message: 'HTTP Method:',
|
|
49
|
+
choices: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
50
|
+
default: options.method?.toUpperCase() || 'POST',
|
|
51
|
+
when: !options.method
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'path',
|
|
56
|
+
message: 'Route path:',
|
|
57
|
+
default: `/api/${name.toLowerCase()}`,
|
|
58
|
+
when: !options.path,
|
|
59
|
+
validate: (input) => {
|
|
60
|
+
if (!input.startsWith('/')) {
|
|
61
|
+
return 'Path must start with /';
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'confirm',
|
|
68
|
+
name: 'addValidation',
|
|
69
|
+
message: 'Add validation?',
|
|
70
|
+
default: true
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
const config = {
|
|
74
|
+
name,
|
|
75
|
+
method: (options.method?.toUpperCase() || answers.method),
|
|
76
|
+
path: options.path || answers.path,
|
|
77
|
+
addValidation: answers.addValidation,
|
|
78
|
+
fields: [],
|
|
79
|
+
addAsyncValidators: false,
|
|
80
|
+
dbOperations: []
|
|
81
|
+
};
|
|
82
|
+
// If validation is enabled, collect fields
|
|
83
|
+
if (config.addValidation && config.method !== 'GET' && config.method !== 'DELETE') {
|
|
84
|
+
info('\nDefine request body fields:');
|
|
85
|
+
let addingFields = true;
|
|
86
|
+
while (addingFields) {
|
|
87
|
+
const fieldAnswers = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'input',
|
|
90
|
+
name: 'fieldName',
|
|
91
|
+
message: 'Field name (press Enter to finish):',
|
|
92
|
+
validate: (input) => {
|
|
93
|
+
if (input && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) {
|
|
94
|
+
return 'Field name must be a valid identifier';
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
if (!fieldAnswers.fieldName) {
|
|
101
|
+
addingFields = false;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const fieldConfig = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'list',
|
|
107
|
+
name: 'type',
|
|
108
|
+
message: `Type for "${fieldAnswers.fieldName}":`,
|
|
109
|
+
choices: ['string', 'number', 'boolean', 'date', 'email', 'array', 'object']
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
name: 'required',
|
|
114
|
+
message: 'Required?',
|
|
115
|
+
default: true
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
const field = {
|
|
119
|
+
name: fieldAnswers.fieldName,
|
|
120
|
+
type: fieldConfig.type,
|
|
121
|
+
required: fieldConfig.required,
|
|
122
|
+
rules: []
|
|
123
|
+
};
|
|
124
|
+
// Add type-specific rules
|
|
125
|
+
if (fieldConfig.type === 'string' || fieldConfig.type === 'email') {
|
|
126
|
+
const stringRules = await inquirer.prompt([
|
|
127
|
+
{
|
|
128
|
+
type: 'input',
|
|
129
|
+
name: 'minLength',
|
|
130
|
+
message: 'Minimum length (press Enter to skip):',
|
|
131
|
+
validate: (input) => !input || !isNaN(parseInt(input)) || 'Must be a number'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'input',
|
|
135
|
+
name: 'maxLength',
|
|
136
|
+
message: 'Maximum length (press Enter to skip):',
|
|
137
|
+
validate: (input) => !input || !isNaN(parseInt(input)) || 'Must be a number'
|
|
138
|
+
}
|
|
139
|
+
]);
|
|
140
|
+
if (stringRules.minLength) {
|
|
141
|
+
field.rules.push({ type: 'minLength', value: parseInt(stringRules.minLength) });
|
|
142
|
+
}
|
|
143
|
+
if (stringRules.maxLength) {
|
|
144
|
+
field.rules.push({ type: 'maxLength', value: parseInt(stringRules.maxLength) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (fieldConfig.type === 'number') {
|
|
148
|
+
const numberRules = await inquirer.prompt([
|
|
149
|
+
{
|
|
150
|
+
type: 'input',
|
|
151
|
+
name: 'min',
|
|
152
|
+
message: 'Minimum value (press Enter to skip):',
|
|
153
|
+
validate: (input) => !input || !isNaN(parseFloat(input)) || 'Must be a number'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'input',
|
|
157
|
+
name: 'max',
|
|
158
|
+
message: 'Maximum value (press Enter to skip):',
|
|
159
|
+
validate: (input) => !input || !isNaN(parseFloat(input)) || 'Must be a number'
|
|
160
|
+
}
|
|
161
|
+
]);
|
|
162
|
+
if (numberRules.min) {
|
|
163
|
+
field.rules.push({ type: 'min', value: parseFloat(numberRules.min) });
|
|
164
|
+
}
|
|
165
|
+
if (numberRules.max) {
|
|
166
|
+
field.rules.push({ type: 'max', value: parseFloat(numberRules.max) });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
config.fields.push(field);
|
|
170
|
+
gray(` ✓ Added field: ${field.name} (${field.type})`);
|
|
171
|
+
}
|
|
172
|
+
if (config.fields.length > 0) {
|
|
173
|
+
const { addAsync } = await inquirer.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'confirm',
|
|
176
|
+
name: 'addAsync',
|
|
177
|
+
message: 'Add async validators (e.g., unique email)?',
|
|
178
|
+
default: false
|
|
179
|
+
}
|
|
180
|
+
]);
|
|
181
|
+
config.addAsyncValidators = addAsync;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Generate files
|
|
185
|
+
const spinner = ora('Generating endpoint files...').start();
|
|
186
|
+
try {
|
|
187
|
+
const basePath = path.join(process.cwd(), 'src', 'api', name.toLowerCase());
|
|
188
|
+
// Generate validator
|
|
189
|
+
if (config.addValidation) {
|
|
190
|
+
const validatorPath = path.join(basePath, `${name.toLowerCase()}.validator.ts`);
|
|
191
|
+
const validatorContent = generateValidator(config);
|
|
192
|
+
await writeFile(validatorPath, validatorContent);
|
|
193
|
+
spinner.text = `Generated ${name.toLowerCase()}.validator.ts`;
|
|
194
|
+
}
|
|
195
|
+
// Generate controller
|
|
196
|
+
const controllerPath = path.join(basePath, `${name.toLowerCase()}.controller.ts`);
|
|
197
|
+
const controllerContent = generateController(config);
|
|
198
|
+
await writeFile(controllerPath, controllerContent);
|
|
199
|
+
spinner.text = `Generated ${name.toLowerCase()}.controller.ts`;
|
|
200
|
+
// Generate handler
|
|
201
|
+
const handlerPath = path.join(basePath, `${name.toLowerCase()}.handler.ts`);
|
|
202
|
+
const handlerContent = generateHandler(config);
|
|
203
|
+
await writeFile(handlerPath, handlerContent);
|
|
204
|
+
spinner.text = `Generated ${name.toLowerCase()}.handler.ts`;
|
|
205
|
+
// Generate route
|
|
206
|
+
const routePath = path.join(basePath, `${name.toLowerCase()}.routes.ts`);
|
|
207
|
+
const routeContent = generateRoute(config);
|
|
208
|
+
await writeFile(routePath, routeContent);
|
|
209
|
+
spinner.text = `Generated ${name.toLowerCase()}.routes.ts`;
|
|
210
|
+
// Update main router
|
|
211
|
+
await updateMainRouter(name, config);
|
|
212
|
+
spinner.succeed('Endpoint files generated');
|
|
213
|
+
success(`\n✨ Endpoint "${name}" created successfully!\n`);
|
|
214
|
+
info('Generated files:');
|
|
215
|
+
gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.validator.ts`);
|
|
216
|
+
gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.controller.ts`);
|
|
217
|
+
gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.handler.ts`);
|
|
218
|
+
gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.routes.ts`);
|
|
219
|
+
gray(` Updated: src/api/routes.ts\n`);
|
|
220
|
+
gray(` Updated: src/api/routes.ts\n`);
|
|
221
|
+
info('Test your endpoint:');
|
|
222
|
+
gray(` ${config.method} ${config.path}\n`);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
spinner.fail('Failed to generate endpoint');
|
|
226
|
+
error(err instanceof Error ? err.message : 'Unknown error');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { success, error, warning, gray, title, info } from '../utils/logger.js';
|
|
5
|
+
import { fileExists, writeFile, toPascalCase, toCamelCase } from '../utils/file-utils.js';
|
|
6
|
+
export async function generateCrudCommand(resourceName, options = {}) {
|
|
7
|
+
title(`🔧 Generate CRUD: ${resourceName}`);
|
|
8
|
+
// Check if we're in a project
|
|
9
|
+
if (!fileExists(path.join(process.cwd(), 'package.json'))) {
|
|
10
|
+
error('Not in a project directory!');
|
|
11
|
+
gray('Run this command from your project root\n');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// Interactive prompts
|
|
15
|
+
const answers = await inquirer.prompt([
|
|
16
|
+
{
|
|
17
|
+
type: 'input',
|
|
18
|
+
name: 'tableName',
|
|
19
|
+
message: 'Database table name:',
|
|
20
|
+
default: `${resourceName.toLowerCase()}s`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'addAuth',
|
|
25
|
+
message: 'Require authentication for endpoints?',
|
|
26
|
+
default: options.auth !== false
|
|
27
|
+
}
|
|
28
|
+
]);
|
|
29
|
+
info('\nDefine fields for the resource:');
|
|
30
|
+
const fields = [];
|
|
31
|
+
let addingFields = true;
|
|
32
|
+
while (addingFields) {
|
|
33
|
+
const fieldAnswers = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'fieldName',
|
|
37
|
+
message: 'Field name (press Enter to finish):',
|
|
38
|
+
validate: (input) => {
|
|
39
|
+
if (input && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) {
|
|
40
|
+
return 'Field name must be a valid identifier';
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
if (!fieldAnswers.fieldName) {
|
|
47
|
+
addingFields = false;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const fieldConfig = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'list',
|
|
53
|
+
name: 'type',
|
|
54
|
+
message: `Type for "${fieldAnswers.fieldName}":`,
|
|
55
|
+
choices: ['string', 'number', 'boolean', 'date', 'email']
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'confirm',
|
|
59
|
+
name: 'required',
|
|
60
|
+
message: 'Required?',
|
|
61
|
+
default: true
|
|
62
|
+
}
|
|
63
|
+
]);
|
|
64
|
+
fields.push({
|
|
65
|
+
name: fieldAnswers.fieldName,
|
|
66
|
+
type: fieldConfig.type,
|
|
67
|
+
required: fieldConfig.required,
|
|
68
|
+
rules: []
|
|
69
|
+
});
|
|
70
|
+
gray(` ✓ Added field: ${fieldAnswers.fieldName} (${fieldConfig.type})`);
|
|
71
|
+
}
|
|
72
|
+
if (fields.length === 0) {
|
|
73
|
+
error('At least one field is required to generate CRUD');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const config = {
|
|
77
|
+
resourceName,
|
|
78
|
+
tableName: answers.tableName,
|
|
79
|
+
fields,
|
|
80
|
+
addAuth: answers.addAuth
|
|
81
|
+
};
|
|
82
|
+
const spinner = ora('Generating CRUD endpoints...').start();
|
|
83
|
+
try {
|
|
84
|
+
await generateCRUDFiles(config);
|
|
85
|
+
spinner.succeed('CRUD endpoints generated');
|
|
86
|
+
success(`\n✨ CRUD for "${resourceName}" created successfully!\n`);
|
|
87
|
+
info('Generated endpoints:');
|
|
88
|
+
gray(` GET /api/${resourceName.toLowerCase()} - List all`);
|
|
89
|
+
gray(` GET /api/${resourceName.toLowerCase()}/:id - Get one`);
|
|
90
|
+
gray(` POST /api/${resourceName.toLowerCase()} - Create`);
|
|
91
|
+
gray(` PUT /api/${resourceName.toLowerCase()}/:id - Update`);
|
|
92
|
+
gray(` DELETE /api/${resourceName.toLowerCase()}/:id - Delete\n`);
|
|
93
|
+
if (config.addAuth) {
|
|
94
|
+
warning('Remember to implement authentication middleware!');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
spinner.fail('Failed to generate CRUD');
|
|
99
|
+
error(err instanceof Error ? err.message : 'Unknown error');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function generateCRUDFiles(config) {
|
|
104
|
+
const { resourceName, fields } = config;
|
|
105
|
+
const basePath = path.join(process.cwd(), 'src', 'api', resourceName.toLowerCase());
|
|
106
|
+
// Generate validator with all operations
|
|
107
|
+
const validatorContent = generateCRUDValidator(config);
|
|
108
|
+
await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.validator.ts`), validatorContent);
|
|
109
|
+
// Generate controller with CRUD operations
|
|
110
|
+
const controllerContent = generateCRUDController(config);
|
|
111
|
+
await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.controller.ts`), controllerContent);
|
|
112
|
+
// Generate routes with all CRUD endpoints
|
|
113
|
+
const routesContent = generateCRUDRoutes(config);
|
|
114
|
+
await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.routes.ts`), routesContent);
|
|
115
|
+
// Update main router
|
|
116
|
+
const { updateMainRouter } = await import('../generators/router-updater.js');
|
|
117
|
+
await updateMainRouter(resourceName, {
|
|
118
|
+
name: resourceName,
|
|
119
|
+
method: 'GET',
|
|
120
|
+
path: `/api/${resourceName.toLowerCase()}`,
|
|
121
|
+
addValidation: true,
|
|
122
|
+
fields,
|
|
123
|
+
addAsyncValidators: false
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function generateCRUDValidator(config) {
|
|
127
|
+
const { resourceName, fields } = config;
|
|
128
|
+
const pascalName = toPascalCase(resourceName);
|
|
129
|
+
const camelName = toCamelCase(resourceName);
|
|
130
|
+
let content = `import { k, kataxInfer } from 'katax-core';\n\n`;
|
|
131
|
+
content += `// ==================== SCHEMAS ====================\n\n`;
|
|
132
|
+
// Create schema
|
|
133
|
+
content += `export const create${pascalName}Schema = k.object({\n`;
|
|
134
|
+
fields.forEach((field, index) => {
|
|
135
|
+
const isLast = index === fields.length - 1;
|
|
136
|
+
let fieldSchema = field.type === 'email' ? 'k.string().email()' : `k.${field.type}()`;
|
|
137
|
+
if (!field.required)
|
|
138
|
+
fieldSchema += '.optional()';
|
|
139
|
+
content += ` ${field.name}: ${fieldSchema}${isLast ? '' : ','}\n`;
|
|
140
|
+
});
|
|
141
|
+
content += `});\n\n`;
|
|
142
|
+
// Update schema (all fields optional)
|
|
143
|
+
content += `export const update${pascalName}Schema = k.object({\n`;
|
|
144
|
+
fields.forEach((field, index) => {
|
|
145
|
+
const isLast = index === fields.length - 1;
|
|
146
|
+
const fieldSchema = field.type === 'email' ? 'k.string().email().optional()' : `k.${field.type}().optional()`;
|
|
147
|
+
content += ` ${field.name}: ${fieldSchema}${isLast ? '' : ','}\n`;
|
|
148
|
+
});
|
|
149
|
+
content += `});\n\n`;
|
|
150
|
+
content += `export type Create${pascalName}Data = kataxInfer<typeof create${pascalName}Schema>;\n`;
|
|
151
|
+
content += `export type Update${pascalName}Data = kataxInfer<typeof update${pascalName}Schema>;\n`;
|
|
152
|
+
return content;
|
|
153
|
+
}
|
|
154
|
+
function generateCRUDController(config) {
|
|
155
|
+
const { resourceName, tableName } = config;
|
|
156
|
+
const pascalName = toPascalCase(resourceName);
|
|
157
|
+
const camelName = toCamelCase(resourceName);
|
|
158
|
+
return `import { Create${pascalName}Data, Update${pascalName}Data } from './${resourceName.toLowerCase()}.validator.js';
|
|
159
|
+
import { ControllerResult, createSuccessResult, createErrorResult } from '../../shared/api.utils.js';
|
|
160
|
+
|
|
161
|
+
// List all ${camelName}s
|
|
162
|
+
export async function list${pascalName}s(): Promise<ControllerResult<any[]>> {
|
|
163
|
+
try {
|
|
164
|
+
// TODO: Implement database query
|
|
165
|
+
// const result = await pool.query('SELECT * FROM ${tableName}');
|
|
166
|
+
|
|
167
|
+
const mock${pascalName}s = [
|
|
168
|
+
{ id: 1, name: 'Sample ${pascalName} 1', createdAt: new Date().toISOString() },
|
|
169
|
+
{ id: 2, name: 'Sample ${pascalName} 2', createdAt: new Date().toISOString() }
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
return createSuccessResult('${pascalName}s retrieved', mock${pascalName}s);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return createErrorResult('Failed to list ${camelName}s', error instanceof Error ? error.message : 'Unknown error', 500);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get single ${camelName}
|
|
179
|
+
export async function get${pascalName}(id: string): Promise<ControllerResult<any>> {
|
|
180
|
+
try {
|
|
181
|
+
// TODO: Implement database query
|
|
182
|
+
// const result = await pool.query('SELECT * FROM ${tableName} WHERE id = $1', [id]);
|
|
183
|
+
|
|
184
|
+
const mock${pascalName} = { id: parseInt(id), name: 'Sample ${pascalName}', createdAt: new Date().toISOString() };
|
|
185
|
+
return createSuccessResult('${pascalName} retrieved', mock${pascalName});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return createErrorResult('Failed to get ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create ${camelName}
|
|
192
|
+
export async function create${pascalName}(data: Create${pascalName}Data): Promise<ControllerResult<any>> {
|
|
193
|
+
try {
|
|
194
|
+
// TODO: Implement database insertion
|
|
195
|
+
const new${pascalName} = { id: Math.floor(Math.random() * 1000), ...data, createdAt: new Date().toISOString() };
|
|
196
|
+
return createSuccessResult('${pascalName} created', new${pascalName}, undefined, 201);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return createErrorResult('Failed to create ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Update ${camelName}
|
|
203
|
+
export async function update${pascalName}(id: string, data: Update${pascalName}Data): Promise<ControllerResult<any>> {
|
|
204
|
+
try {
|
|
205
|
+
// TODO: Implement database update
|
|
206
|
+
const updated${pascalName} = { id: parseInt(id), ...data, updatedAt: new Date().toISOString() };
|
|
207
|
+
return createSuccessResult('${pascalName} updated', updated${pascalName});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return createErrorResult('Failed to update ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Delete ${camelName}
|
|
214
|
+
export async function delete${pascalName}(id: string): Promise<ControllerResult<void>> {
|
|
215
|
+
try {
|
|
216
|
+
// TODO: Implement database deletion
|
|
217
|
+
return createSuccessResult('${pascalName} deleted');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return createErrorResult('Failed to delete ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
function generateCRUDRoutes(config) {
|
|
225
|
+
const { resourceName, addAuth } = config;
|
|
226
|
+
const pascalName = toPascalCase(resourceName);
|
|
227
|
+
return `import { Router, Request, Response } from 'express';
|
|
228
|
+
import {
|
|
229
|
+
list${pascalName}s,
|
|
230
|
+
get${pascalName},
|
|
231
|
+
create${pascalName},
|
|
232
|
+
update${pascalName},
|
|
233
|
+
delete${pascalName}
|
|
234
|
+
} from './${resourceName.toLowerCase()}.controller.js';
|
|
235
|
+
import { create${pascalName}Schema, update${pascalName}Schema } from './${resourceName.toLowerCase()}.validator.js';
|
|
236
|
+
import { sendResponse } from '../../shared/api.utils.js';
|
|
237
|
+
${addAuth ? "// import { requireAuth } from '../auth/auth.middleware.js';\n" : ''}
|
|
238
|
+
const router = Router();
|
|
239
|
+
|
|
240
|
+
// List all
|
|
241
|
+
router.get('/', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
|
|
242
|
+
try {
|
|
243
|
+
const result = await list${pascalName}s();
|
|
244
|
+
res.status(result.statusCode || 200).json(result);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
res.status(500).json({ success: false, message: 'Internal error', error });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Get one
|
|
251
|
+
router.get('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
|
|
252
|
+
try {
|
|
253
|
+
const result = await get${pascalName}(req.params.id);
|
|
254
|
+
res.status(result.statusCode || 200).json(result);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
res.status(500).json({ success: false, message: 'Internal error', error });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Create
|
|
261
|
+
router.post('/', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
|
|
262
|
+
await sendResponse(req, res, () => create${pascalName}Schema.safeParse(req.body), (data) => create${pascalName}(data));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Update
|
|
266
|
+
router.put('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
|
|
267
|
+
await sendResponse(req, res, () => update${pascalName}Schema.safeParse(req.body), (data) => update${pascalName}(req.params.id, data));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Delete
|
|
271
|
+
router.delete('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
|
|
272
|
+
try {
|
|
273
|
+
const result = await delete${pascalName}(req.params.id);
|
|
274
|
+
res.status(result.statusCode || 200).json(result);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
res.status(500).json({ success: false, message: 'Internal error', error });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export default router;
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function infoCommand(): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { title, info, gray, error, warning } from '../utils/logger.js';
|
|
3
|
+
import { fileExists } from '../utils/file-utils.js';
|
|
4
|
+
export async function infoCommand() {
|
|
5
|
+
title('📊 Project Information');
|
|
6
|
+
// Check if in project
|
|
7
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
8
|
+
if (!fileExists(packageJsonPath)) {
|
|
9
|
+
error('Not in a project directory!');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
// Read package.json
|
|
13
|
+
const fs = await import('fs');
|
|
14
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf-8'));
|
|
15
|
+
info('Project Details:');
|
|
16
|
+
gray(` Name: ${packageJson.name}`);
|
|
17
|
+
gray(` Version: ${packageJson.version}`);
|
|
18
|
+
gray(` Description: ${packageJson.description || 'N/A'}\n`);
|
|
19
|
+
// Check for katax-core
|
|
20
|
+
const hasKataxCore = packageJson.dependencies?.['katax-core'];
|
|
21
|
+
info('Dependencies:');
|
|
22
|
+
gray(` katax-core: ${hasKataxCore ? '✅ Installed' : '❌ Not installed'}`);
|
|
23
|
+
gray(` express: ${packageJson.dependencies?.express ? '✅ Installed' : '❌ Not installed'}`);
|
|
24
|
+
gray(` TypeScript: ${packageJson.devDependencies?.typescript ? '✅ Installed' : '❌ Not installed'}\n`);
|
|
25
|
+
// Scan for API routes
|
|
26
|
+
const apiPath = path.join(process.cwd(), 'src', 'api');
|
|
27
|
+
if (await dirExists(apiPath)) {
|
|
28
|
+
info('API Routes:');
|
|
29
|
+
const routes = await scanRoutes(apiPath);
|
|
30
|
+
if (routes.length > 0) {
|
|
31
|
+
routes.forEach(route => gray(` ${route}`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
gray(' No routes found');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
warning('API directory not found');
|
|
39
|
+
}
|
|
40
|
+
console.log();
|
|
41
|
+
}
|
|
42
|
+
async function dirExists(dirPath) {
|
|
43
|
+
try {
|
|
44
|
+
const fs = await import('fs');
|
|
45
|
+
const stats = await fs.promises.stat(dirPath);
|
|
46
|
+
return stats.isDirectory();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function scanRoutes(apiPath) {
|
|
53
|
+
const routes = [];
|
|
54
|
+
try {
|
|
55
|
+
const fs = await import('fs');
|
|
56
|
+
const entries = await fs.promises.readdir(apiPath, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.isDirectory() && entry.name !== 'routes') {
|
|
59
|
+
const routeFile = path.join(apiPath, entry.name, `${entry.name}.routes.ts`);
|
|
60
|
+
if (await fileExistsAsync(routeFile)) {
|
|
61
|
+
routes.push(`/${entry.name}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// Ignore errors
|
|
68
|
+
}
|
|
69
|
+
return routes;
|
|
70
|
+
}
|
|
71
|
+
async function fileExistsAsync(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const fs = await import('fs');
|
|
74
|
+
await fs.promises.access(filePath);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|