servcraft 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/.dockerignore +45 -0
- package/.env.example +46 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +11 -0
- package/Dockerfile +76 -0
- package/Dockerfile.dev +31 -0
- package/README.md +232 -0
- package/commitlint.config.js +24 -0
- package/dist/cli/index.cjs +3968 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3945 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2458 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +828 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +2332 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.prod.yml +118 -0
- package/docker-compose.yml +147 -0
- package/eslint.config.js +27 -0
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
- package/npm-cache/_update-notifier-last-checked +0 -0
- package/package.json +112 -0
- package/prisma/schema.prisma +157 -0
- package/src/cli/commands/add-module.ts +422 -0
- package/src/cli/commands/db.ts +137 -0
- package/src/cli/commands/docs.ts +16 -0
- package/src/cli/commands/generate.ts +459 -0
- package/src/cli/commands/init.ts +640 -0
- package/src/cli/index.ts +32 -0
- package/src/cli/templates/controller.ts +67 -0
- package/src/cli/templates/dynamic-prisma.ts +89 -0
- package/src/cli/templates/dynamic-schemas.ts +232 -0
- package/src/cli/templates/dynamic-types.ts +60 -0
- package/src/cli/templates/module-index.ts +33 -0
- package/src/cli/templates/prisma-model.ts +17 -0
- package/src/cli/templates/repository.ts +104 -0
- package/src/cli/templates/routes.ts +70 -0
- package/src/cli/templates/schemas.ts +26 -0
- package/src/cli/templates/service.ts +58 -0
- package/src/cli/templates/types.ts +27 -0
- package/src/cli/utils/docs-generator.ts +47 -0
- package/src/cli/utils/field-parser.ts +315 -0
- package/src/cli/utils/helpers.ts +89 -0
- package/src/config/env.ts +80 -0
- package/src/config/index.ts +97 -0
- package/src/core/index.ts +5 -0
- package/src/core/logger.ts +43 -0
- package/src/core/server.ts +132 -0
- package/src/database/index.ts +7 -0
- package/src/database/prisma.ts +54 -0
- package/src/database/seed.ts +59 -0
- package/src/index.ts +63 -0
- package/src/middleware/error-handler.ts +73 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/security.ts +116 -0
- package/src/modules/audit/audit.service.ts +192 -0
- package/src/modules/audit/index.ts +2 -0
- package/src/modules/audit/types.ts +37 -0
- package/src/modules/auth/auth.controller.ts +182 -0
- package/src/modules/auth/auth.middleware.ts +87 -0
- package/src/modules/auth/auth.routes.ts +123 -0
- package/src/modules/auth/auth.service.ts +142 -0
- package/src/modules/auth/index.ts +49 -0
- package/src/modules/auth/schemas.ts +52 -0
- package/src/modules/auth/types.ts +69 -0
- package/src/modules/email/email.service.ts +212 -0
- package/src/modules/email/index.ts +10 -0
- package/src/modules/email/templates.ts +213 -0
- package/src/modules/email/types.ts +57 -0
- package/src/modules/swagger/index.ts +3 -0
- package/src/modules/swagger/schema-builder.ts +263 -0
- package/src/modules/swagger/swagger.service.ts +169 -0
- package/src/modules/swagger/types.ts +68 -0
- package/src/modules/user/index.ts +30 -0
- package/src/modules/user/schemas.ts +49 -0
- package/src/modules/user/types.ts +78 -0
- package/src/modules/user/user.controller.ts +139 -0
- package/src/modules/user/user.repository.ts +156 -0
- package/src/modules/user/user.routes.ts +199 -0
- package/src/modules/user/user.service.ts +145 -0
- package/src/modules/validation/index.ts +18 -0
- package/src/modules/validation/validator.ts +104 -0
- package/src/types/common.ts +61 -0
- package/src/types/index.ts +10 -0
- package/src/utils/errors.ts +66 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/pagination.ts +38 -0
- package/src/utils/response.ts +63 -0
- package/tests/integration/auth.test.ts +59 -0
- package/tests/setup.ts +17 -0
- package/tests/unit/modules/validation.test.ts +88 -0
- package/tests/unit/utils/errors.test.ts +113 -0
- package/tests/unit/utils/pagination.test.ts +82 -0
- package/tsconfig.json +33 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { execSync, spawn } from 'child_process';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { success, error, info } from '../utils/helpers.js';
|
|
6
|
+
|
|
7
|
+
export const dbCommand = new Command('db')
|
|
8
|
+
.description('Database management commands');
|
|
9
|
+
|
|
10
|
+
dbCommand
|
|
11
|
+
.command('migrate')
|
|
12
|
+
.description('Run database migrations')
|
|
13
|
+
.option('-n, --name <name>', 'Migration name')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
const spinner = ora('Running migrations...').start();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const cmd = options.name
|
|
19
|
+
? `npx prisma migrate dev --name ${options.name}`
|
|
20
|
+
: 'npx prisma migrate dev';
|
|
21
|
+
|
|
22
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
23
|
+
spinner.succeed('Migrations completed!');
|
|
24
|
+
} catch (err) {
|
|
25
|
+
spinner.fail('Migration failed');
|
|
26
|
+
error(err instanceof Error ? err.message : String(err));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
dbCommand
|
|
31
|
+
.command('push')
|
|
32
|
+
.description('Push schema changes to database (no migration)')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
const spinner = ora('Pushing schema...').start();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
execSync('npx prisma db push', { stdio: 'inherit' });
|
|
38
|
+
spinner.succeed('Schema pushed successfully!');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail('Push failed');
|
|
41
|
+
error(err instanceof Error ? err.message : String(err));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
dbCommand
|
|
46
|
+
.command('generate')
|
|
47
|
+
.description('Generate Prisma client')
|
|
48
|
+
.action(async () => {
|
|
49
|
+
const spinner = ora('Generating Prisma client...').start();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
execSync('npx prisma generate', { stdio: 'inherit' });
|
|
53
|
+
spinner.succeed('Prisma client generated!');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
spinner.fail('Generation failed');
|
|
56
|
+
error(err instanceof Error ? err.message : String(err));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
dbCommand
|
|
61
|
+
.command('studio')
|
|
62
|
+
.description('Open Prisma Studio')
|
|
63
|
+
.action(async () => {
|
|
64
|
+
info('Opening Prisma Studio...');
|
|
65
|
+
const studio = spawn('npx', ['prisma', 'studio'], {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
shell: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
studio.on('close', (code) => {
|
|
71
|
+
if (code !== 0) {
|
|
72
|
+
error('Prisma Studio closed with error');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
dbCommand
|
|
78
|
+
.command('seed')
|
|
79
|
+
.description('Run database seed')
|
|
80
|
+
.action(async () => {
|
|
81
|
+
const spinner = ora('Seeding database...').start();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execSync('npx prisma db seed', { stdio: 'inherit' });
|
|
85
|
+
spinner.succeed('Database seeded!');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
spinner.fail('Seeding failed');
|
|
88
|
+
error(err instanceof Error ? err.message : String(err));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
dbCommand
|
|
93
|
+
.command('reset')
|
|
94
|
+
.description('Reset database (drop all data and re-run migrations)')
|
|
95
|
+
.option('-f, --force', 'Skip confirmation')
|
|
96
|
+
.action(async (options) => {
|
|
97
|
+
if (!options.force) {
|
|
98
|
+
console.log(chalk.yellow('\nā ļø WARNING: This will delete all data in your database!\n'));
|
|
99
|
+
|
|
100
|
+
const readline = await import('readline');
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const answer = await new Promise<string>((resolve) => {
|
|
107
|
+
rl.question('Are you sure you want to continue? (y/N) ', resolve);
|
|
108
|
+
});
|
|
109
|
+
rl.close();
|
|
110
|
+
|
|
111
|
+
if (answer.toLowerCase() !== 'y') {
|
|
112
|
+
info('Operation cancelled');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const spinner = ora('Resetting database...').start();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
execSync('npx prisma migrate reset --force', { stdio: 'inherit' });
|
|
121
|
+
spinner.succeed('Database reset completed!');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
spinner.fail('Reset failed');
|
|
124
|
+
error(err instanceof Error ? err.message : String(err));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
dbCommand
|
|
129
|
+
.command('status')
|
|
130
|
+
.description('Show migration status')
|
|
131
|
+
.action(async () => {
|
|
132
|
+
try {
|
|
133
|
+
execSync('npx prisma migrate status', { stdio: 'inherit' });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
error('Failed to get migration status');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { generateDocs } from '../utils/docs-generator.js';
|
|
3
|
+
import { success, error } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
export const docsCommand = new Command('docs')
|
|
6
|
+
.description('Generate Swagger/OpenAPI documentation')
|
|
7
|
+
.option('-o, --output <path>', 'Output file path', 'openapi.json')
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
try {
|
|
10
|
+
const outputPath = await generateDocs(options.output);
|
|
11
|
+
success(`Documentation written to ${outputPath}`);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
error(err instanceof Error ? err.message : String(err));
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import {
|
|
6
|
+
toPascalCase,
|
|
7
|
+
toCamelCase,
|
|
8
|
+
toKebabCase,
|
|
9
|
+
pluralize,
|
|
10
|
+
fileExists,
|
|
11
|
+
writeFile,
|
|
12
|
+
success,
|
|
13
|
+
error,
|
|
14
|
+
warn,
|
|
15
|
+
info,
|
|
16
|
+
getModulesDir,
|
|
17
|
+
} from '../utils/helpers.js';
|
|
18
|
+
import { parseFields, type FieldDefinition } from '../utils/field-parser.js';
|
|
19
|
+
import { controllerTemplate } from '../templates/controller.js';
|
|
20
|
+
import { serviceTemplate } from '../templates/service.js';
|
|
21
|
+
import { repositoryTemplate } from '../templates/repository.js';
|
|
22
|
+
import { typesTemplate } from '../templates/types.js';
|
|
23
|
+
import { schemasTemplate } from '../templates/schemas.js';
|
|
24
|
+
import { routesTemplate } from '../templates/routes.js';
|
|
25
|
+
import { moduleIndexTemplate } from '../templates/module-index.js';
|
|
26
|
+
import { prismaModelTemplate } from '../templates/prisma-model.js';
|
|
27
|
+
import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
|
|
28
|
+
import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
|
|
29
|
+
import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
30
|
+
import { generateDocs } from '../utils/docs-generator.js';
|
|
31
|
+
|
|
32
|
+
export const generateCommand = new Command('generate')
|
|
33
|
+
.alias('g')
|
|
34
|
+
.description('Generate resources (module, controller, service, etc.)');
|
|
35
|
+
|
|
36
|
+
// Generate full module
|
|
37
|
+
generateCommand
|
|
38
|
+
.command('module <name> [fields...]')
|
|
39
|
+
.alias('m')
|
|
40
|
+
.description('Generate a complete module with controller, service, repository, types, schemas, and routes')
|
|
41
|
+
.option('--no-routes', 'Skip routes generation')
|
|
42
|
+
.option('--no-repository', 'Skip repository generation')
|
|
43
|
+
.option('--prisma', 'Generate Prisma model suggestion')
|
|
44
|
+
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
45
|
+
.option('-i, --interactive', 'Interactive mode to define fields')
|
|
46
|
+
.action(async (name: string, fieldsArgs: string[], options) => {
|
|
47
|
+
let fields: FieldDefinition[] = [];
|
|
48
|
+
|
|
49
|
+
// Parse fields from command line or interactive mode
|
|
50
|
+
if (options.interactive) {
|
|
51
|
+
fields = await promptForFields();
|
|
52
|
+
} else if (fieldsArgs.length > 0) {
|
|
53
|
+
fields = parseFields(fieldsArgs.join(' '));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const spinner = ora('Generating module...').start();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const kebabName = toKebabCase(name);
|
|
60
|
+
const pascalName = toPascalCase(name);
|
|
61
|
+
const camelName = toCamelCase(name);
|
|
62
|
+
const pluralName = pluralize(kebabName);
|
|
63
|
+
const tableName = pluralize(kebabName.replace(/-/g, '_'));
|
|
64
|
+
const validatorType = (options.validator || 'zod') as ValidatorType;
|
|
65
|
+
|
|
66
|
+
const moduleDir = path.join(getModulesDir(), kebabName);
|
|
67
|
+
|
|
68
|
+
// Check if module already exists
|
|
69
|
+
if (await fileExists(moduleDir)) {
|
|
70
|
+
spinner.stop();
|
|
71
|
+
error(`Module "${kebabName}" already exists`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Use dynamic templates if fields are provided
|
|
76
|
+
const hasFields = fields.length > 0;
|
|
77
|
+
|
|
78
|
+
const files = [
|
|
79
|
+
{
|
|
80
|
+
name: `${kebabName}.types.ts`,
|
|
81
|
+
content: hasFields
|
|
82
|
+
? dynamicTypesTemplate(kebabName, pascalName, fields)
|
|
83
|
+
: typesTemplate(kebabName, pascalName),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: `${kebabName}.schemas.ts`,
|
|
87
|
+
content: hasFields
|
|
88
|
+
? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType)
|
|
89
|
+
: schemasTemplate(kebabName, pascalName, camelName),
|
|
90
|
+
},
|
|
91
|
+
{ name: `${kebabName}.service.ts`, content: serviceTemplate(kebabName, pascalName, camelName) },
|
|
92
|
+
{ name: `${kebabName}.controller.ts`, content: controllerTemplate(kebabName, pascalName, camelName) },
|
|
93
|
+
{ name: 'index.ts', content: moduleIndexTemplate(kebabName, pascalName, camelName) },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (options.repository !== false) {
|
|
97
|
+
files.push({
|
|
98
|
+
name: `${kebabName}.repository.ts`,
|
|
99
|
+
content: repositoryTemplate(kebabName, pascalName, camelName, pluralName),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options.routes !== false) {
|
|
104
|
+
files.push({
|
|
105
|
+
name: `${kebabName}.routes.ts`,
|
|
106
|
+
content: routesTemplate(kebabName, pascalName, camelName, pluralName, fields),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Write all files
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
await writeFile(path.join(moduleDir, file.name), file.content);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
116
|
+
|
|
117
|
+
// Show Prisma model if requested or fields provided
|
|
118
|
+
if (options.prisma || hasFields) {
|
|
119
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
120
|
+
info('Prisma model suggestion:');
|
|
121
|
+
if (hasFields) {
|
|
122
|
+
console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
|
|
123
|
+
} else {
|
|
124
|
+
console.log(prismaModelTemplate(kebabName, pascalName, tableName));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Show fields summary if provided
|
|
129
|
+
if (hasFields) {
|
|
130
|
+
console.log('\nš Fields defined:');
|
|
131
|
+
fields.forEach((f) => {
|
|
132
|
+
const opts = [];
|
|
133
|
+
if (f.isOptional) opts.push('optional');
|
|
134
|
+
if (f.isArray) opts.push('array');
|
|
135
|
+
if (f.isUnique) opts.push('unique');
|
|
136
|
+
const optsStr = opts.length > 0 ? ` (${opts.join(', ')})` : '';
|
|
137
|
+
success(` ${f.name}: ${f.type}${optsStr}`);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Show next steps
|
|
142
|
+
console.log('\nš Files created:');
|
|
143
|
+
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
144
|
+
|
|
145
|
+
console.log('\nš Next steps:');
|
|
146
|
+
if (!hasFields) {
|
|
147
|
+
info(' 1. Update the types in ' + `${kebabName}.types.ts`);
|
|
148
|
+
info(' 2. Update the schemas in ' + `${kebabName}.schemas.ts`);
|
|
149
|
+
info(' 3. Register the module in your app');
|
|
150
|
+
} else {
|
|
151
|
+
info(' 1. Review generated types and schemas');
|
|
152
|
+
info(' 2. Register the module in your app');
|
|
153
|
+
}
|
|
154
|
+
if (options.prisma || hasFields) {
|
|
155
|
+
info(` ${hasFields ? '3' : '4'}. Add the Prisma model to schema.prisma`);
|
|
156
|
+
info(` ${hasFields ? '4' : '5'}. Run: npm run db:migrate`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { generateDocsNow } = await inquirer.prompt<{ generateDocsNow: boolean }>([
|
|
160
|
+
{
|
|
161
|
+
type: 'confirm',
|
|
162
|
+
name: 'generateDocsNow',
|
|
163
|
+
message: 'Generate Swagger/OpenAPI documentation now?',
|
|
164
|
+
default: true,
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
if (generateDocsNow) {
|
|
169
|
+
await generateDocs('openapi.json', true);
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
spinner.fail('Failed to generate module');
|
|
173
|
+
error(err instanceof Error ? err.message : String(err));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Generate controller only
|
|
178
|
+
generateCommand
|
|
179
|
+
.command('controller <name>')
|
|
180
|
+
.alias('c')
|
|
181
|
+
.description('Generate a controller')
|
|
182
|
+
.option('-m, --module <module>', 'Target module name')
|
|
183
|
+
.action(async (name: string, options) => {
|
|
184
|
+
const spinner = ora('Generating controller...').start();
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const kebabName = toKebabCase(name);
|
|
188
|
+
const pascalName = toPascalCase(name);
|
|
189
|
+
const camelName = toCamelCase(name);
|
|
190
|
+
|
|
191
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
192
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
193
|
+
const filePath = path.join(moduleDir, `${kebabName}.controller.ts`);
|
|
194
|
+
|
|
195
|
+
if (await fileExists(filePath)) {
|
|
196
|
+
spinner.stop();
|
|
197
|
+
error(`Controller "${kebabName}" already exists`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
|
|
202
|
+
|
|
203
|
+
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
204
|
+
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
spinner.fail('Failed to generate controller');
|
|
207
|
+
error(err instanceof Error ? err.message : String(err));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Generate service only
|
|
212
|
+
generateCommand
|
|
213
|
+
.command('service <name>')
|
|
214
|
+
.alias('s')
|
|
215
|
+
.description('Generate a service')
|
|
216
|
+
.option('-m, --module <module>', 'Target module name')
|
|
217
|
+
.action(async (name: string, options) => {
|
|
218
|
+
const spinner = ora('Generating service...').start();
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const kebabName = toKebabCase(name);
|
|
222
|
+
const pascalName = toPascalCase(name);
|
|
223
|
+
const camelName = toCamelCase(name);
|
|
224
|
+
|
|
225
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
226
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
227
|
+
const filePath = path.join(moduleDir, `${kebabName}.service.ts`);
|
|
228
|
+
|
|
229
|
+
if (await fileExists(filePath)) {
|
|
230
|
+
spinner.stop();
|
|
231
|
+
error(`Service "${kebabName}" already exists`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
|
|
236
|
+
|
|
237
|
+
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
238
|
+
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
spinner.fail('Failed to generate service');
|
|
241
|
+
error(err instanceof Error ? err.message : String(err));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Generate repository only
|
|
246
|
+
generateCommand
|
|
247
|
+
.command('repository <name>')
|
|
248
|
+
.alias('r')
|
|
249
|
+
.description('Generate a repository')
|
|
250
|
+
.option('-m, --module <module>', 'Target module name')
|
|
251
|
+
.action(async (name: string, options) => {
|
|
252
|
+
const spinner = ora('Generating repository...').start();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const kebabName = toKebabCase(name);
|
|
256
|
+
const pascalName = toPascalCase(name);
|
|
257
|
+
const camelName = toCamelCase(name);
|
|
258
|
+
const pluralName = pluralize(kebabName);
|
|
259
|
+
|
|
260
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
261
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
262
|
+
const filePath = path.join(moduleDir, `${kebabName}.repository.ts`);
|
|
263
|
+
|
|
264
|
+
if (await fileExists(filePath)) {
|
|
265
|
+
spinner.stop();
|
|
266
|
+
error(`Repository "${kebabName}" already exists`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
|
|
271
|
+
|
|
272
|
+
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
273
|
+
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
spinner.fail('Failed to generate repository');
|
|
276
|
+
error(err instanceof Error ? err.message : String(err));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Generate types only
|
|
281
|
+
generateCommand
|
|
282
|
+
.command('types <name>')
|
|
283
|
+
.alias('t')
|
|
284
|
+
.description('Generate types/interfaces')
|
|
285
|
+
.option('-m, --module <module>', 'Target module name')
|
|
286
|
+
.action(async (name: string, options) => {
|
|
287
|
+
const spinner = ora('Generating types...').start();
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const kebabName = toKebabCase(name);
|
|
291
|
+
const pascalName = toPascalCase(name);
|
|
292
|
+
|
|
293
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
294
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
295
|
+
const filePath = path.join(moduleDir, `${kebabName}.types.ts`);
|
|
296
|
+
|
|
297
|
+
if (await fileExists(filePath)) {
|
|
298
|
+
spinner.stop();
|
|
299
|
+
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
304
|
+
|
|
305
|
+
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
306
|
+
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
spinner.fail('Failed to generate types');
|
|
309
|
+
error(err instanceof Error ? err.message : String(err));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Generate schemas/validators only
|
|
314
|
+
generateCommand
|
|
315
|
+
.command('schema <name>')
|
|
316
|
+
.alias('v')
|
|
317
|
+
.description('Generate validation schemas')
|
|
318
|
+
.option('-m, --module <module>', 'Target module name')
|
|
319
|
+
.action(async (name: string, options) => {
|
|
320
|
+
const spinner = ora('Generating schemas...').start();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const kebabName = toKebabCase(name);
|
|
324
|
+
const pascalName = toPascalCase(name);
|
|
325
|
+
const camelName = toCamelCase(name);
|
|
326
|
+
|
|
327
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
328
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
329
|
+
const filePath = path.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
330
|
+
|
|
331
|
+
if (await fileExists(filePath)) {
|
|
332
|
+
spinner.stop();
|
|
333
|
+
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
338
|
+
|
|
339
|
+
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
340
|
+
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
spinner.fail('Failed to generate schemas');
|
|
343
|
+
error(err instanceof Error ? err.message : String(err));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Generate routes only
|
|
348
|
+
generateCommand
|
|
349
|
+
.command('routes <name>')
|
|
350
|
+
.description('Generate routes')
|
|
351
|
+
.option('-m, --module <module>', 'Target module name')
|
|
352
|
+
.action(async (name: string, options) => {
|
|
353
|
+
const spinner = ora('Generating routes...').start();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const kebabName = toKebabCase(name);
|
|
357
|
+
const pascalName = toPascalCase(name);
|
|
358
|
+
const camelName = toCamelCase(name);
|
|
359
|
+
const pluralName = pluralize(kebabName);
|
|
360
|
+
|
|
361
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
362
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
363
|
+
const filePath = path.join(moduleDir, `${kebabName}.routes.ts`);
|
|
364
|
+
|
|
365
|
+
if (await fileExists(filePath)) {
|
|
366
|
+
spinner.stop();
|
|
367
|
+
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
|
|
372
|
+
|
|
373
|
+
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
374
|
+
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
spinner.fail('Failed to generate routes');
|
|
377
|
+
error(err instanceof Error ? err.message : String(err));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Interactive field prompt helper
|
|
382
|
+
async function promptForFields(): Promise<FieldDefinition[]> {
|
|
383
|
+
const fields: FieldDefinition[] = [];
|
|
384
|
+
|
|
385
|
+
console.log('\nš Define your model fields (press Enter with empty name to finish)\n');
|
|
386
|
+
|
|
387
|
+
const fieldTypes = [
|
|
388
|
+
'string',
|
|
389
|
+
'number',
|
|
390
|
+
'boolean',
|
|
391
|
+
'date',
|
|
392
|
+
'datetime',
|
|
393
|
+
'text',
|
|
394
|
+
'email',
|
|
395
|
+
'url',
|
|
396
|
+
'uuid',
|
|
397
|
+
'int',
|
|
398
|
+
'float',
|
|
399
|
+
'decimal',
|
|
400
|
+
'json',
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
let addMore = true;
|
|
404
|
+
|
|
405
|
+
while (addMore) {
|
|
406
|
+
const answers = await inquirer.prompt([
|
|
407
|
+
{
|
|
408
|
+
type: 'input',
|
|
409
|
+
name: 'name',
|
|
410
|
+
message: 'Field name (empty to finish):',
|
|
411
|
+
},
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
if (!answers.name) {
|
|
415
|
+
addMore = false;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const fieldDetails = await inquirer.prompt([
|
|
420
|
+
{
|
|
421
|
+
type: 'list',
|
|
422
|
+
name: 'type',
|
|
423
|
+
message: `Type for "${answers.name}":`,
|
|
424
|
+
choices: fieldTypes,
|
|
425
|
+
default: 'string',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: 'confirm',
|
|
429
|
+
name: 'isOptional',
|
|
430
|
+
message: 'Is optional?',
|
|
431
|
+
default: false,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: 'confirm',
|
|
435
|
+
name: 'isUnique',
|
|
436
|
+
message: 'Is unique?',
|
|
437
|
+
default: false,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
type: 'confirm',
|
|
441
|
+
name: 'isArray',
|
|
442
|
+
message: 'Is array?',
|
|
443
|
+
default: false,
|
|
444
|
+
},
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
fields.push({
|
|
448
|
+
name: answers.name,
|
|
449
|
+
type: fieldDetails.type,
|
|
450
|
+
isOptional: fieldDetails.isOptional,
|
|
451
|
+
isUnique: fieldDetails.isUnique,
|
|
452
|
+
isArray: fieldDetails.isArray,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
console.log(` ā Added: ${answers.name}: ${fieldDetails.type}\n`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return fields;
|
|
459
|
+
}
|