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.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +1 -1
  3. package/src/generator.js +1 -16
  4. package/src/moduleGenerator.js +597 -121
  5. package/src/postSetup.js +423 -402
  6. package/src/prompts.js +25 -3
  7. package/templates/orm/drizzle/package.json +2 -1
  8. package/templates/orm/mongoose/package.json +2 -1
  9. package/templates/orm/prisma/package.json +1 -0
  10. package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +0 -34
  11. package/templates/base-crud/src/modules/products/dto/product.dto.ts +0 -36
  12. package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +0 -4
  13. package/templates/base-crud/src/modules/products/products.controller.ts +0 -78
  14. package/templates/base-crud-drizzle/src/modules/products/dto/create-product.dto.ts +0 -34
  15. package/templates/base-crud-drizzle/src/modules/products/dto/product.dto.ts +0 -36
  16. package/templates/base-crud-drizzle/src/modules/products/dto/update-product.dto.ts +0 -4
  17. package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +0 -78
  18. package/templates/base-crud-drizzle/src/modules/products/products.module.ts +0 -24
  19. package/templates/base-crud-drizzle/src/modules/products/products.service.ts +0 -140
  20. package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +0 -39
  21. package/templates/base-crud-mongoose/src/modules/products/dto/create-product.dto.ts +0 -34
  22. package/templates/base-crud-mongoose/src/modules/products/dto/product.dto.ts +0 -36
  23. package/templates/base-crud-mongoose/src/modules/products/dto/update-product.dto.ts +0 -4
  24. package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +0 -78
  25. package/templates/base-crud-mongoose/src/modules/products/products.module.ts +0 -26
  26. package/templates/base-crud-mongoose/src/modules/products/products.service.ts +0 -133
  27. package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +0 -67
  28. package/templates/base-crud-prisma/src/modules/products/dto/create-product.dto.ts +0 -34
  29. package/templates/base-crud-prisma/src/modules/products/dto/product.dto.ts +0 -36
  30. package/templates/base-crud-prisma/src/modules/products/dto/update-product.dto.ts +0 -4
  31. package/templates/base-crud-prisma/src/modules/products/products.controller.ts +0 -78
  32. package/templates/base-crud-prisma/src/modules/products/products.module.ts +0 -12
  33. package/templates/base-crud-prisma/src/modules/products/products.service.ts +0 -100
  34. package/templates/base-crud-typeorm/src/modules/products/dto/create-product.dto.ts +0 -34
  35. package/templates/base-crud-typeorm/src/modules/products/dto/product.dto.ts +0 -36
  36. package/templates/base-crud-typeorm/src/modules/products/dto/update-product.dto.ts +0 -4
  37. package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +0 -58
  38. package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +0 -78
  39. package/templates/base-crud-typeorm/src/modules/products/products.module.ts +0 -25
  40. package/templates/base-crud-typeorm/src/modules/products/products.service.ts +0 -102
@@ -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 = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) };
36
-
37
- if (deps['prisma']) return 'prisma';
38
- if (deps['typeorm']) return 'typeorm';
39
- if (deps['mongoose']) return 'mongoose';
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 (error) {
43
- // Ignore error and fallback to default
75
+ } catch {
76
+ // Fallback to prisma
44
77
  }
45
78
  return 'prisma';
46
79
  }
47
80
 
48
- async function promptForModuleOptions(moduleName) {
49
- const questions = [];
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
- questions.push({
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
- questions.push({
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 (answers.crudMode === 'custom') {
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: false },
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: false }
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: moduleName || answers.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, NotFoundException } from '@nestjs/common';
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
- // Placeholder for TypeORM Entity. Ensure you create entities/${kebabName}.entity.ts
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(Object /* Replace with your Entity class, e.g. ${pascalName}Entity */) private readonly repo: Repository<${pascalName}Entity>) {
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, NotFoundException } from '@nestjs/common';
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
- // Placeholder for Mongoose Document. Ensure you create schemas/${kebabName}.schema.ts
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}Document, ${createDtoName}, ${updateDtoName}> {
166
- constructor(private readonly model: Model<${pascalName}Document>) {}
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}Document> {
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}Document> { throw new Error('Not implemented'); }`}
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}Document[]; total: number }> {
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}Document[]; total: number }> { throw new Error('Not implemented'); }`}
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}Document | null> {
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}Document | null> { throw new Error('Not implemented'); }`}
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}Document> {
188
- return this.model.findByIdAndUpdate(id, dto as any, { new: true }).exec() as Promise<${pascalName}Document>;
189
- }` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Document> { throw new Error('Not implemented'); }`}
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}Document> {
192
- return this.model.findByIdAndUpdate(id, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}Document>;
193
- }` : `async remove(id: string): Promise<${pascalName}Document> { throw new Error('Not implemented'); }`}
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}Document, ${createDtoName}, ${updateDtoName}> {
611
+ export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
198
612
  private readonly repository: Mongoose${pascalName}Repository;
199
613
 
200
- constructor(@InjectModel('${pascalName}') private readonly model: Model<${pascalName}Document>) {
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}Document, ${createDtoName}, ${updateDtoName}> {
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, NotFoundException } from '@nestjs/common';
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
- // Placeholder for Drizzle schema
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}Schema).values(dto).returning();
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}Schema)
234
- .where(isNull(${camelName}Schema.deletedAt))
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}Schema.createdAt));
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}Schema)
243
- .where(eq(${camelName}Schema.id, id));
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}Schema).set(dto).where(eq(${camelName}Schema.id, id)).returning();
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}Schema).set({ deletedAt: new Date() }).where(eq(${camelName}Schema.id, id)).returning();
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
- // Placeholder for Prisma Model type.
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).${camelName}.create({ data: dto });
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).${camelName}.findMany({ where: { deletedAt: null }, skip, take: limit, orderBy: { createdAt: 'desc' } }),
297
- (this.prisma as any).${camelName}.count({ where: { deletedAt: null } }),
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).${camelName}.findFirst({ where: { id, deletedAt: null } });
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).${camelName}.update({ where: { id }, data: dto });
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).${camelName}.update({ where: { id }, data: { deletedAt: new Date() } });
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([/* ${pascalName}Entity */])],
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: '${pascalName}', schema: null /* ${pascalName}Schema */ }])],
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. Tambahkan baris import di paling atas file
820
+ // 1. Add import statement at top of file
406
821
  content = `${moduleImport}\n` + content;
407
822
 
408
- // 2. Inject ${pascalName}Module tepat di dalam decorator @Module({ imports: [ ... ] })
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)) ? path.join(srcDir, 'modules', kebabName) : path.join(targetDir, 'modules', kebabName);
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. Generate DTOs
456
- const createDtoName = `Create${pascalName}Dto`;
457
- const updateDtoName = `Update${pascalName}Dto`;
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
- @ApiProperty({ description: 'Example property', example: 'Example value' })
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
- @ApiProperty({ example: 'Example value' })
499
- name: string;
963
+ ${responseFieldsText}
500
964
 
501
- @ApiPropertyOptional({ example: 'Optional value' })
502
- description?: string;
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
- // 2. Generate Service
517
- const serviceContent = getServiceContent(orm, pascalName, camelName, kebabName, createDtoName, updateDtoName, ops);
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
- // 3. Generate Controller
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: 201, description: '${pascalName} successfully created', schema: ApiResponseSchema(${responseDtoName}) })
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: 200, description: 'Paginated ${kebabName}', schema: PaginatedResponseSchema(${responseDtoName}) })
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: 200, description: '${pascalName} found', schema: ApiResponseSchema(${responseDtoName}) })
562
- @ApiResponse({ status: 404, description: '${pascalName} not found' })
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: 200, description: '${pascalName} successfully updated', schema: ApiResponseSchema(${responseDtoName}) })
571
- @ApiResponse({ status: 404, description: '${pascalName} not found' })
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: 200, description: '${pascalName} successfully deleted', schema: ApiResponseSchema(${responseDtoName}) })
583
- @ApiResponse({ status: 404, description: '${pascalName} not found' })
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
- // 4. Generate Module
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
- // 5. Auto-register in src/app.module.ts
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