speedrun-cli 2.6.12 → 2.7.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/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/src/generator.js +1 -16
- package/src/moduleGenerator.js +597 -121
- package/src/postSetup.js +423 -402
- package/src/prompts.js +25 -3
- package/templates/orm/drizzle/package.json +2 -1
- package/templates/orm/mongoose/package.json +2 -1
- package/templates/orm/prisma/package.json +1 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-drizzle/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-drizzle/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-drizzle/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +0 -24
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +0 -140
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +0 -39
- package/templates/base-crud-mongoose/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-mongoose/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-mongoose/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +0 -26
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +0 -133
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +0 -67
- package/templates/base-crud-prisma/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-prisma/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-prisma/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-prisma/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +0 -12
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +0 -100
- package/templates/base-crud-typeorm/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-typeorm/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-typeorm/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +0 -58
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +0 -25
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +0 -102
package/src/moduleGenerator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module Generator for create-nestjs-auth
|
|
2
|
+
* Shared Interactive Module Generator for create-nestjs-auth
|
|
3
3
|
* @module moduleGenerator
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ const chalk = require('chalk');
|
|
|
11
11
|
function toPascalCase(str) {
|
|
12
12
|
return str
|
|
13
13
|
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
|
14
|
-
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
|
|
14
|
+
.map((x) => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
|
|
15
15
|
.join('');
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -23,56 +23,91 @@ function toCamelCase(str) {
|
|
|
23
23
|
function toKebabCase(str) {
|
|
24
24
|
return str
|
|
25
25
|
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
|
26
|
-
.map(x => x.toLowerCase())
|
|
26
|
+
.map((x) => x.toLowerCase())
|
|
27
27
|
.join('-');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function toSnakeCase(str) {
|
|
31
|
+
return str
|
|
32
|
+
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
|
33
|
+
.map((x) => x.toLowerCase())
|
|
34
|
+
.join('_');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toSingularPascal(pascalStr) {
|
|
38
|
+
if (pascalStr.endsWith('ies')) {
|
|
39
|
+
return pascalStr.slice(0, -3) + 'y';
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
pascalStr.endsWith('s') &&
|
|
43
|
+
!pascalStr.endsWith('ss') &&
|
|
44
|
+
!pascalStr.endsWith('us') &&
|
|
45
|
+
!pascalStr.endsWith('is')
|
|
46
|
+
) {
|
|
47
|
+
return pascalStr.slice(0, -1);
|
|
48
|
+
}
|
|
49
|
+
return pascalStr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toSingularKebab(kebabStr) {
|
|
53
|
+
return toKebabCase(toSingularPascal(toPascalCase(kebabStr)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toSingularCamel(camelStr) {
|
|
57
|
+
return toCamelCase(toSingularPascal(toPascalCase(camelStr)));
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
async function detectOrm(targetDir) {
|
|
31
61
|
try {
|
|
32
62
|
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
33
63
|
if (await fs.pathExists(packageJsonPath)) {
|
|
34
64
|
const packageJson = await fs.readJSON(packageJsonPath);
|
|
35
|
-
const deps = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
const deps = {
|
|
66
|
+
...(packageJson.dependencies || {}),
|
|
67
|
+
...(packageJson.devDependencies || {}),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (deps['prisma'] || deps['@prisma/client']) return 'prisma';
|
|
71
|
+
if (deps['typeorm'] || deps['@nestjs/typeorm']) return 'typeorm';
|
|
72
|
+
if (deps['mongoose'] || deps['@nestjs/mongoose']) return 'mongoose';
|
|
40
73
|
if (deps['drizzle-orm']) return 'drizzle';
|
|
41
74
|
}
|
|
42
|
-
} catch
|
|
43
|
-
//
|
|
75
|
+
} catch {
|
|
76
|
+
// Fallback to prisma
|
|
44
77
|
}
|
|
45
78
|
return 'prisma';
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Interactive prompt for module options (Name, CRUD Mode, Fields)
|
|
83
|
+
*/
|
|
84
|
+
async function promptForModuleOptions(providedModuleName) {
|
|
85
|
+
let moduleName = providedModuleName;
|
|
50
86
|
|
|
51
87
|
if (!moduleName) {
|
|
52
|
-
|
|
88
|
+
const nameAnswer = await inquirer.prompt([{
|
|
53
89
|
type: 'input',
|
|
54
90
|
name: 'moduleName',
|
|
55
91
|
message: 'What module do you want to generate? (e.g., orders, products)',
|
|
56
|
-
validate: (input) => input.trim() ? true : 'Module name is required',
|
|
57
|
-
});
|
|
92
|
+
validate: (input) => (input && input.trim() ? true : 'Module name is required'),
|
|
93
|
+
}]);
|
|
94
|
+
moduleName = nameAnswer.moduleName.trim();
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
|
|
97
|
+
const { crudMode } = await inquirer.prompt([{
|
|
61
98
|
type: 'list',
|
|
62
99
|
name: 'crudMode',
|
|
63
100
|
message: 'Which CRUD mode do you want to use?',
|
|
64
101
|
choices: [
|
|
65
102
|
{ name: 'Full CRUD (Create, Read All, Read One, Update, Delete)', value: 'full' },
|
|
66
|
-
{ name: 'Custom Selection...', value: 'custom' }
|
|
103
|
+
{ name: 'Custom Selection...', value: 'custom' },
|
|
67
104
|
],
|
|
68
|
-
default: 'full'
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const answers = await inquirer.prompt(questions);
|
|
105
|
+
default: 'full',
|
|
106
|
+
}]);
|
|
72
107
|
|
|
73
108
|
let selectedOperations = ['create', 'findAll', 'findOne', 'update', 'remove'];
|
|
74
109
|
|
|
75
|
-
if (
|
|
110
|
+
if (crudMode === 'custom') {
|
|
76
111
|
const { customOps } = await inquirer.prompt([{
|
|
77
112
|
type: 'checkbox',
|
|
78
113
|
name: 'customOps',
|
|
@@ -80,31 +115,411 @@ async function promptForModuleOptions(moduleName) {
|
|
|
80
115
|
choices: [
|
|
81
116
|
{ name: 'Create (POST)', value: 'create', checked: true },
|
|
82
117
|
{ name: 'Read All / findAll (GET)', value: 'findAll', checked: true },
|
|
83
|
-
{ name: 'Read One / findOne (GET /:id)', value: 'findOne', checked:
|
|
118
|
+
{ name: 'Read One / findOne (GET /:id)', value: 'findOne', checked: true },
|
|
84
119
|
{ name: 'Update (PUT /:id)', value: 'update', checked: true },
|
|
85
|
-
{ name: 'Delete / remove (DELETE /:id)', value: 'remove', checked:
|
|
86
|
-
]
|
|
120
|
+
{ name: 'Delete / remove (DELETE /:id)', value: 'remove', checked: true },
|
|
121
|
+
],
|
|
87
122
|
}]);
|
|
88
123
|
selectedOperations = customOps;
|
|
89
124
|
}
|
|
90
125
|
|
|
126
|
+
// Interactive Field Builder Loop
|
|
127
|
+
const { addCustomFields } = await inquirer.prompt([{
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
name: 'addCustomFields',
|
|
130
|
+
message: `Do you want to add custom fields to '${moduleName}'?`,
|
|
131
|
+
default: true,
|
|
132
|
+
}]);
|
|
133
|
+
|
|
134
|
+
const fields = [];
|
|
135
|
+
|
|
136
|
+
if (addCustomFields) {
|
|
137
|
+
let addAnother = true;
|
|
138
|
+
while (addAnother) {
|
|
139
|
+
const fieldAnswers = await inquirer.prompt([
|
|
140
|
+
{
|
|
141
|
+
type: 'input',
|
|
142
|
+
name: 'fieldName',
|
|
143
|
+
message: 'Enter field name (e.g., totalAmount, title):',
|
|
144
|
+
validate: (input) => {
|
|
145
|
+
if (!input || !input.trim()) return 'Field name is required';
|
|
146
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
|
|
147
|
+
return 'Field name must be a valid identifier (e.g., totalAmount)';
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'list',
|
|
154
|
+
name: 'fieldType',
|
|
155
|
+
message: (answers) => `Select field type for '${answers.fieldName}':`,
|
|
156
|
+
choices: ['String', 'Number', 'Boolean', 'Date'],
|
|
157
|
+
default: 'String',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'confirm',
|
|
161
|
+
name: 'isOptional',
|
|
162
|
+
message: (answers) => `Is '${answers.fieldName}' optional?`,
|
|
163
|
+
default: false,
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
fields.push({
|
|
168
|
+
name: fieldAnswers.fieldName.trim(),
|
|
169
|
+
type: fieldAnswers.fieldType,
|
|
170
|
+
isOptional: fieldAnswers.isOptional,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const { continueLoop } = await inquirer.prompt([{
|
|
174
|
+
type: 'confirm',
|
|
175
|
+
name: 'continueLoop',
|
|
176
|
+
message: 'Do you want to add another field?',
|
|
177
|
+
default: false,
|
|
178
|
+
}]);
|
|
179
|
+
|
|
180
|
+
addAnother = continueLoop;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback if no custom fields added
|
|
185
|
+
if (fields.length === 0) {
|
|
186
|
+
fields.push({ name: 'name', type: 'String', isOptional: false });
|
|
187
|
+
}
|
|
188
|
+
|
|
91
189
|
return {
|
|
92
|
-
moduleName
|
|
93
|
-
operations: selectedOperations
|
|
190
|
+
moduleName,
|
|
191
|
+
operations: selectedOperations,
|
|
192
|
+
fields,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getFieldExampleValue(field, pascalName) {
|
|
197
|
+
const name = field.name.toLowerCase();
|
|
198
|
+
if (field.type === 'String') {
|
|
199
|
+
if (name.includes('email')) return 'user@example.com';
|
|
200
|
+
if (name.includes('phone')) return '+1234567890';
|
|
201
|
+
if (name.includes('url')) return 'https://example.com';
|
|
202
|
+
if (name.includes('sku') || name.includes('code')) return 'SKU-1001';
|
|
203
|
+
return `Sample ${field.name}`;
|
|
204
|
+
}
|
|
205
|
+
if (field.type === 'Number') {
|
|
206
|
+
if (name.includes('price') || name.includes('amount') || name.includes('total') || name.includes('cost')) {
|
|
207
|
+
return 99.99;
|
|
208
|
+
}
|
|
209
|
+
return 10;
|
|
210
|
+
}
|
|
211
|
+
if (field.type === 'Boolean') return true;
|
|
212
|
+
if (field.type === 'Date') return '2025-01-01T00:00:00.000Z';
|
|
213
|
+
return 'Example value';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getTsType(fieldType) {
|
|
217
|
+
switch (fieldType) {
|
|
218
|
+
case 'String':
|
|
219
|
+
return 'string';
|
|
220
|
+
case 'Number':
|
|
221
|
+
return 'number';
|
|
222
|
+
case 'Boolean':
|
|
223
|
+
return 'boolean';
|
|
224
|
+
case 'Date':
|
|
225
|
+
return 'Date';
|
|
226
|
+
default:
|
|
227
|
+
return 'string';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Dynamic ORM Schema Synchronization: Prisma
|
|
233
|
+
*/
|
|
234
|
+
async function syncPrismaSchema(targetDir, singularPascal, kebabName, fields) {
|
|
235
|
+
try {
|
|
236
|
+
const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
237
|
+
if (!(await fs.pathExists(schemaPath))) return;
|
|
238
|
+
|
|
239
|
+
let content = await fs.readFile(schemaPath, 'utf8');
|
|
240
|
+
|
|
241
|
+
// Avoid duplicate model definition
|
|
242
|
+
if (new RegExp(`\\bmodel\\s+${singularPascal}\\b`).test(content)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const fieldLines = fields.map((f) => {
|
|
247
|
+
let pType = 'String';
|
|
248
|
+
if (f.type === 'Number') {
|
|
249
|
+
const nameLower = f.name.toLowerCase();
|
|
250
|
+
if (
|
|
251
|
+
nameLower.includes('price') ||
|
|
252
|
+
nameLower.includes('amount') ||
|
|
253
|
+
nameLower.includes('total') ||
|
|
254
|
+
nameLower.includes('cost') ||
|
|
255
|
+
nameLower.includes('fee') ||
|
|
256
|
+
nameLower.includes('rate') ||
|
|
257
|
+
nameLower.includes('score')
|
|
258
|
+
) {
|
|
259
|
+
pType = 'Float';
|
|
260
|
+
} else {
|
|
261
|
+
pType = 'Int';
|
|
262
|
+
}
|
|
263
|
+
} else if (f.type === 'Boolean') {
|
|
264
|
+
pType = 'Boolean';
|
|
265
|
+
} else if (f.type === 'Date') {
|
|
266
|
+
pType = 'DateTime';
|
|
267
|
+
}
|
|
268
|
+
return ` ${f.name} ${pType}${f.isOptional ? '?' : ''}`;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const modelDefinition = `
|
|
272
|
+
model ${singularPascal} {
|
|
273
|
+
id String @id @default(uuid())
|
|
274
|
+
${fieldLines.join('\n')}
|
|
275
|
+
status String @default("ACTIVE")
|
|
276
|
+
createdAt DateTime @default(now())
|
|
277
|
+
updatedAt DateTime @updatedAt
|
|
278
|
+
deletedAt DateTime?
|
|
279
|
+
|
|
280
|
+
@@map("${toSnakeCase(kebabName)}")
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
content += modelDefinition;
|
|
285
|
+
await fs.writeFile(schemaPath, content, 'utf8');
|
|
286
|
+
console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Dynamic ORM Schema Synchronization: TypeORM
|
|
294
|
+
*/
|
|
295
|
+
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
296
|
+
try {
|
|
297
|
+
const entityDir = path.join(moduleDir, 'entities');
|
|
298
|
+
await fs.ensureDir(entityDir);
|
|
299
|
+
const entityPath = path.join(entityDir, `${toSingularKebab(kebabName)}.entity.ts`);
|
|
300
|
+
|
|
301
|
+
const fieldLines = fields.map((f) => {
|
|
302
|
+
let colDecorator = `@Column({ nullable: ${f.isOptional} })`;
|
|
303
|
+
let tsType = 'string';
|
|
304
|
+
|
|
305
|
+
if (f.type === 'Number') {
|
|
306
|
+
colDecorator = `@Column('decimal', { precision: 10, scale: 2, nullable: ${f.isOptional} })`;
|
|
307
|
+
tsType = 'number';
|
|
308
|
+
} else if (f.type === 'Boolean') {
|
|
309
|
+
colDecorator = `@Column({ default: false, nullable: ${f.isOptional} })`;
|
|
310
|
+
tsType = 'boolean';
|
|
311
|
+
} else if (f.type === 'Date') {
|
|
312
|
+
colDecorator = `@Column({ type: 'timestamp', nullable: ${f.isOptional} })`;
|
|
313
|
+
tsType = 'Date';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return ` ${colDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const entityContent = `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
|
|
320
|
+
|
|
321
|
+
@Entity('${toSnakeCase(kebabName)}')
|
|
322
|
+
export class ${singularPascal} {
|
|
323
|
+
@PrimaryGeneratedColumn('uuid')
|
|
324
|
+
id: string;
|
|
325
|
+
|
|
326
|
+
${fieldLines.join('\n\n')}
|
|
327
|
+
|
|
328
|
+
@Column({ default: 'ACTIVE' })
|
|
329
|
+
status: string;
|
|
330
|
+
|
|
331
|
+
@CreateDateColumn()
|
|
332
|
+
createdAt: Date;
|
|
333
|
+
|
|
334
|
+
@UpdateDateColumn()
|
|
335
|
+
updatedAt: Date;
|
|
336
|
+
|
|
337
|
+
@DeleteDateColumn({ nullable: true })
|
|
338
|
+
deletedAt?: Date | null;
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
|
|
342
|
+
await fs.writeFile(entityPath, entityContent, 'utf8');
|
|
343
|
+
console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Dynamic ORM Schema Synchronization: Mongoose
|
|
351
|
+
*/
|
|
352
|
+
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
353
|
+
try {
|
|
354
|
+
const schemaDir = path.join(moduleDir, 'schemas');
|
|
355
|
+
await fs.ensureDir(schemaDir);
|
|
356
|
+
const schemaPath = path.join(schemaDir, `${toSingularKebab(kebabName)}.schema.ts`);
|
|
357
|
+
|
|
358
|
+
const fieldLines = fields.map((f) => {
|
|
359
|
+
let propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
360
|
+
let tsType = 'string';
|
|
361
|
+
|
|
362
|
+
if (f.type === 'Number') {
|
|
363
|
+
propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
364
|
+
tsType = 'number';
|
|
365
|
+
} else if (f.type === 'Boolean') {
|
|
366
|
+
propDecorator = `@Prop({ default: false })`;
|
|
367
|
+
tsType = 'boolean';
|
|
368
|
+
} else if (f.type === 'Date') {
|
|
369
|
+
propDecorator = `@Prop({ type: Date, required: ${!f.isOptional} })`;
|
|
370
|
+
tsType = 'Date';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return ` ${propDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const schemaContent = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
377
|
+
import { HydratedDocument } from 'mongoose';
|
|
378
|
+
|
|
379
|
+
export type ${singularPascal}Document = HydratedDocument<${singularPascal}>;
|
|
380
|
+
|
|
381
|
+
@Schema({ timestamps: true })
|
|
382
|
+
export class ${singularPascal} {
|
|
383
|
+
${fieldLines.join('\n\n')}
|
|
384
|
+
|
|
385
|
+
@Prop({ default: 'ACTIVE' })
|
|
386
|
+
status: string;
|
|
387
|
+
|
|
388
|
+
@Prop({ type: Date, default: null })
|
|
389
|
+
deletedAt?: Date | null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPascal});
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
396
|
+
console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Dynamic ORM Schema Synchronization: Drizzle
|
|
404
|
+
*/
|
|
405
|
+
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
406
|
+
try {
|
|
407
|
+
const schemaDir = path.join(moduleDir, 'schema');
|
|
408
|
+
await fs.ensureDir(schemaDir);
|
|
409
|
+
const schemaPath = path.join(schemaDir, `${kebabName}.schema.ts`);
|
|
410
|
+
|
|
411
|
+
const fieldLines = fields.map((f) => {
|
|
412
|
+
let colDef = `varchar('${toSnakeCase(f.name)}', { length: 255 })`;
|
|
413
|
+
if (f.type === 'Number') {
|
|
414
|
+
colDef = `numeric('${toSnakeCase(f.name)}')`;
|
|
415
|
+
} else if (f.type === 'Boolean') {
|
|
416
|
+
colDef = `boolean('${toSnakeCase(f.name)}').default(false)`;
|
|
417
|
+
} else if (f.type === 'Date') {
|
|
418
|
+
colDef = `timestamp('${toSnakeCase(f.name)}')`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!f.isOptional) {
|
|
422
|
+
colDef += '.notNull()';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return ` ${f.name}: ${colDef},`;
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const schemaContent = `import { pgTable, varchar, numeric, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
429
|
+
|
|
430
|
+
export const ${toCamelCase(kebabName)}s = pgTable('${toSnakeCase(kebabName)}', {
|
|
431
|
+
id: varchar('id', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
432
|
+
${fieldLines.join('\n')}
|
|
433
|
+
status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),
|
|
434
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
435
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
436
|
+
deletedAt: timestamp('deleted_at'),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
export type ${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferSelect;
|
|
440
|
+
export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInsert;
|
|
441
|
+
`;
|
|
442
|
+
|
|
443
|
+
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
444
|
+
console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Auto-Generate Starter Seed File Template
|
|
452
|
+
*/
|
|
453
|
+
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, fields) {
|
|
454
|
+
try {
|
|
455
|
+
const singularCamel = toSingularCamel(kebabName);
|
|
456
|
+
const dummyObjFields = fields.map((f) => {
|
|
457
|
+
const ex = getFieldExampleValue(f, singularPascal);
|
|
458
|
+
const valStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
459
|
+
return ` ${f.name}: ${valStr},`;
|
|
460
|
+
}).join('\n');
|
|
461
|
+
|
|
462
|
+
if (orm === 'prisma') {
|
|
463
|
+
const seedsDir = path.join(targetDir, 'prisma', 'seeds');
|
|
464
|
+
await fs.ensureDir(seedsDir);
|
|
465
|
+
const seedPath = path.join(seedsDir, `${kebabName}.seed.ts`);
|
|
466
|
+
|
|
467
|
+
const seedContent = `import { PrismaClient } from '@prisma/client';
|
|
468
|
+
|
|
469
|
+
export async function seed${singularPascal}(prisma: PrismaClient) {
|
|
470
|
+
console.log('🌱 Seeding ${singularPascal}...');
|
|
471
|
+
await (prisma as any).${singularCamel}.create({
|
|
472
|
+
data: {
|
|
473
|
+
${dummyObjFields}
|
|
474
|
+
status: 'ACTIVE',
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
479
|
+
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
480
|
+
console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
|
|
481
|
+
} else {
|
|
482
|
+
const seedsDir = path.join(targetDir, 'src', 'database', 'seeds');
|
|
483
|
+
await fs.ensureDir(seedsDir);
|
|
484
|
+
const seedPath = path.join(seedsDir, `${kebabName}.seed.ts`);
|
|
485
|
+
|
|
486
|
+
const seedContent = `// Starter seed template for ${singularPascal}
|
|
487
|
+
export async function seed${singularPascal}(dbOrRepo: any) {
|
|
488
|
+
console.log('🌱 Seeding ${singularPascal}...');
|
|
489
|
+
const seedData = {
|
|
490
|
+
${dummyObjFields}
|
|
491
|
+
status: 'ACTIVE',
|
|
94
492
|
};
|
|
493
|
+
if (dbOrRepo && typeof dbOrRepo.create === 'function') {
|
|
494
|
+
const item = dbOrRepo.create(seedData);
|
|
495
|
+
if (typeof dbOrRepo.save === 'function') {
|
|
496
|
+
await dbOrRepo.save(item);
|
|
497
|
+
}
|
|
498
|
+
} else if (dbOrRepo && typeof dbOrRepo.insert === 'function') {
|
|
499
|
+
await dbOrRepo.insert(seedData);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
`;
|
|
503
|
+
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
504
|
+
console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
|
|
508
|
+
}
|
|
95
509
|
}
|
|
96
510
|
|
|
97
|
-
function getServiceContent(orm, pascalName, camelName, kebabName, createDtoName, updateDtoName, ops) {
|
|
511
|
+
function getServiceContent(orm, pascalName, singularPascal, camelName, kebabName, createDtoName, updateDtoName, ops) {
|
|
512
|
+
const singularKebab = toSingularKebab(kebabName);
|
|
513
|
+
|
|
98
514
|
if (orm === 'typeorm') {
|
|
99
|
-
return `import { Injectable
|
|
515
|
+
return `import { Injectable } from '@nestjs/common';
|
|
100
516
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
101
517
|
import { Repository, IsNull } from 'typeorm';
|
|
102
518
|
import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
|
|
103
519
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : ''}
|
|
520
|
+
import { ${singularPascal} } from './entities/${singularKebab}.entity';
|
|
104
521
|
|
|
105
|
-
|
|
106
|
-
type ${pascalName}Entity = any;
|
|
107
|
-
${!ops.create && !ops.update ? `\ntype ${createDtoName} = any;\ntype ${updateDtoName} = any;` : ''}
|
|
522
|
+
export type ${pascalName}Entity = ${singularPascal};
|
|
108
523
|
|
|
109
524
|
class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
110
525
|
constructor(private readonly repo: Repository<${pascalName}Entity>) {}
|
|
@@ -141,7 +556,7 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
141
556
|
export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
142
557
|
private readonly repository: TypeOrm${pascalName}Repository;
|
|
143
558
|
|
|
144
|
-
constructor(@InjectRepository(
|
|
559
|
+
constructor(@InjectRepository(${singularPascal}) private readonly repo: Repository<${pascalName}Entity>) {
|
|
145
560
|
super();
|
|
146
561
|
this.repository = new TypeOrm${pascalName}Repository(this.repo);
|
|
147
562
|
}
|
|
@@ -152,25 +567,24 @@ export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${cre
|
|
|
152
567
|
}
|
|
153
568
|
`;
|
|
154
569
|
} else if (orm === 'mongoose') {
|
|
155
|
-
return `import { Injectable
|
|
570
|
+
return `import { Injectable } from '@nestjs/common';
|
|
156
571
|
import { InjectModel } from '@nestjs/mongoose';
|
|
157
572
|
import { Model } from 'mongoose';
|
|
158
573
|
import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
|
|
159
574
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : ''}
|
|
575
|
+
import { ${singularPascal}, ${singularPascal}Document } from './schemas/${singularKebab}.schema';
|
|
160
576
|
|
|
161
|
-
|
|
162
|
-
type ${pascalName}Document = any;
|
|
163
|
-
${!ops.create && !ops.update ? `\ntype ${createDtoName} = any;\ntype ${updateDtoName} = any;` : ''}
|
|
577
|
+
export type ${pascalName}Entity = ${singularPascal}Document;
|
|
164
578
|
|
|
165
|
-
class Mongoose${pascalName}Repository implements IBaseRepository<${pascalName}
|
|
166
|
-
constructor(private readonly model: Model<${pascalName}
|
|
579
|
+
class Mongoose${pascalName}Repository implements IBaseRepository<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
580
|
+
constructor(private readonly model: Model<${pascalName}Entity>) {}
|
|
167
581
|
|
|
168
|
-
${ops.create ? `async create(dto: ${createDtoName}): Promise<${pascalName}
|
|
582
|
+
${ops.create ? `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> {
|
|
169
583
|
const created = new this.model(dto);
|
|
170
584
|
return created.save();
|
|
171
|
-
}` : `async create(dto: ${createDtoName}): Promise<${pascalName}
|
|
585
|
+
}` : `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
172
586
|
|
|
173
|
-
${ops.findAll ? `async findAll(pagination: PaginationQueryDto): Promise<{ data: ${pascalName}
|
|
587
|
+
${ops.findAll ? `async findAll(pagination: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> {
|
|
174
588
|
const { page = 1, limit = 10 } = pagination;
|
|
175
589
|
const skip = (page - 1) * limit;
|
|
176
590
|
const [data, total] = await Promise.all([
|
|
@@ -178,51 +592,49 @@ class Mongoose${pascalName}Repository implements IBaseRepository<${pascalName}Do
|
|
|
178
592
|
this.model.countDocuments({ deletedAt: null }).exec()
|
|
179
593
|
]);
|
|
180
594
|
return { data, total };
|
|
181
|
-
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}
|
|
595
|
+
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
182
596
|
|
|
183
|
-
${ops.findOne ? `async findOne(id: string): Promise<${pascalName}
|
|
597
|
+
${ops.findOne ? `async findOne(id: string): Promise<${pascalName}Entity | null> {
|
|
184
598
|
return this.model.findOne({ _id: id, deletedAt: null }).exec();
|
|
185
|
-
}` : `async findOne(id: string): Promise<${pascalName}
|
|
599
|
+
}` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
186
600
|
|
|
187
|
-
${ops.update ? `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}
|
|
188
|
-
return this.model.findByIdAndUpdate(id, dto as any, { new: true }).exec() as Promise<${pascalName}
|
|
189
|
-
}` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}
|
|
601
|
+
${ops.update ? `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
602
|
+
return this.model.findByIdAndUpdate(id, dto as any, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
603
|
+
}` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
190
604
|
|
|
191
|
-
${ops.remove ? `async remove(id: string): Promise<${pascalName}
|
|
192
|
-
return this.model.findByIdAndUpdate(id, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}
|
|
193
|
-
}` : `async remove(id: string): Promise<${pascalName}
|
|
605
|
+
${ops.remove ? `async remove(id: string): Promise<${pascalName}Entity> {
|
|
606
|
+
return this.model.findByIdAndUpdate(id, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
607
|
+
}` : `async remove(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
194
608
|
}
|
|
195
609
|
|
|
196
610
|
@Injectable()
|
|
197
|
-
export class ${pascalName}Service extends BaseService<${pascalName}
|
|
611
|
+
export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
198
612
|
private readonly repository: Mongoose${pascalName}Repository;
|
|
199
613
|
|
|
200
|
-
constructor(@InjectModel(
|
|
614
|
+
constructor(@InjectModel(${singularPascal}.name) private readonly model: Model<${pascalName}Entity>) {
|
|
201
615
|
super();
|
|
202
616
|
this.repository = new Mongoose${pascalName}Repository(this.model);
|
|
203
617
|
}
|
|
204
618
|
|
|
205
|
-
protected getRepository(): IBaseRepository<${pascalName}
|
|
619
|
+
protected getRepository(): IBaseRepository<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
206
620
|
return this.repository;
|
|
207
621
|
}
|
|
208
622
|
}
|
|
209
623
|
`;
|
|
210
624
|
} else if (orm === 'drizzle') {
|
|
211
|
-
return `import { Injectable, Inject
|
|
625
|
+
return `import { Injectable, Inject } from '@nestjs/common';
|
|
212
626
|
import { eq, isNull, desc } from 'drizzle-orm';
|
|
213
627
|
import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
|
|
214
628
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : ''}
|
|
629
|
+
import { ${camelName}s, ${singularPascal} } from './schema/${kebabName}.schema';
|
|
215
630
|
|
|
216
|
-
|
|
217
|
-
const ${camelName}Schema = {} as any;
|
|
218
|
-
type ${pascalName}Entity = any;
|
|
219
|
-
${!ops.create && !ops.update ? `\ntype ${createDtoName} = any;\ntype ${updateDtoName} = any;` : ''}
|
|
631
|
+
export type ${pascalName}Entity = ${singularPascal};
|
|
220
632
|
|
|
221
633
|
class Drizzle${pascalName}Repository implements IBaseRepository<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
222
634
|
constructor(private readonly db: any) {}
|
|
223
635
|
|
|
224
636
|
${ops.create ? `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> {
|
|
225
|
-
const [result] = await this.db.insert(${camelName}
|
|
637
|
+
const [result] = await this.db.insert(${camelName}s).values(dto).returning();
|
|
226
638
|
return result;
|
|
227
639
|
}` : `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
228
640
|
|
|
@@ -230,27 +642,27 @@ class Drizzle${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
230
642
|
const { page = 1, limit = 10 } = pagination;
|
|
231
643
|
const skip = (page - 1) * limit;
|
|
232
644
|
|
|
233
|
-
const data = await this.db.select().from(${camelName}
|
|
234
|
-
.where(isNull(${camelName}
|
|
645
|
+
const data = await this.db.select().from(${camelName}s)
|
|
646
|
+
.where(isNull(${camelName}s.deletedAt))
|
|
235
647
|
.limit(limit).offset(skip)
|
|
236
|
-
.orderBy(desc(${camelName}
|
|
648
|
+
.orderBy(desc(${camelName}s.createdAt));
|
|
237
649
|
|
|
238
650
|
return { data, total: 0 };
|
|
239
651
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
240
652
|
|
|
241
653
|
${ops.findOne ? `async findOne(id: string): Promise<${pascalName}Entity | null> {
|
|
242
|
-
const [result] = await this.db.select().from(${camelName}
|
|
243
|
-
.where(eq(${camelName}
|
|
654
|
+
const [result] = await this.db.select().from(${camelName}s)
|
|
655
|
+
.where(eq(${camelName}s.id, id));
|
|
244
656
|
return result || null;
|
|
245
657
|
}` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
246
658
|
|
|
247
659
|
${ops.update ? `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
248
|
-
const [result] = await this.db.update(${camelName}
|
|
660
|
+
const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s.id, id)).returning();
|
|
249
661
|
return result;
|
|
250
662
|
}` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
251
663
|
|
|
252
664
|
${ops.remove ? `async remove(id: string): Promise<${pascalName}Entity> {
|
|
253
|
-
const [result] = await this.db.update(${camelName}
|
|
665
|
+
const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s.id, id)).returning();
|
|
254
666
|
return result;
|
|
255
667
|
}` : `async remove(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
256
668
|
}
|
|
@@ -272,43 +684,42 @@ export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${cre
|
|
|
272
684
|
}
|
|
273
685
|
|
|
274
686
|
// DEFAULT (PRISMA)
|
|
687
|
+
const singularCamel = toSingularCamel(kebabName);
|
|
275
688
|
return `import { Injectable } from '@nestjs/common';
|
|
276
689
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
277
690
|
import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
|
|
278
691
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : ''}
|
|
279
692
|
|
|
280
|
-
|
|
281
|
-
// Assuming a model named \`${pascalName}\` exists in schema.prisma
|
|
282
|
-
type ${pascalName}Entity = any;
|
|
693
|
+
export type ${pascalName}Entity = any;
|
|
283
694
|
${!ops.create && !ops.update ? `\ntype ${createDtoName} = any;\ntype ${updateDtoName} = any;` : ''}
|
|
284
695
|
|
|
285
696
|
class Prisma${pascalName}Repository implements IBaseRepository<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
|
|
286
697
|
constructor(private readonly prisma: PrismaService) {}
|
|
287
698
|
|
|
288
699
|
${ops.create ? `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> {
|
|
289
|
-
return (this.prisma as any).${
|
|
700
|
+
return (this.prisma as any).${singularCamel}.create({ data: dto });
|
|
290
701
|
}` : `async create(dto: ${createDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
291
702
|
|
|
292
703
|
${ops.findAll ? `async findAll(pagination: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> {
|
|
293
704
|
const { page = 1, limit = 10 } = pagination;
|
|
294
705
|
const skip = (page - 1) * limit;
|
|
295
706
|
const [data, total] = await this.prisma.$transaction([
|
|
296
|
-
(this.prisma as any).${
|
|
297
|
-
(this.prisma as any).${
|
|
707
|
+
(this.prisma as any).${singularCamel}.findMany({ where: { deletedAt: null }, skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
708
|
+
(this.prisma as any).${singularCamel}.count({ where: { deletedAt: null } }),
|
|
298
709
|
]);
|
|
299
710
|
return { data, total };
|
|
300
711
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
301
712
|
|
|
302
713
|
${ops.findOne ? `async findOne(id: string): Promise<${pascalName}Entity | null> {
|
|
303
|
-
return (this.prisma as any).${
|
|
714
|
+
return (this.prisma as any).${singularCamel}.findFirst({ where: { id, deletedAt: null } });
|
|
304
715
|
}` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
305
716
|
|
|
306
717
|
${ops.update ? `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
307
|
-
return (this.prisma as any).${
|
|
718
|
+
return (this.prisma as any).${singularCamel}.update({ where: { id }, data: dto });
|
|
308
719
|
}` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
309
720
|
|
|
310
721
|
${ops.remove ? `async remove(id: string): Promise<${pascalName}Entity> {
|
|
311
|
-
return (this.prisma as any).${
|
|
722
|
+
return (this.prisma as any).${singularCamel}.update({ where: { id }, data: { deletedAt: new Date() } });
|
|
312
723
|
}` : `async remove(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
313
724
|
}
|
|
314
725
|
|
|
@@ -328,15 +739,18 @@ export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${cre
|
|
|
328
739
|
`;
|
|
329
740
|
}
|
|
330
741
|
|
|
331
|
-
function getModuleContent(orm, pascalName, kebabName) {
|
|
742
|
+
function getModuleContent(orm, pascalName, singularPascal, kebabName) {
|
|
743
|
+
const singularKebab = toSingularKebab(kebabName);
|
|
744
|
+
|
|
332
745
|
if (orm === 'typeorm') {
|
|
333
746
|
return `import { Module } from '@nestjs/common';
|
|
334
747
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
335
748
|
import { ${pascalName}Controller } from './${kebabName}.controller';
|
|
336
749
|
import { ${pascalName}Service } from './${kebabName}.service';
|
|
750
|
+
import { ${singularPascal} } from './entities/${singularKebab}.entity';
|
|
337
751
|
|
|
338
752
|
@Module({
|
|
339
|
-
imports: [TypeOrmModule.forFeature([
|
|
753
|
+
imports: [TypeOrmModule.forFeature([${singularPascal}])],
|
|
340
754
|
controllers: [${pascalName}Controller],
|
|
341
755
|
providers: [${pascalName}Service],
|
|
342
756
|
exports: [${pascalName}Service],
|
|
@@ -347,9 +761,10 @@ export class ${pascalName}Module {}`;
|
|
|
347
761
|
import { MongooseModule } from '@nestjs/mongoose';
|
|
348
762
|
import { ${pascalName}Controller } from './${kebabName}.controller';
|
|
349
763
|
import { ${pascalName}Service } from './${kebabName}.service';
|
|
764
|
+
import { ${singularPascal}, ${singularPascal}Schema } from './schemas/${singularKebab}.schema';
|
|
350
765
|
|
|
351
766
|
@Module({
|
|
352
|
-
imports: [MongooseModule.forFeature([{ name:
|
|
767
|
+
imports: [MongooseModule.forFeature([{ name: ${singularPascal}.name, schema: ${singularPascal}Schema }])],
|
|
353
768
|
controllers: [${pascalName}Controller],
|
|
354
769
|
providers: [${pascalName}Service],
|
|
355
770
|
exports: [${pascalName}Service],
|
|
@@ -389,29 +804,29 @@ export class ${pascalName}Module {}
|
|
|
389
804
|
async function registerInAppModule(targetDir, pascalName, kebabName) {
|
|
390
805
|
try {
|
|
391
806
|
const appModulePath = path.join(targetDir, 'src', 'app.module.ts');
|
|
392
|
-
|
|
807
|
+
|
|
393
808
|
if (!(await fs.pathExists(appModulePath))) {
|
|
394
809
|
return false;
|
|
395
810
|
}
|
|
396
811
|
|
|
397
812
|
let content = await fs.readFile(appModulePath, 'utf8');
|
|
398
813
|
const moduleImport = `import { ${pascalName}Module } from './modules/${kebabName}/${kebabName}.module';`;
|
|
399
|
-
|
|
814
|
+
|
|
400
815
|
// Prevent duplicate registration — use exact module name match
|
|
401
816
|
if (content.includes(moduleImport) || new RegExp(`\\b${pascalName}Module\\b`).test(content)) {
|
|
402
817
|
return true;
|
|
403
818
|
}
|
|
404
819
|
|
|
405
|
-
// 1.
|
|
820
|
+
// 1. Add import statement at top of file
|
|
406
821
|
content = `${moduleImport}\n` + content;
|
|
407
822
|
|
|
408
|
-
// 2. Inject ${pascalName}Module
|
|
823
|
+
// 2. Inject ${pascalName}Module inside @Module({ imports: [ ... ] })
|
|
409
824
|
const importsKeywordIndex = content.indexOf('imports: [');
|
|
410
|
-
|
|
825
|
+
|
|
411
826
|
if (importsKeywordIndex !== -1) {
|
|
412
827
|
const insertPosition = importsKeywordIndex + 'imports: ['.length;
|
|
413
828
|
content = content.slice(0, insertPosition) + `\n ${pascalName}Module,` + content.slice(insertPosition);
|
|
414
|
-
|
|
829
|
+
|
|
415
830
|
await fs.writeFile(appModulePath, content, 'utf8');
|
|
416
831
|
console.log(chalk.green(`✨ Automatically registered ${pascalName}Module in src/app.module.ts`));
|
|
417
832
|
return true;
|
|
@@ -422,15 +837,19 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
|
|
|
422
837
|
return false;
|
|
423
838
|
}
|
|
424
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Main Shared Interactive Module Generator entry point
|
|
842
|
+
*/
|
|
425
843
|
async function generateModule(providedModuleName, targetDir = process.cwd(), specifiedOrm = null) {
|
|
426
844
|
try {
|
|
427
845
|
const options = await promptForModuleOptions(providedModuleName);
|
|
428
846
|
const kebabName = toKebabCase(options.moduleName);
|
|
429
847
|
const pascalName = toPascalCase(options.moduleName);
|
|
430
848
|
const camelName = toCamelCase(options.moduleName);
|
|
849
|
+
const singularPascal = toSingularPascal(pascalName);
|
|
431
850
|
|
|
432
851
|
// Detect ORM
|
|
433
|
-
const orm = specifiedOrm || await detectOrm(targetDir);
|
|
852
|
+
const orm = specifiedOrm || (await detectOrm(targetDir));
|
|
434
853
|
|
|
435
854
|
// Ensure inside a NestJS project structure
|
|
436
855
|
const srcDir = path.join(targetDir, 'src');
|
|
@@ -438,7 +857,9 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
438
857
|
console.warn(chalk.yellow('⚠️ Could not find "src" directory. Generating at current directory.'));
|
|
439
858
|
}
|
|
440
859
|
|
|
441
|
-
const moduleDir = (await fs.pathExists(srcDir))
|
|
860
|
+
const moduleDir = (await fs.pathExists(srcDir))
|
|
861
|
+
? path.join(srcDir, 'modules', kebabName)
|
|
862
|
+
: path.join(targetDir, 'modules', kebabName);
|
|
442
863
|
const dtoDir = path.join(moduleDir, 'dto');
|
|
443
864
|
|
|
444
865
|
await fs.ensureDir(moduleDir);
|
|
@@ -449,30 +870,62 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
449
870
|
findAll: options.operations.includes('findAll'),
|
|
450
871
|
findOne: options.operations.includes('findOne'),
|
|
451
872
|
update: options.operations.includes('update'),
|
|
452
|
-
remove: options.operations.includes('remove')
|
|
873
|
+
remove: options.operations.includes('remove'),
|
|
453
874
|
};
|
|
454
875
|
|
|
455
|
-
// 1.
|
|
456
|
-
|
|
457
|
-
|
|
876
|
+
// 1. Dynamic ORM Schema Synchronization
|
|
877
|
+
if (orm === 'prisma') {
|
|
878
|
+
await syncPrismaSchema(targetDir, singularPascal, kebabName, options.fields);
|
|
879
|
+
} else if (orm === 'typeorm') {
|
|
880
|
+
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
881
|
+
} else if (orm === 'mongoose') {
|
|
882
|
+
await syncMongooseSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
883
|
+
} else if (orm === 'drizzle') {
|
|
884
|
+
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// 2. Generate DTOs
|
|
888
|
+
const createDtoName = `Create${singularPascal}Dto`;
|
|
889
|
+
const updateDtoName = `Update${singularPascal}Dto`;
|
|
458
890
|
const responseDtoName = `${pascalName}Dto`;
|
|
459
891
|
|
|
892
|
+
const createFieldsText = options.fields.map((f) => {
|
|
893
|
+
const tsType = getTsType(f.type);
|
|
894
|
+
const ex = getFieldExampleValue(f, pascalName);
|
|
895
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
896
|
+
|
|
897
|
+
const swaggerDecorator = f.isOptional
|
|
898
|
+
? `@ApiPropertyOptional({ description: '${f.name} property', example: ${exValStr} })`
|
|
899
|
+
: `@ApiProperty({ description: '${f.name} property', example: ${exValStr} })`;
|
|
900
|
+
|
|
901
|
+
const valDecorators = [];
|
|
902
|
+
if (f.type === 'String') valDecorators.push('@IsString()');
|
|
903
|
+
if (f.type === 'Number') valDecorators.push('@IsNumber()');
|
|
904
|
+
if (f.type === 'Boolean') valDecorators.push('@IsBoolean()');
|
|
905
|
+
if (f.type === 'Date') {
|
|
906
|
+
valDecorators.push('@IsDate()');
|
|
907
|
+
valDecorators.push('@Type(() => Date)');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (f.isOptional) {
|
|
911
|
+
valDecorators.push('@IsOptional()');
|
|
912
|
+
} else {
|
|
913
|
+
valDecorators.push('@IsNotEmpty()');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
917
|
+
}).join('\n\n');
|
|
918
|
+
|
|
919
|
+
const hasDateFields = options.fields.some((f) => f.type === 'Date');
|
|
920
|
+
|
|
460
921
|
if (ops.create || ops.update) {
|
|
461
922
|
await fs.writeFile(
|
|
462
923
|
path.join(dtoDir, `create-${kebabName}.dto.ts`),
|
|
463
924
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
464
|
-
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
|
|
465
|
-
|
|
925
|
+
import { IsString, IsNumber, IsBoolean, IsDate, IsNotEmpty, IsOptional } from 'class-validator';
|
|
926
|
+
${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
|
|
466
927
|
export class ${createDtoName} {
|
|
467
|
-
|
|
468
|
-
@IsString()
|
|
469
|
-
@IsNotEmpty()
|
|
470
|
-
name: string;
|
|
471
|
-
|
|
472
|
-
@ApiPropertyOptional({ description: 'Optional property', example: 'Optional value' })
|
|
473
|
-
@IsString()
|
|
474
|
-
@IsOptional()
|
|
475
|
-
description?: string;
|
|
928
|
+
${createFieldsText}
|
|
476
929
|
}
|
|
477
930
|
`
|
|
478
931
|
);
|
|
@@ -487,6 +940,18 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
|
487
940
|
);
|
|
488
941
|
}
|
|
489
942
|
|
|
943
|
+
const responseFieldsText = options.fields.map((f) => {
|
|
944
|
+
const tsType = getTsType(f.type);
|
|
945
|
+
const ex = getFieldExampleValue(f, pascalName);
|
|
946
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
947
|
+
|
|
948
|
+
const swaggerDecorator = f.isOptional
|
|
949
|
+
? `@ApiPropertyOptional({ example: ${exValStr} })`
|
|
950
|
+
: `@ApiProperty({ example: ${exValStr} })`;
|
|
951
|
+
|
|
952
|
+
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
953
|
+
}).join('\n\n');
|
|
954
|
+
|
|
490
955
|
await fs.writeFile(
|
|
491
956
|
path.join(dtoDir, `${kebabName}.dto.ts`),
|
|
492
957
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
@@ -495,11 +960,10 @@ export class ${responseDtoName} {
|
|
|
495
960
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
496
961
|
id: string;
|
|
497
962
|
|
|
498
|
-
|
|
499
|
-
name: string;
|
|
963
|
+
${responseFieldsText}
|
|
500
964
|
|
|
501
|
-
@
|
|
502
|
-
|
|
965
|
+
@ApiProperty({ example: 'ACTIVE' })
|
|
966
|
+
status: string;
|
|
503
967
|
|
|
504
968
|
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
505
969
|
createdAt: Date;
|
|
@@ -513,11 +977,20 @@ export class ${responseDtoName} {
|
|
|
513
977
|
`
|
|
514
978
|
);
|
|
515
979
|
|
|
516
|
-
//
|
|
517
|
-
const serviceContent = getServiceContent(
|
|
980
|
+
// 3. Generate Service
|
|
981
|
+
const serviceContent = getServiceContent(
|
|
982
|
+
orm,
|
|
983
|
+
pascalName,
|
|
984
|
+
singularPascal,
|
|
985
|
+
camelName,
|
|
986
|
+
kebabName,
|
|
987
|
+
createDtoName,
|
|
988
|
+
updateDtoName,
|
|
989
|
+
ops
|
|
990
|
+
);
|
|
518
991
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
|
|
519
992
|
|
|
520
|
-
//
|
|
993
|
+
// 4. Generate Controller
|
|
521
994
|
const controllerContent = `import { Controller${ops.findAll ? ', Query' : ''}${ops.findOne || ops.update || ops.remove ? ', Param, ParseUUIDPipe, HttpStatus' : ''}${ops.create ? ', Post, Body' : ''}${ops.findAll || ops.findOne ? ', Get' : ''}${ops.update ? ', Put' : ''}${ops.remove ? ', Delete' : ''}, Type } from '@nestjs/common';
|
|
522
995
|
import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
|
523
996
|
import { BaseController } from '../../common/base/base.controller';
|
|
@@ -526,7 +999,7 @@ import { ${pascalName}Service } from './${kebabName}.service';
|
|
|
526
999
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : `type ${createDtoName} = any;\ntype ${updateDtoName} = any;`}
|
|
527
1000
|
import { ${responseDtoName} } from './dto/${kebabName}.dto';
|
|
528
1001
|
|
|
529
|
-
type ${pascalName}Entity = any;
|
|
1002
|
+
type ${pascalName}Entity = any;
|
|
530
1003
|
|
|
531
1004
|
@ApiTags('${pascalName}')
|
|
532
1005
|
@ApiBearerAuth('bearer')
|
|
@@ -543,14 +1016,14 @@ export class ${pascalName}Controller extends BaseController<${pascalName}Entity,
|
|
|
543
1016
|
${ops.create ? `
|
|
544
1017
|
@Post()
|
|
545
1018
|
@ApiOperation({ summary: 'Create a new ${kebabName}' })
|
|
546
|
-
@ApiResponse({ status:
|
|
1019
|
+
@ApiResponse({ status: HttpStatus.CREATED, schema: ApiResponseSchema(${responseDtoName}) })
|
|
547
1020
|
override async create(@Body() dto: ${createDtoName}): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
548
1021
|
return super.create(dto);
|
|
549
1022
|
}
|
|
550
1023
|
` : ''}${ops.findAll ? `
|
|
551
1024
|
@Get()
|
|
552
1025
|
@ApiOperation({ summary: 'Get all ${kebabName} (paginated)' })
|
|
553
|
-
@ApiResponse({ status:
|
|
1026
|
+
@ApiResponse({ status: HttpStatus.OK, schema: PaginatedResponseSchema(${responseDtoName}) })
|
|
554
1027
|
override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<${pascalName}Entity>> {
|
|
555
1028
|
return super.findAll(pagination);
|
|
556
1029
|
}
|
|
@@ -558,8 +1031,8 @@ ${ops.create ? `
|
|
|
558
1031
|
@Get(':id')
|
|
559
1032
|
@ApiOperation({ summary: 'Get ${kebabName} by ID' })
|
|
560
1033
|
@ApiParam({ name: 'id', format: 'uuid' })
|
|
561
|
-
@ApiResponse({ status:
|
|
562
|
-
@ApiResponse({ status:
|
|
1034
|
+
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1035
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
563
1036
|
override async findOne(@Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
564
1037
|
return super.findOne(id);
|
|
565
1038
|
}
|
|
@@ -567,8 +1040,8 @@ ${ops.create ? `
|
|
|
567
1040
|
@Put(':id')
|
|
568
1041
|
@ApiOperation({ summary: 'Update ${kebabName} by ID' })
|
|
569
1042
|
@ApiParam({ name: 'id', format: 'uuid' })
|
|
570
|
-
@ApiResponse({ status:
|
|
571
|
-
@ApiResponse({ status:
|
|
1043
|
+
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1044
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
572
1045
|
override async update(
|
|
573
1046
|
@Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string,
|
|
574
1047
|
@Body() dto: ${updateDtoName}
|
|
@@ -579,8 +1052,8 @@ ${ops.create ? `
|
|
|
579
1052
|
@Delete(':id')
|
|
580
1053
|
@ApiOperation({ summary: 'Delete ${kebabName} by ID' })
|
|
581
1054
|
@ApiParam({ name: 'id', format: 'uuid' })
|
|
582
|
-
@ApiResponse({ status:
|
|
583
|
-
@ApiResponse({ status:
|
|
1055
|
+
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1056
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
584
1057
|
override async remove(@Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
585
1058
|
return super.remove(id);
|
|
586
1059
|
}
|
|
@@ -589,13 +1062,16 @@ ${ops.create ? `
|
|
|
589
1062
|
`;
|
|
590
1063
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.controller.ts`), controllerContent);
|
|
591
1064
|
|
|
592
|
-
//
|
|
593
|
-
const moduleContent = getModuleContent(orm, pascalName, kebabName);
|
|
1065
|
+
// 5. Generate Module
|
|
1066
|
+
const moduleContent = getModuleContent(orm, pascalName, singularPascal, kebabName);
|
|
594
1067
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.module.ts`), moduleContent);
|
|
595
1068
|
|
|
596
|
-
//
|
|
1069
|
+
// 6. Auto-register in src/app.module.ts
|
|
597
1070
|
const isAutoRegistered = await registerInAppModule(targetDir, pascalName, kebabName);
|
|
598
1071
|
|
|
1072
|
+
// 7. Auto-Generate Starter Seed File Template
|
|
1073
|
+
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, options.fields);
|
|
1074
|
+
|
|
599
1075
|
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
|
|
600
1076
|
console.log(chalk.gray(` Detected ORM: ${orm}`));
|
|
601
1077
|
|