@forinda/kickjs-cli 1.1.3 → 1.2.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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/generators/module.ts
5
5
  import { join } from "path";
6
+ import { createInterface } from "readline";
6
7
 
7
8
  // src/utils/fs.ts
8
9
  import { writeFile, mkdir, access, readFile } from "fs/promises";
@@ -59,9 +60,25 @@ __name(pluralizePascal, "pluralizePascal");
59
60
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
60
61
 
61
62
  // src/generators/templates/module-index.ts
63
+ function repoMaps(pascal, kebab, repo) {
64
+ const repoClassMap = {
65
+ inmemory: `InMemory${pascal}Repository`,
66
+ drizzle: `Drizzle${pascal}Repository`,
67
+ prisma: `Prisma${pascal}Repository`
68
+ };
69
+ const repoFileMap = {
70
+ inmemory: `in-memory-${kebab}`,
71
+ drizzle: `drizzle-${kebab}`,
72
+ prisma: `prisma-${kebab}`
73
+ };
74
+ return {
75
+ repoClass: repoClassMap[repo] ?? repoClassMap.inmemory,
76
+ repoFile: repoFileMap[repo] ?? repoFileMap.inmemory
77
+ };
78
+ }
79
+ __name(repoMaps, "repoMaps");
62
80
  function generateModuleIndex(pascal, kebab, plural, repo) {
63
- const repoClass = repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`;
64
- const repoFile = repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`;
81
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
65
82
  return `/**
66
83
  * ${pascal} Module
67
84
  *
@@ -114,6 +131,65 @@ export class ${pascal}Module implements AppModule {
114
131
  `;
115
132
  }
116
133
  __name(generateModuleIndex, "generateModuleIndex");
134
+ function generateRestModuleIndex(pascal, kebab, plural, repo) {
135
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
136
+ return `/**
137
+ * ${pascal} Module
138
+ *
139
+ * REST module with a flat folder structure.
140
+ * Controller delegates to service, service wraps the repository.
141
+ *
142
+ * Structure:
143
+ * ${kebab}.controller.ts \u2014 HTTP routes (CRUD)
144
+ * ${kebab}.service.ts \u2014 Business logic
145
+ * ${kebab}.repository.ts \u2014 Repository interface
146
+ * ${repoFile}.repository.ts \u2014 Repository implementation
147
+ * dtos/ \u2014 Request/response schemas
148
+ */
149
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
150
+ import { buildRoutes } from '@forinda/kickjs-http'
151
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
152
+ import { ${repoClass} } from './${repoFile}.repository'
153
+ import { ${pascal}Controller } from './${kebab}.controller'
154
+
155
+ // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
156
+ import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })
157
+
158
+ export class ${pascal}Module implements AppModule {
159
+ register(container: Container): void {
160
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
161
+ container.resolve(${repoClass}),
162
+ )
163
+ }
164
+
165
+ routes(): ModuleRoutes {
166
+ return {
167
+ path: '/${plural}',
168
+ router: buildRoutes(${pascal}Controller),
169
+ controller: ${pascal}Controller,
170
+ }
171
+ }
172
+ }
173
+ `;
174
+ }
175
+ __name(generateRestModuleIndex, "generateRestModuleIndex");
176
+ function generateMinimalModuleIndex(pascal, kebab, plural) {
177
+ return `import { type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
178
+ import { buildRoutes } from '@forinda/kickjs-http'
179
+ import { ${pascal}Controller } from './${kebab}.controller'
180
+
181
+ export class ${pascal}Module implements AppModule {
182
+ routes(): ModuleRoutes {
183
+ return {
184
+ path: '/${plural}',
185
+ router: buildRoutes(${pascal}Controller),
186
+ controller: ${pascal}Controller,
187
+ }
188
+ }
189
+ }
190
+ `;
191
+ }
192
+ __name(generateMinimalModuleIndex, "generateMinimalModuleIndex");
117
193
 
118
194
  // src/generators/templates/controller.ts
119
195
  function generateController(pascal, kebab, plural, pluralPascal) {
@@ -179,6 +255,62 @@ export class ${pascal}Controller {
179
255
  `;
180
256
  }
181
257
  __name(generateController, "generateController");
258
+ function generateRestController(pascal, kebab, plural, pluralPascal) {
259
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
260
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
261
+ import type { RequestContext } from '@forinda/kickjs-http'
262
+ import { ApiTags } from '@forinda/kickjs-swagger'
263
+ import { ${pascal}Service } from './${kebab}.service'
264
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
265
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
266
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
267
+
268
+ @Controller()
269
+ export class ${pascal}Controller {
270
+ @Autowired() private ${camel}Service!: ${pascal}Service
271
+
272
+ @Get('/')
273
+ @ApiTags('${pascal}')
274
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
275
+ async list(ctx: RequestContext) {
276
+ return ctx.paginate(
277
+ (parsed) => this.${camel}Service.findPaginated(parsed),
278
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
279
+ )
280
+ }
281
+
282
+ @Get('/:id')
283
+ @ApiTags('${pascal}')
284
+ async getById(ctx: RequestContext) {
285
+ const result = await this.${camel}Service.findById(ctx.params.id)
286
+ if (!result) return ctx.notFound('${pascal} not found')
287
+ ctx.json(result)
288
+ }
289
+
290
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
291
+ @ApiTags('${pascal}')
292
+ async create(ctx: RequestContext) {
293
+ const result = await this.${camel}Service.create(ctx.body)
294
+ ctx.created(result)
295
+ }
296
+
297
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
298
+ @ApiTags('${pascal}')
299
+ async update(ctx: RequestContext) {
300
+ const result = await this.${camel}Service.update(ctx.params.id, ctx.body)
301
+ ctx.json(result)
302
+ }
303
+
304
+ @Delete('/:id')
305
+ @ApiTags('${pascal}')
306
+ async remove(ctx: RequestContext) {
307
+ await this.${camel}Service.delete(ctx.params.id)
308
+ ctx.noContent()
309
+ }
310
+ }
311
+ `;
312
+ }
313
+ __name(generateRestController, "generateRestController");
182
314
 
183
315
  // src/generators/templates/constants.ts
184
316
  function generateConstants(pascal) {
@@ -342,20 +474,19 @@ export class Delete${pascal}UseCase {
342
474
  __name(generateUseCases, "generateUseCases");
343
475
 
344
476
  // src/generators/templates/repository.ts
345
- function generateRepositoryInterface(pascal, kebab) {
477
+ function generateRepositoryInterface(pascal, kebab, dtoPrefix = "../../application/dtos") {
346
478
  return `/**
347
479
  * ${pascal} Repository Interface
348
480
  *
349
- * Domain layer \u2014 defines the contract for data access.
350
- * The interface lives in the domain layer; implementations live in infrastructure.
351
- * This inversion of dependencies keeps the domain pure and testable.
481
+ * Defines the contract for data access.
482
+ * The interface declares what operations are available;
483
+ * implementations (in-memory, Drizzle, Prisma) fulfill the contract.
352
484
  *
353
- * To swap implementations (e.g. in-memory -> Drizzle -> Prisma),
354
- * change the factory in the module's register() method.
485
+ * To swap implementations, change the factory in the module's register() method.
355
486
  */
356
- import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
357
- import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
358
- import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
487
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
488
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
489
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
359
490
  import type { ParsedQuery } from '@forinda/kickjs-http'
360
491
 
361
492
  export interface I${pascal}Repository {
@@ -371,11 +502,11 @@ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
371
502
  `;
372
503
  }
373
504
  __name(generateRepositoryInterface, "generateRepositoryInterface");
374
- function generateInMemoryRepository(pascal, kebab) {
505
+ function generateInMemoryRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
375
506
  return `/**
376
507
  * In-Memory ${pascal} Repository
377
508
  *
378
- * Infrastructure layer \u2014 implements the repository interface using a Map.
509
+ * Implements the repository interface using a Map.
379
510
  * Useful for prototyping and testing. Replace with a database implementation
380
511
  * (Drizzle, Prisma, etc.) for production use.
381
512
  *
@@ -384,10 +515,10 @@ function generateInMemoryRepository(pascal, kebab) {
384
515
  import { randomUUID } from 'node:crypto'
385
516
  import { Repository, HttpException } from '@forinda/kickjs-core'
386
517
  import type { ParsedQuery } from '@forinda/kickjs-http'
387
- import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
388
- import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
389
- import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
390
- import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
518
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
519
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
520
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
521
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
391
522
 
392
523
  @Repository()
393
524
  export class InMemory${pascal}Repository implements I${pascal}Repository {
@@ -435,6 +566,162 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
435
566
  `;
436
567
  }
437
568
  __name(generateInMemoryRepository, "generateInMemoryRepository");
569
+ function generateDrizzleRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
570
+ return `/**
571
+ * Drizzle ${pascal} Repository
572
+ *
573
+ * Implements the repository interface using Drizzle ORM.
574
+ * Requires a Drizzle database instance injected via the DI container.
575
+ *
576
+ * TODO: Update the schema import to match your Drizzle schema file.
577
+ * TODO: Replace 'db' injection token with your actual database token.
578
+ *
579
+ * @Repository() registers this class in the DI container as a singleton.
580
+ */
581
+ import { eq, sql } from 'drizzle-orm'
582
+ import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
583
+ import type { ParsedQuery } from '@forinda/kickjs-http'
584
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
585
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
586
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
587
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
588
+
589
+ // TODO: Import your Drizzle schema table \u2014 e.g.:
590
+ // import { ${kebab}s } from '@/db/schema'
591
+
592
+ // TODO: Import your Drizzle DB injection token \u2014 e.g.:
593
+ // import { DRIZZLE_DB } from '@/db/drizzle.provider'
594
+
595
+ @Repository()
596
+ export class Drizzle${pascal}Repository implements I${pascal}Repository {
597
+ // TODO: Uncomment and configure your Drizzle DB injection:
598
+ // @Autowired(DRIZZLE_DB) private db!: DrizzleDB
599
+
600
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
601
+ // TODO: Implement with Drizzle
602
+ // const [row] = await this.db.select().from(${kebab}s).where(eq(${kebab}s.id, id))
603
+ // return row ?? null
604
+ throw new Error('Drizzle ${pascal} repository not yet implemented \u2014 update schema imports and queries')
605
+ }
606
+
607
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
608
+ // TODO: Implement with Drizzle
609
+ // return this.db.select().from(${kebab}s)
610
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
611
+ }
612
+
613
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
614
+ // TODO: Implement with Drizzle
615
+ // const data = await this.db.select().from(${kebab}s)
616
+ // .limit(parsed.pagination.limit)
617
+ // .offset(parsed.pagination.offset)
618
+ // const [{ count }] = await this.db.select({ count: sql\`count(*)\` }).from(${kebab}s)
619
+ // return { data, total: Number(count) }
620
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
621
+ }
622
+
623
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
624
+ // TODO: Implement with Drizzle
625
+ // const [row] = await this.db.insert(${kebab}s).values(dto).returning()
626
+ // return row
627
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
628
+ }
629
+
630
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
631
+ // TODO: Implement with Drizzle
632
+ // const [row] = await this.db.update(${kebab}s).set(dto).where(eq(${kebab}s.id, id)).returning()
633
+ // if (!row) throw HttpException.notFound('${pascal} not found')
634
+ // return row
635
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
636
+ }
637
+
638
+ async delete(id: string): Promise<void> {
639
+ // TODO: Implement with Drizzle
640
+ // const result = await this.db.delete(${kebab}s).where(eq(${kebab}s.id, id))
641
+ // if (!result.rowCount) throw HttpException.notFound('${pascal} not found')
642
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
643
+ }
644
+ }
645
+ `;
646
+ }
647
+ __name(generateDrizzleRepository, "generateDrizzleRepository");
648
+ function generatePrismaRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
649
+ const camel = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
650
+ return `/**
651
+ * Prisma ${pascal} Repository
652
+ *
653
+ * Implements the repository interface using Prisma Client.
654
+ * Requires a PrismaClient instance injected via the DI container.
655
+ *
656
+ * TODO: Ensure your Prisma schema has a '${pascal}' model defined.
657
+ * TODO: Replace 'PRISMA_CLIENT' with your actual Prisma injection token.
658
+ *
659
+ * @Repository() registers this class in the DI container as a singleton.
660
+ */
661
+ import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
662
+ import type { ParsedQuery } from '@forinda/kickjs-http'
663
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
664
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
665
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
666
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
667
+
668
+ // TODO: Import your Prisma injection token \u2014 e.g.:
669
+ // import { PRISMA_CLIENT } from '@/db/prisma.provider'
670
+ // import type { PrismaClient } from '@prisma/client'
671
+
672
+ @Repository()
673
+ export class Prisma${pascal}Repository implements I${pascal}Repository {
674
+ // TODO: Uncomment and configure your Prisma injection:
675
+ // @Autowired(PRISMA_CLIENT) private prisma!: PrismaClient
676
+
677
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
678
+ // TODO: Implement with Prisma
679
+ // return this.prisma.${camel}.findUnique({ where: { id } })
680
+ throw new Error('Prisma ${pascal} repository not yet implemented \u2014 update Prisma imports and queries')
681
+ }
682
+
683
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
684
+ // TODO: Implement with Prisma
685
+ // return this.prisma.${camel}.findMany()
686
+ throw new Error('Prisma ${pascal} repository not yet implemented')
687
+ }
688
+
689
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
690
+ // TODO: Implement with Prisma
691
+ // const [data, total] = await Promise.all([
692
+ // this.prisma.${camel}.findMany({
693
+ // skip: parsed.pagination.offset,
694
+ // take: parsed.pagination.limit,
695
+ // }),
696
+ // this.prisma.${camel}.count(),
697
+ // ])
698
+ // return { data, total }
699
+ throw new Error('Prisma ${pascal} repository not yet implemented')
700
+ }
701
+
702
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
703
+ // TODO: Implement with Prisma
704
+ // return this.prisma.${camel}.create({ data: dto })
705
+ throw new Error('Prisma ${pascal} repository not yet implemented')
706
+ }
707
+
708
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
709
+ // TODO: Implement with Prisma
710
+ // const row = await this.prisma.${camel}.update({ where: { id }, data: dto })
711
+ // if (!row) throw HttpException.notFound('${pascal} not found')
712
+ // return row
713
+ throw new Error('Prisma ${pascal} repository not yet implemented')
714
+ }
715
+
716
+ async delete(id: string): Promise<void> {
717
+ // TODO: Implement with Prisma
718
+ // await this.prisma.${camel}.delete({ where: { id } })
719
+ throw new Error('Prisma ${pascal} repository not yet implemented')
720
+ }
721
+ }
722
+ `;
723
+ }
724
+ __name(generatePrismaRepository, "generatePrismaRepository");
438
725
 
439
726
  // src/generators/templates/domain.ts
440
727
  function generateDomainService(pascal, kebab) {
@@ -633,9 +920,9 @@ describe('${pascal}Controller', () => {
633
920
  `;
634
921
  }
635
922
  __name(generateControllerTest, "generateControllerTest");
636
- function generateRepositoryTest(pascal, kebab, plural) {
923
+ function generateRepositoryTest(pascal, kebab, plural, repoImport = `../infrastructure/repositories/in-memory-${kebab}.repository`) {
637
924
  return `import { describe, it, expect, beforeEach } from 'vitest'
638
- import { InMemory${pascal}Repository } from '../infrastructure/repositories/in-memory-${kebab}.repository'
925
+ import { InMemory${pascal}Repository } from '${repoImport}'
639
926
 
640
927
  describe('InMemory${pascal}Repository', () => {
641
928
  let repo: InMemory${pascal}Repository
@@ -700,21 +987,553 @@ describe('InMemory${pascal}Repository', () => {
700
987
  }
701
988
  __name(generateRepositoryTest, "generateRepositoryTest");
702
989
 
990
+ // src/generators/templates/rest-service.ts
991
+ function generateRestService(pascal, kebab) {
992
+ return `import { Service, Inject, HttpException } from '@forinda/kickjs-core'
993
+ import type { ParsedQuery } from '@forinda/kickjs-http'
994
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from './${kebab}.repository'
995
+ import type { ${pascal}ResponseDTO } from './dtos/${kebab}-response.dto'
996
+ import type { Create${pascal}DTO } from './dtos/create-${kebab}.dto'
997
+ import type { Update${pascal}DTO } from './dtos/update-${kebab}.dto'
998
+
999
+ @Service()
1000
+ export class ${pascal}Service {
1001
+ constructor(
1002
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1003
+ ) {}
1004
+
1005
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1006
+ return this.repo.findById(id)
1007
+ }
1008
+
1009
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1010
+ return this.repo.findAll()
1011
+ }
1012
+
1013
+ async findPaginated(parsed: ParsedQuery) {
1014
+ return this.repo.findPaginated(parsed)
1015
+ }
1016
+
1017
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1018
+ return this.repo.create(dto)
1019
+ }
1020
+
1021
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1022
+ return this.repo.update(id, dto)
1023
+ }
1024
+
1025
+ async delete(id: string): Promise<void> {
1026
+ await this.repo.delete(id)
1027
+ }
1028
+ }
1029
+ `;
1030
+ }
1031
+ __name(generateRestService, "generateRestService");
1032
+ function generateRestConstants(pascal) {
1033
+ return `import type { QueryFieldConfig } from '@forinda/kickjs-http'
1034
+
1035
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
1036
+ filterable: ['name'],
1037
+ sortable: ['name', 'createdAt'],
1038
+ searchable: ['name'],
1039
+ }
1040
+ `;
1041
+ }
1042
+ __name(generateRestConstants, "generateRestConstants");
1043
+
1044
+ // src/generators/templates/cqrs.ts
1045
+ function generateCqrsModuleIndex(pascal, kebab, plural, repo) {
1046
+ const repoClassMap = {
1047
+ inmemory: `InMemory${pascal}Repository`,
1048
+ drizzle: `Drizzle${pascal}Repository`,
1049
+ prisma: `Prisma${pascal}Repository`
1050
+ };
1051
+ const repoFileMap = {
1052
+ inmemory: `in-memory-${kebab}`,
1053
+ drizzle: `drizzle-${kebab}`,
1054
+ prisma: `prisma-${kebab}`
1055
+ };
1056
+ const repoClass = repoClassMap[repo] ?? repoClassMap.inmemory;
1057
+ const repoFile = repoFileMap[repo] ?? repoFileMap.inmemory;
1058
+ return `/**
1059
+ * ${pascal} Module \u2014 CQRS Pattern
1060
+ *
1061
+ * Separates read (queries) and write (commands) operations.
1062
+ * Events are emitted after state changes and can be handled via
1063
+ * WebSocket broadcasts, queue jobs, or ETL pipelines.
1064
+ *
1065
+ * Structure:
1066
+ * commands/ \u2014 Write operations (create, update, delete)
1067
+ * queries/ \u2014 Read operations (get, list)
1068
+ * events/ \u2014 Domain events + handlers (WS broadcast, queue dispatch)
1069
+ * dtos/ \u2014 Request/response schemas
1070
+ */
1071
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
1072
+ import { buildRoutes } from '@forinda/kickjs-http'
1073
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
1074
+ import { ${repoClass} } from './${repoFile}.repository'
1075
+ import { ${pascal}Controller } from './${kebab}.controller'
1076
+
1077
+ // Eagerly load decorated classes
1078
+ import.meta.glob(
1079
+ [
1080
+ './commands/**/*.ts',
1081
+ './queries/**/*.ts',
1082
+ './events/**/*.ts',
1083
+ '!./**/*.test.ts',
1084
+ ],
1085
+ { eager: true },
1086
+ )
1087
+
1088
+ export class ${pascal}Module implements AppModule {
1089
+ register(container: Container): void {
1090
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
1091
+ container.resolve(${repoClass}),
1092
+ )
1093
+ }
1094
+
1095
+ routes(): ModuleRoutes {
1096
+ return {
1097
+ path: '/${plural}',
1098
+ router: buildRoutes(${pascal}Controller),
1099
+ controller: ${pascal}Controller,
1100
+ }
1101
+ }
1102
+ }
1103
+ `;
1104
+ }
1105
+ __name(generateCqrsModuleIndex, "generateCqrsModuleIndex");
1106
+ function generateCqrsController(pascal, kebab, plural, pluralPascal) {
1107
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
1108
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
1109
+ import type { RequestContext } from '@forinda/kickjs-http'
1110
+ import { ApiTags } from '@forinda/kickjs-swagger'
1111
+ import { Create${pascal}Command } from './commands/create-${kebab}.command'
1112
+ import { Update${pascal}Command } from './commands/update-${kebab}.command'
1113
+ import { Delete${pascal}Command } from './commands/delete-${kebab}.command'
1114
+ import { Get${pascal}Query } from './queries/get-${kebab}.query'
1115
+ import { List${pluralPascal}Query } from './queries/list-${plural}.query'
1116
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
1117
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
1118
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
1119
+
1120
+ @Controller()
1121
+ export class ${pascal}Controller {
1122
+ @Autowired() private create${pascal}Command!: Create${pascal}Command
1123
+ @Autowired() private update${pascal}Command!: Update${pascal}Command
1124
+ @Autowired() private delete${pascal}Command!: Delete${pascal}Command
1125
+ @Autowired() private get${pascal}Query!: Get${pascal}Query
1126
+ @Autowired() private list${pluralPascal}Query!: List${pluralPascal}Query
1127
+
1128
+ @Get('/')
1129
+ @ApiTags('${pascal}')
1130
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
1131
+ async list(ctx: RequestContext) {
1132
+ return ctx.paginate(
1133
+ (parsed) => this.list${pluralPascal}Query.execute(parsed),
1134
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
1135
+ )
1136
+ }
1137
+
1138
+ @Get('/:id')
1139
+ @ApiTags('${pascal}')
1140
+ async getById(ctx: RequestContext) {
1141
+ const result = await this.get${pascal}Query.execute(ctx.params.id)
1142
+ if (!result) return ctx.notFound('${pascal} not found')
1143
+ ctx.json(result)
1144
+ }
1145
+
1146
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
1147
+ @ApiTags('${pascal}')
1148
+ async create(ctx: RequestContext) {
1149
+ const result = await this.create${pascal}Command.execute(ctx.body)
1150
+ ctx.created(result)
1151
+ }
1152
+
1153
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
1154
+ @ApiTags('${pascal}')
1155
+ async update(ctx: RequestContext) {
1156
+ const result = await this.update${pascal}Command.execute(ctx.params.id, ctx.body)
1157
+ ctx.json(result)
1158
+ }
1159
+
1160
+ @Delete('/:id')
1161
+ @ApiTags('${pascal}')
1162
+ async remove(ctx: RequestContext) {
1163
+ await this.delete${pascal}Command.execute(ctx.params.id)
1164
+ ctx.noContent()
1165
+ }
1166
+ }
1167
+ `;
1168
+ }
1169
+ __name(generateCqrsController, "generateCqrsController");
1170
+ function generateCqrsCommands(pascal, kebab) {
1171
+ return [
1172
+ {
1173
+ file: `create-${kebab}.command.ts`,
1174
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1175
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1176
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
1177
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1178
+ import { ${pascal}Events } from '../events/${kebab}.events'
1179
+
1180
+ @Service()
1181
+ export class Create${pascal}Command {
1182
+ constructor(
1183
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1184
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1185
+ ) {}
1186
+
1187
+ async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1188
+ const result = await this.repo.create(dto)
1189
+ this.events.emit('${kebab}.created', result)
1190
+ return result
1191
+ }
1192
+ }
1193
+ `
1194
+ },
1195
+ {
1196
+ file: `update-${kebab}.command.ts`,
1197
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1198
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1199
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
1200
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1201
+ import { ${pascal}Events } from '../events/${kebab}.events'
1202
+
1203
+ @Service()
1204
+ export class Update${pascal}Command {
1205
+ constructor(
1206
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1207
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1208
+ ) {}
1209
+
1210
+ async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1211
+ const result = await this.repo.update(id, dto)
1212
+ this.events.emit('${kebab}.updated', result)
1213
+ return result
1214
+ }
1215
+ }
1216
+ `
1217
+ },
1218
+ {
1219
+ file: `delete-${kebab}.command.ts`,
1220
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1221
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1222
+ import { ${pascal}Events } from '../events/${kebab}.events'
1223
+
1224
+ @Service()
1225
+ export class Delete${pascal}Command {
1226
+ constructor(
1227
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1228
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1229
+ ) {}
1230
+
1231
+ async execute(id: string): Promise<void> {
1232
+ await this.repo.delete(id)
1233
+ this.events.emit('${kebab}.deleted', { id })
1234
+ }
1235
+ }
1236
+ `
1237
+ }
1238
+ ];
1239
+ }
1240
+ __name(generateCqrsCommands, "generateCqrsCommands");
1241
+ function generateCqrsQueries(pascal, kebab, plural, pluralPascal) {
1242
+ return [
1243
+ {
1244
+ file: `get-${kebab}.query.ts`,
1245
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1246
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1247
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1248
+
1249
+ @Service()
1250
+ export class Get${pascal}Query {
1251
+ constructor(
1252
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1253
+ ) {}
1254
+
1255
+ async execute(id: string): Promise<${pascal}ResponseDTO | null> {
1256
+ return this.repo.findById(id)
1257
+ }
1258
+ }
1259
+ `
1260
+ },
1261
+ {
1262
+ file: `list-${plural}.query.ts`,
1263
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1264
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1265
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1266
+
1267
+ @Service()
1268
+ export class List${pluralPascal}Query {
1269
+ constructor(
1270
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1271
+ ) {}
1272
+
1273
+ async execute(parsed: ParsedQuery) {
1274
+ return this.repo.findPaginated(parsed)
1275
+ }
1276
+ }
1277
+ `
1278
+ }
1279
+ ];
1280
+ }
1281
+ __name(generateCqrsQueries, "generateCqrsQueries");
1282
+ function generateCqrsEvents(pascal, kebab) {
1283
+ return [
1284
+ {
1285
+ file: `${kebab}.events.ts`,
1286
+ content: `import { Service } from '@forinda/kickjs-core'
1287
+ import { EventEmitter } from 'node:events'
1288
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1289
+
1290
+ /**
1291
+ * ${pascal} domain event types.
1292
+ *
1293
+ * These events are emitted by commands after state changes.
1294
+ * Subscribe to them in event handlers for side effects:
1295
+ * - WebSocket broadcasts (real-time UI updates)
1296
+ * - Queue jobs (async processing, ETL pipelines)
1297
+ * - Audit logging
1298
+ * - Cache invalidation
1299
+ */
1300
+ export interface ${pascal}EventMap {
1301
+ '${kebab}.created': ${pascal}ResponseDTO
1302
+ '${kebab}.updated': ${pascal}ResponseDTO
1303
+ '${kebab}.deleted': { id: string }
1304
+ }
1305
+
1306
+ @Service()
1307
+ export class ${pascal}Events {
1308
+ private emitter = new EventEmitter()
1309
+
1310
+ emit<K extends keyof ${pascal}EventMap>(event: K, data: ${pascal}EventMap[K]): void {
1311
+ this.emitter.emit(event, data)
1312
+ }
1313
+
1314
+ on<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1315
+ this.emitter.on(event, handler)
1316
+ }
1317
+
1318
+ off<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1319
+ this.emitter.off(event, handler)
1320
+ }
1321
+ }
1322
+ `
1323
+ },
1324
+ {
1325
+ file: `on-${kebab}-change.handler.ts`,
1326
+ content: `import { Service, Autowired } from '@forinda/kickjs-core'
1327
+ import { ${pascal}Events } from './${kebab}.events'
1328
+
1329
+ /**
1330
+ * ${pascal} Change Event Handler
1331
+ *
1332
+ * Reacts to domain events emitted by commands.
1333
+ * Wire up side effects here:
1334
+ *
1335
+ * 1. WebSocket broadcast \u2014 notify connected clients in real-time
1336
+ * import { WsGateway } from '@forinda/kickjs-ws'
1337
+ * this.ws.broadcast('${kebab}-channel', { event, data })
1338
+ *
1339
+ * 2. Queue dispatch \u2014 offload heavy processing to background workers
1340
+ * import { QueueService } from '@forinda/kickjs-queue'
1341
+ * this.queue.add('${kebab}-etl', { action: event, payload: data })
1342
+ *
1343
+ * 3. ETL pipeline \u2014 transform and load data to external systems
1344
+ * await this.etlPipeline.process(data)
1345
+ */
1346
+ @Service()
1347
+ export class On${pascal}ChangeHandler {
1348
+ @Autowired() private events!: ${pascal}Events
1349
+
1350
+ // Uncomment to inject WebSocket and Queue services:
1351
+ // @Autowired() private ws!: WsGateway
1352
+ // @Autowired() private queue!: QueueService
1353
+
1354
+ onInit(): void {
1355
+ this.events.on('${kebab}.created', (data) => {
1356
+ console.log('[${pascal}] Created:', data.id)
1357
+ // TODO: Broadcast via WebSocket
1358
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.created', data })
1359
+ // TODO: Dispatch to queue for async processing / ETL
1360
+ // this.queue.add('${kebab}-etl', { action: 'create', payload: data })
1361
+ })
1362
+
1363
+ this.events.on('${kebab}.updated', (data) => {
1364
+ console.log('[${pascal}] Updated:', data.id)
1365
+ // TODO: Broadcast via WebSocket
1366
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.updated', data })
1367
+ })
1368
+
1369
+ this.events.on('${kebab}.deleted', (data) => {
1370
+ console.log('[${pascal}] Deleted:', data.id)
1371
+ // TODO: Broadcast via WebSocket
1372
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.deleted', data })
1373
+ })
1374
+ }
1375
+ }
1376
+ `
1377
+ }
1378
+ ];
1379
+ }
1380
+ __name(generateCqrsEvents, "generateCqrsEvents");
1381
+
703
1382
  // src/generators/module.ts
1383
+ function promptUser(question) {
1384
+ const rl = createInterface({
1385
+ input: process.stdin,
1386
+ output: process.stdout
1387
+ });
1388
+ return new Promise((resolve2) => {
1389
+ rl.question(question, (answer) => {
1390
+ rl.close();
1391
+ resolve2(answer.trim().toLowerCase());
1392
+ });
1393
+ });
1394
+ }
1395
+ __name(promptUser, "promptUser");
704
1396
  async function generateModule(options) {
705
- const { name, modulesDir, noEntity, noTests, repo = "inmemory", minimal } = options;
1397
+ const { name, modulesDir, noEntity, noTests, repo = "inmemory", force } = options;
1398
+ let pattern = options.pattern ?? "ddd";
1399
+ if (options.minimal) pattern = "minimal";
706
1400
  const kebab = toKebabCase(name);
707
1401
  const pascal = toPascalCase(name);
708
- const camel = toCamelCase(name);
709
1402
  const plural = pluralize(kebab);
710
1403
  const pluralPascal = pluralizePascal(pascal);
711
1404
  const moduleDir = join(modulesDir, plural);
712
1405
  const files = [];
1406
+ let overwriteAll = force ?? false;
713
1407
  const write = /* @__PURE__ */ __name(async (relativePath, content) => {
714
1408
  const fullPath = join(moduleDir, relativePath);
1409
+ if (!overwriteAll && await fileExists(fullPath)) {
1410
+ const answer = await promptUser(` File already exists: ${relativePath}
1411
+ Overwrite? (y/n/a = yes/no/all) `);
1412
+ if (answer === "a") {
1413
+ overwriteAll = true;
1414
+ } else if (answer !== "y") {
1415
+ console.log(` Skipped: ${relativePath}`);
1416
+ return;
1417
+ }
1418
+ }
715
1419
  await writeFileSafe(fullPath, content);
716
1420
  files.push(fullPath);
717
1421
  }, "write");
1422
+ const ctx = {
1423
+ kebab,
1424
+ pascal,
1425
+ plural,
1426
+ pluralPascal,
1427
+ moduleDir,
1428
+ repo,
1429
+ noEntity: noEntity ?? false,
1430
+ noTests: noTests ?? false,
1431
+ write,
1432
+ files
1433
+ };
1434
+ switch (pattern) {
1435
+ case "minimal":
1436
+ await generateMinimalFiles(ctx);
1437
+ break;
1438
+ case "rest":
1439
+ await generateRestFiles(ctx);
1440
+ break;
1441
+ case "cqrs":
1442
+ await generateCqrsFiles(ctx);
1443
+ break;
1444
+ case "graphql":
1445
+ case "ddd":
1446
+ default:
1447
+ await generateDddFiles(ctx);
1448
+ break;
1449
+ }
1450
+ await autoRegisterModule(modulesDir, pascal, plural);
1451
+ return files;
1452
+ }
1453
+ __name(generateModule, "generateModule");
1454
+ async function generateMinimalFiles(ctx) {
1455
+ const { pascal, kebab, plural, write } = ctx;
1456
+ await write("index.ts", generateMinimalModuleIndex(pascal, kebab, plural));
1457
+ await write(`${kebab}.controller.ts`, `import { Controller, Get } from '@forinda/kickjs-core'
1458
+ import type { RequestContext } from '@forinda/kickjs-http'
1459
+
1460
+ @Controller()
1461
+ export class ${pascal}Controller {
1462
+ @Get('/')
1463
+ async list(ctx: RequestContext) {
1464
+ ctx.json({ message: '${pascal} list' })
1465
+ }
1466
+ }
1467
+ `);
1468
+ }
1469
+ __name(generateMinimalFiles, "generateMinimalFiles");
1470
+ async function generateRestFiles(ctx) {
1471
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
1472
+ await write("index.ts", generateRestModuleIndex(pascal, kebab, plural, repo));
1473
+ await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
1474
+ await write(`${kebab}.controller.ts`, generateRestController(pascal, kebab, plural, pluralPascal));
1475
+ await write(`${kebab}.service.ts`, generateRestService(pascal, kebab));
1476
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
1477
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
1478
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
1479
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
1480
+ const repoFileMap = {
1481
+ inmemory: `in-memory-${kebab}`,
1482
+ drizzle: `drizzle-${kebab}`,
1483
+ prisma: `prisma-${kebab}`
1484
+ };
1485
+ const repoGeneratorMap = {
1486
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
1487
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
1488
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
1489
+ };
1490
+ await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
1491
+ if (!noTests) {
1492
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
1493
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
1494
+ }
1495
+ }
1496
+ __name(generateRestFiles, "generateRestFiles");
1497
+ async function generateCqrsFiles(ctx) {
1498
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
1499
+ await write("index.ts", generateCqrsModuleIndex(pascal, kebab, plural, repo));
1500
+ await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
1501
+ await write(`${kebab}.controller.ts`, generateCqrsController(pascal, kebab, plural, pluralPascal));
1502
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
1503
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
1504
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
1505
+ const commands = generateCqrsCommands(pascal, kebab);
1506
+ for (const cmd of commands) {
1507
+ await write(`commands/${cmd.file}`, cmd.content);
1508
+ }
1509
+ const queries = generateCqrsQueries(pascal, kebab, plural, pluralPascal);
1510
+ for (const q of queries) {
1511
+ await write(`queries/${q.file}`, q.content);
1512
+ }
1513
+ const events = generateCqrsEvents(pascal, kebab);
1514
+ for (const e of events) {
1515
+ await write(`events/${e.file}`, e.content);
1516
+ }
1517
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
1518
+ const repoFileMap = {
1519
+ inmemory: `in-memory-${kebab}`,
1520
+ drizzle: `drizzle-${kebab}`,
1521
+ prisma: `prisma-${kebab}`
1522
+ };
1523
+ const repoGeneratorMap = {
1524
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
1525
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
1526
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
1527
+ };
1528
+ await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
1529
+ if (!noTests) {
1530
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
1531
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
1532
+ }
1533
+ }
1534
+ __name(generateCqrsFiles, "generateCqrsFiles");
1535
+ async function generateDddFiles(ctx) {
1536
+ const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, write } = ctx;
718
1537
  await write("index.ts", generateModuleIndex(pascal, kebab, plural, repo));
719
1538
  await write("constants.ts", generateConstants(pascal));
720
1539
  await write(`presentation/${kebab}.controller.ts`, generateController(pascal, kebab, plural, pluralPascal));
@@ -727,10 +1546,18 @@ async function generateModule(options) {
727
1546
  }
728
1547
  await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab));
729
1548
  await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService(pascal, kebab));
730
- if (repo === "inmemory") {
731
- await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, generateInMemoryRepository(pascal, kebab));
732
- }
733
- if (!noEntity && !minimal) {
1549
+ const repoFileMap = {
1550
+ inmemory: `in-memory-${kebab}`,
1551
+ drizzle: `drizzle-${kebab}`,
1552
+ prisma: `prisma-${kebab}`
1553
+ };
1554
+ const repoGeneratorMap = {
1555
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab), "inmemory"),
1556
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab), "drizzle"),
1557
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab), "prisma")
1558
+ };
1559
+ await write(`infrastructure/repositories/${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
1560
+ if (!noEntity) {
734
1561
  await write(`domain/entities/${kebab}.entity.ts`, generateEntity(pascal, kebab));
735
1562
  await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject(pascal, kebab));
736
1563
  }
@@ -738,10 +1565,8 @@ async function generateModule(options) {
738
1565
  await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
739
1566
  await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural));
740
1567
  }
741
- await autoRegisterModule(modulesDir, pascal, plural);
742
- return files;
743
1568
  }
744
- __name(generateModule, "generateModule");
1569
+ __name(generateDddFiles, "generateDddFiles");
745
1570
  async function autoRegisterModule(modulesDir, pascal, plural) {
746
1571
  const indexPath = join(modulesDir, "index.ts");
747
1572
  const exists = await fileExists(indexPath);
@@ -877,13 +1702,64 @@ export class ${pascal}Adapter implements AppAdapter {
877
1702
  __name(generateAdapter, "generateAdapter");
878
1703
 
879
1704
  // src/generators/middleware.ts
880
- import { join as join3 } from "path";
1705
+ import { join as join4 } from "path";
1706
+
1707
+ // src/utils/resolve-out-dir.ts
1708
+ import { resolve, join as join3 } from "path";
1709
+ var DDD_FOLDER_MAP = {
1710
+ controller: "presentation",
1711
+ service: "domain/services",
1712
+ dto: "application/dtos",
1713
+ guard: "presentation/guards",
1714
+ middleware: "middleware"
1715
+ };
1716
+ var FLAT_FOLDER_MAP = {
1717
+ controller: "",
1718
+ service: "",
1719
+ dto: "dtos",
1720
+ guard: "guards",
1721
+ middleware: "middleware"
1722
+ };
1723
+ var CQRS_FOLDER_MAP = {
1724
+ controller: "",
1725
+ service: "",
1726
+ dto: "dtos",
1727
+ guard: "guards",
1728
+ middleware: "middleware",
1729
+ command: "commands",
1730
+ query: "queries",
1731
+ event: "events"
1732
+ };
1733
+ function resolveOutDir(options) {
1734
+ const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
1735
+ if (outDir) return resolve(outDir);
1736
+ if (moduleName) {
1737
+ const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
1738
+ const kebab = toKebabCase(moduleName);
1739
+ const plural = pluralize(kebab);
1740
+ const subfolder = folderMap[type] ?? "";
1741
+ const base = join3(modulesDir, plural);
1742
+ return resolve(subfolder ? join3(base, subfolder) : base);
1743
+ }
1744
+ return resolve(defaultDir);
1745
+ }
1746
+ __name(resolveOutDir, "resolveOutDir");
1747
+
1748
+ // src/generators/middleware.ts
881
1749
  async function generateMiddleware(options) {
882
- const { name, outDir } = options;
1750
+ const { name, moduleName, modulesDir, pattern } = options;
1751
+ const outDir = resolveOutDir({
1752
+ type: "middleware",
1753
+ outDir: options.outDir,
1754
+ moduleName,
1755
+ modulesDir,
1756
+ defaultDir: "src/middleware",
1757
+ pattern
1758
+ });
883
1759
  const kebab = toKebabCase(name);
884
1760
  const camel = toCamelCase(name);
885
1761
  const files = [];
886
- const filePath = join3(outDir, `${kebab}.middleware.ts`);
1762
+ const filePath = join4(outDir, `${kebab}.middleware.ts`);
887
1763
  await writeFileSafe(filePath, `import type { Request, Response, NextFunction } from 'express'
888
1764
 
889
1765
  export interface ${toPascalCase(name)}Options {
@@ -915,14 +1791,22 @@ export function ${camel}(options: ${toPascalCase(name)}Options = {}) {
915
1791
  __name(generateMiddleware, "generateMiddleware");
916
1792
 
917
1793
  // src/generators/guard.ts
918
- import { join as join4 } from "path";
1794
+ import { join as join5 } from "path";
919
1795
  async function generateGuard(options) {
920
- const { name, outDir } = options;
1796
+ const { name, moduleName, modulesDir, pattern } = options;
1797
+ const outDir = resolveOutDir({
1798
+ type: "guard",
1799
+ outDir: options.outDir,
1800
+ moduleName,
1801
+ modulesDir,
1802
+ defaultDir: "src/guards",
1803
+ pattern
1804
+ });
921
1805
  const kebab = toKebabCase(name);
922
1806
  const camel = toCamelCase(name);
923
1807
  const pascal = toPascalCase(name);
924
1808
  const files = [];
925
- const filePath = join4(outDir, `${kebab}.guard.ts`);
1809
+ const filePath = join5(outDir, `${kebab}.guard.ts`);
926
1810
  await writeFileSafe(filePath, `import { Container, HttpException } from '@forinda/kickjs-core'
927
1811
  import type { RequestContext } from '@forinda/kickjs-http'
928
1812
 
@@ -966,13 +1850,21 @@ export async function ${camel}Guard(ctx: RequestContext, next: () => void): Prom
966
1850
  __name(generateGuard, "generateGuard");
967
1851
 
968
1852
  // src/generators/service.ts
969
- import { join as join5 } from "path";
1853
+ import { join as join6 } from "path";
970
1854
  async function generateService(options) {
971
- const { name, outDir } = options;
1855
+ const { name, moduleName, modulesDir, pattern } = options;
1856
+ const outDir = resolveOutDir({
1857
+ type: "service",
1858
+ outDir: options.outDir,
1859
+ moduleName,
1860
+ modulesDir,
1861
+ defaultDir: "src/services",
1862
+ pattern
1863
+ });
972
1864
  const kebab = toKebabCase(name);
973
1865
  const pascal = toPascalCase(name);
974
1866
  const files = [];
975
- const filePath = join5(outDir, `${kebab}.service.ts`);
1867
+ const filePath = join6(outDir, `${kebab}.service.ts`);
976
1868
  await writeFileSafe(filePath, `import { Service } from '@forinda/kickjs-core'
977
1869
 
978
1870
  @Service()
@@ -989,13 +1881,21 @@ export class ${pascal}Service {
989
1881
  __name(generateService, "generateService");
990
1882
 
991
1883
  // src/generators/controller.ts
992
- import { join as join6 } from "path";
1884
+ import { join as join7 } from "path";
993
1885
  async function generateController2(options) {
994
- const { name, outDir } = options;
1886
+ const { name, moduleName, modulesDir, pattern } = options;
1887
+ const outDir = resolveOutDir({
1888
+ type: "controller",
1889
+ outDir: options.outDir,
1890
+ moduleName,
1891
+ modulesDir,
1892
+ defaultDir: "src/controllers",
1893
+ pattern
1894
+ });
995
1895
  const kebab = toKebabCase(name);
996
1896
  const pascal = toPascalCase(name);
997
1897
  const files = [];
998
- const filePath = join6(outDir, `${kebab}.controller.ts`);
1898
+ const filePath = join7(outDir, `${kebab}.controller.ts`);
999
1899
  await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs-core'
1000
1900
  import type { RequestContext } from '@forinda/kickjs-http'
1001
1901
 
@@ -1020,14 +1920,22 @@ export class ${pascal}Controller {
1020
1920
  __name(generateController2, "generateController");
1021
1921
 
1022
1922
  // src/generators/dto.ts
1023
- import { join as join7 } from "path";
1923
+ import { join as join8 } from "path";
1024
1924
  async function generateDto(options) {
1025
- const { name, outDir } = options;
1925
+ const { name, moduleName, modulesDir, pattern } = options;
1926
+ const outDir = resolveOutDir({
1927
+ type: "dto",
1928
+ outDir: options.outDir,
1929
+ moduleName,
1930
+ modulesDir,
1931
+ defaultDir: "src/dtos",
1932
+ pattern
1933
+ });
1026
1934
  const kebab = toKebabCase(name);
1027
1935
  const pascal = toPascalCase(name);
1028
1936
  const camel = toCamelCase(name);
1029
1937
  const files = [];
1030
- const filePath = join7(outDir, `${kebab}.dto.ts`);
1938
+ const filePath = join8(outDir, `${kebab}.dto.ts`);
1031
1939
  await writeFileSafe(filePath, `import { z } from 'zod'
1032
1940
 
1033
1941
  export const ${camel}Schema = z.object({
@@ -1043,12 +1951,12 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
1043
1951
  __name(generateDto, "generateDto");
1044
1952
 
1045
1953
  // src/generators/project.ts
1046
- import { join as join8, dirname as dirname2 } from "path";
1954
+ import { join as join9, dirname as dirname2 } from "path";
1047
1955
  import { execSync } from "child_process";
1048
1956
  import { readFileSync } from "fs";
1049
1957
  import { fileURLToPath } from "url";
1050
1958
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1051
- var cliPkg = JSON.parse(readFileSync(join8(__dirname, "..", "package.json"), "utf-8"));
1959
+ var cliPkg = JSON.parse(readFileSync(join9(__dirname, "..", "package.json"), "utf-8"));
1052
1960
  var KICKJS_VERSION = `^${cliPkg.version}`;
1053
1961
  async function initProject(options) {
1054
1962
  const { name, directory, packageManager = "pnpm", template = "rest" } = options;
@@ -1068,19 +1976,21 @@ async function initProject(options) {
1068
1976
  };
1069
1977
  if (template !== "minimal") {
1070
1978
  baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
1979
+ baseDeps["@forinda/kickjs-devtools"] = KICKJS_VERSION;
1071
1980
  }
1072
1981
  if (template === "graphql") {
1073
1982
  baseDeps["@forinda/kickjs-graphql"] = KICKJS_VERSION;
1074
1983
  baseDeps["graphql"] = "^16.11.0";
1075
1984
  }
1076
- if (template === "microservice") {
1985
+ if (template === "cqrs") {
1077
1986
  baseDeps["@forinda/kickjs-queue"] = KICKJS_VERSION;
1987
+ baseDeps["@forinda/kickjs-ws"] = KICKJS_VERSION;
1078
1988
  baseDeps["@forinda/kickjs-otel"] = KICKJS_VERSION;
1079
1989
  }
1080
1990
  if (template === "ddd") {
1081
1991
  baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
1082
1992
  }
1083
- await writeFileSafe(join8(dir, "package.json"), JSON.stringify({
1993
+ await writeFileSafe(join9(dir, "package.json"), JSON.stringify({
1084
1994
  name,
1085
1995
  version: cliPkg.version,
1086
1996
  type: "module",
@@ -1109,7 +2019,7 @@ async function initProject(options) {
1109
2019
  prettier: "^3.8.1"
1110
2020
  }
1111
2021
  }, null, 2));
1112
- await writeFileSafe(join8(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
2022
+ await writeFileSafe(join9(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
1113
2023
  import { resolve } from 'path'
1114
2024
  import swc from 'unplugin-swc'
1115
2025
 
@@ -1136,7 +2046,7 @@ export default defineConfig({
1136
2046
  },
1137
2047
  })
1138
2048
  `);
1139
- await writeFileSafe(join8(dir, "tsconfig.json"), JSON.stringify({
2049
+ await writeFileSafe(join9(dir, "tsconfig.json"), JSON.stringify({
1140
2050
  compilerOptions: {
1141
2051
  target: "ES2022",
1142
2052
  module: "ESNext",
@@ -1166,35 +2076,68 @@ export default defineConfig({
1166
2076
  "src"
1167
2077
  ]
1168
2078
  }, null, 2));
1169
- await writeFileSafe(join8(dir, ".prettierrc"), JSON.stringify({
2079
+ await writeFileSafe(join9(dir, ".prettierrc"), JSON.stringify({
1170
2080
  semi: false,
1171
2081
  singleQuote: true,
1172
2082
  trailingComma: "all",
1173
2083
  printWidth: 100,
1174
2084
  tabWidth: 2
1175
2085
  }, null, 2));
1176
- await writeFileSafe(join8(dir, ".gitignore"), `node_modules/
2086
+ await writeFileSafe(join9(dir, ".editorconfig"), `# https://editorconfig.org
2087
+ root = true
2088
+
2089
+ [*]
2090
+ indent_style = space
2091
+ indent_size = 2
2092
+ end_of_line = lf
2093
+ charset = utf-8
2094
+ trim_trailing_whitespace = true
2095
+ insert_final_newline = true
2096
+
2097
+ [*.md]
2098
+ trim_trailing_whitespace = false
2099
+ `);
2100
+ await writeFileSafe(join9(dir, ".gitignore"), `node_modules/
1177
2101
  dist/
1178
2102
  .env
1179
2103
  coverage/
1180
2104
  .DS_Store
1181
2105
  *.tsbuildinfo
1182
2106
  `);
1183
- await writeFileSafe(join8(dir, ".env"), `PORT=3000
2107
+ await writeFileSafe(join9(dir, ".gitattributes"), `# Auto-detect text files and normalise line endings to LF
2108
+ * text=auto eol=lf
2109
+
2110
+ # Explicitly mark generated / binary files
2111
+ *.png binary
2112
+ *.jpg binary
2113
+ *.jpeg binary
2114
+ *.gif binary
2115
+ *.ico binary
2116
+ *.woff binary
2117
+ *.woff2 binary
2118
+ *.ttf binary
2119
+ *.eot binary
2120
+
2121
+ # Lock files \u2014 treat as generated
2122
+ pnpm-lock.yaml -diff linguist-generated
2123
+ yarn.lock -diff linguist-generated
2124
+ package-lock.json -diff linguist-generated
2125
+ `);
2126
+ await writeFileSafe(join9(dir, ".env"), `PORT=3000
1184
2127
  NODE_ENV=development
1185
2128
  `);
1186
- await writeFileSafe(join8(dir, ".env.example"), `PORT=3000
2129
+ await writeFileSafe(join9(dir, ".env.example"), `PORT=3000
1187
2130
  NODE_ENV=development
1188
2131
  `);
1189
- await writeFileSafe(join8(dir, "src/index.ts"), getEntryFile(name, template));
1190
- await writeFileSafe(join8(dir, "src/modules/index.ts"), `import type { AppModuleClass } from '@forinda/kickjs-core'
2132
+ await writeFileSafe(join9(dir, "src/index.ts"), getEntryFile(name, template));
2133
+ await writeFileSafe(join9(dir, "src/modules/index.ts"), `import type { AppModuleClass } from '@forinda/kickjs-core'
1191
2134
 
1192
2135
  export const modules: AppModuleClass[] = []
1193
2136
  `);
1194
2137
  if (template === "graphql") {
1195
- await writeFileSafe(join8(dir, "src/resolvers/.gitkeep"), "");
2138
+ await writeFileSafe(join9(dir, "src/resolvers/.gitkeep"), "");
1196
2139
  }
1197
- await writeFileSafe(join8(dir, "kick.config.ts"), `import { defineConfig } from '@forinda/kickjs-cli'
2140
+ await writeFileSafe(join9(dir, "kick.config.ts"), `import { defineConfig } from '@forinda/kickjs-cli'
1198
2141
 
1199
2142
  export default defineConfig({
1200
2143
  pattern: '${template}',
@@ -1226,7 +2169,7 @@ export default defineConfig({
1226
2169
  ],
1227
2170
  })
1228
2171
  `);
1229
- await writeFileSafe(join8(dir, "vitest.config.ts"), `import { defineConfig } from 'vitest/config'
2172
+ await writeFileSafe(join9(dir, "vitest.config.ts"), `import { defineConfig } from 'vitest/config'
1230
2173
  import swc from 'unplugin-swc'
1231
2174
 
1232
2175
  export default defineConfig({
@@ -1282,19 +2225,36 @@ export default defineConfig({
1282
2225
  rest: "kick g module user",
1283
2226
  graphql: "kick g resolver user",
1284
2227
  ddd: "kick g module user --repo drizzle",
1285
- microservice: "kick g module user && kick g job email",
2228
+ cqrs: "kick g module user --pattern cqrs",
1286
2229
  minimal: "# add your routes to src/index.ts"
1287
2230
  };
1288
2231
  console.log(` ${genHint[template] ?? genHint.rest}`);
1289
2232
  console.log(" kick dev");
1290
2233
  console.log();
1291
2234
  console.log(" Commands:");
1292
- console.log(" kick dev Start dev server with Vite HMR");
1293
- console.log(" kick build Production build via Vite");
1294
- console.log(" kick start Run production build");
1295
- console.log(` kick g module X Generate a DDD module`);
1296
- if (template === "graphql") console.log(" kick g resolver X Generate a GraphQL resolver");
1297
- if (template === "microservice") console.log(" kick g job X Generate a queue job processor");
2235
+ console.log(" kick dev Start dev server with Vite HMR");
2236
+ console.log(" kick build Production build via Vite");
2237
+ console.log(" kick start Run production build");
2238
+ console.log();
2239
+ console.log(" Generators:");
2240
+ console.log(" kick g module <name> Full DDD module (controller, DTOs, use-cases, repo)");
2241
+ console.log(" kick g scaffold <n> <f..> CRUD module from field definitions");
2242
+ console.log(" kick g controller <name> Standalone controller");
2243
+ console.log(" kick g service <name> @Service() class");
2244
+ console.log(" kick g middleware <name> Express middleware");
2245
+ console.log(" kick g guard <name> Route guard (auth, roles, etc.)");
2246
+ console.log(" kick g adapter <name> AppAdapter with lifecycle hooks");
2247
+ console.log(" kick g dto <name> Zod DTO schema");
2248
+ if (template === "graphql") console.log(" kick g resolver <name> GraphQL resolver");
2249
+ if (template === "cqrs") console.log(" kick g job <name> Queue job processor");
2250
+ console.log(" kick g config Generate kick.config.ts");
2251
+ console.log();
2252
+ console.log(" Add packages:");
2253
+ console.log(" kick add <pkg> Install a KickJS package + peers");
2254
+ console.log(" kick add --list Show all available packages");
2255
+ console.log();
2256
+ console.log(" Available: auth, swagger, graphql, drizzle, prisma, ws,");
2257
+ console.log(" cron, queue, mailer, otel, multi-tenant, notifications, testing");
1298
2258
  console.log();
1299
2259
  }
1300
2260
  __name(initProject, "initProject");
@@ -1322,12 +2282,13 @@ bootstrap({
1322
2282
  ],
1323
2283
  })
1324
2284
  `;
1325
- case "microservice":
2285
+ case "cqrs":
1326
2286
  return `import 'reflect-metadata'
1327
2287
  import { bootstrap } from '@forinda/kickjs-http'
1328
2288
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1329
2289
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
1330
2290
  import { OtelAdapter } from '@forinda/kickjs-otel'
2291
+ // import { WsAdapter } from '@forinda/kickjs-ws'
1331
2292
  // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
1332
2293
  import { modules } from './modules'
1333
2294
 
@@ -1339,6 +2300,8 @@ bootstrap({
1339
2300
  new SwaggerAdapter({
1340
2301
  info: { title: '${name}', version: '${cliPkg.version}' },
1341
2302
  }),
2303
+ // Uncomment for WebSocket support:
2304
+ // new WsAdapter(),
1342
2305
  // Uncomment when Redis is available:
1343
2306
  // new QueueAdapter({
1344
2307
  // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
@@ -1378,7 +2341,7 @@ __name(getEntryFile, "getEntryFile");
1378
2341
 
1379
2342
  // src/config.ts
1380
2343
  import { readFile as readFile3, access as access2 } from "fs/promises";
1381
- import { join as join9 } from "path";
2344
+ import { join as join10 } from "path";
1382
2345
  function defineConfig(config) {
1383
2346
  return config;
1384
2347
  }
@@ -1391,7 +2354,7 @@ var CONFIG_FILES = [
1391
2354
  ];
1392
2355
  async function loadKickConfig(cwd) {
1393
2356
  for (const filename of CONFIG_FILES) {
1394
- const filepath = join9(cwd, filename);
2357
+ const filepath = join10(cwd, filename);
1395
2358
  try {
1396
2359
  await access2(filepath);
1397
2360
  } catch {