@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/cli.js +1303 -172
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +88 -67
- package/dist/index.js +1031 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
350
|
-
* The interface
|
|
351
|
-
*
|
|
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
|
|
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 '
|
|
357
|
-
import type { Create${pascal}DTO } from '
|
|
358
|
-
import type { Update${pascal}DTO } from '
|
|
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
|
-
*
|
|
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 '
|
|
388
|
-
import type { ${pascal}ResponseDTO } from '
|
|
389
|
-
import type { Create${pascal}DTO } from '
|
|
390
|
-
import type { Update${pascal}DTO } from '
|
|
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 '
|
|
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",
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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(
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
1794
|
+
import { join as join5 } from "path";
|
|
919
1795
|
async function generateGuard(options) {
|
|
920
|
-
const { name,
|
|
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 =
|
|
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
|
|
1853
|
+
import { join as join6 } from "path";
|
|
970
1854
|
async function generateService(options) {
|
|
971
|
-
const { name,
|
|
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 =
|
|
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
|
|
1884
|
+
import { join as join7 } from "path";
|
|
993
1885
|
async function generateController2(options) {
|
|
994
|
-
const { name,
|
|
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 =
|
|
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
|
|
1923
|
+
import { join as join8 } from "path";
|
|
1024
1924
|
async function generateDto(options) {
|
|
1025
|
-
const { name,
|
|
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 =
|
|
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
|
|
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(
|
|
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 === "
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2129
|
+
await writeFileSafe(join9(dir, ".env.example"), `PORT=3000
|
|
1187
2130
|
NODE_ENV=development
|
|
1188
2131
|
`);
|
|
1189
|
-
await writeFileSafe(
|
|
1190
|
-
await writeFileSafe(
|
|
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(
|
|
2138
|
+
await writeFileSafe(join9(dir, "src/resolvers/.gitkeep"), "");
|
|
1196
2139
|
}
|
|
1197
|
-
await writeFileSafe(
|
|
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(
|
|
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
|
-
|
|
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
|
|
1293
|
-
console.log(" kick build
|
|
1294
|
-
console.log(" kick start
|
|
1295
|
-
console.log(
|
|
1296
|
-
|
|
1297
|
-
|
|
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 "
|
|
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
|
|
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 =
|
|
2357
|
+
const filepath = join10(cwd, filename);
|
|
1395
2358
|
try {
|
|
1396
2359
|
await access2(filepath);
|
|
1397
2360
|
} catch {
|