@forinda/kickjs-cli 0.5.0 → 0.6.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 +300 -94
- package/dist/cli.js.map +1 -1
- package/dist/index.js +300 -94
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -416,21 +416,12 @@ __name(pluralizePascal, "pluralizePascal");
|
|
|
416
416
|
|
|
417
417
|
// src/generators/module.ts
|
|
418
418
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
const pluralPascal = pluralizePascal(pascal);
|
|
426
|
-
const moduleDir = join2(modulesDir, plural);
|
|
427
|
-
const files = [];
|
|
428
|
-
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
429
|
-
const fullPath = join2(moduleDir, relativePath);
|
|
430
|
-
await writeFileSafe(fullPath, content);
|
|
431
|
-
files.push(fullPath);
|
|
432
|
-
}, "write");
|
|
433
|
-
await write("index.ts", `/**
|
|
419
|
+
|
|
420
|
+
// src/generators/templates/module-index.ts
|
|
421
|
+
function generateModuleIndex(pascal, kebab, plural, repo) {
|
|
422
|
+
const repoClass = repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`;
|
|
423
|
+
const repoFile = repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`;
|
|
424
|
+
return `/**
|
|
434
425
|
* ${pascal} Module
|
|
435
426
|
*
|
|
436
427
|
* Self-contained feature module following Domain-Driven Design (DDD).
|
|
@@ -445,7 +436,7 @@ async function generateModule(options) {
|
|
|
445
436
|
import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
|
|
446
437
|
import { buildRoutes } from '@forinda/kickjs-http'
|
|
447
438
|
import { ${pascal.toUpperCase()}_REPOSITORY } from './domain/repositories/${kebab}.repository'
|
|
448
|
-
import { ${
|
|
439
|
+
import { ${repoClass} } from './infrastructure/repositories/${repoFile}.repository'
|
|
449
440
|
import { ${pascal}Controller } from './presentation/${kebab}.controller'
|
|
450
441
|
|
|
451
442
|
// Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
|
|
@@ -462,7 +453,7 @@ export class ${pascal}Module implements AppModule {
|
|
|
462
453
|
*/
|
|
463
454
|
register(container: Container): void {
|
|
464
455
|
container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
|
|
465
|
-
container.resolve(${
|
|
456
|
+
container.resolve(${repoClass}),
|
|
466
457
|
)
|
|
467
458
|
}
|
|
468
459
|
|
|
@@ -479,24 +470,15 @@ export class ${pascal}Module implements AppModule {
|
|
|
479
470
|
}
|
|
480
471
|
}
|
|
481
472
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
* @Get/@Post/@Put/@Delete(path?, validation?) \u2014 defines routes with optional Zod validation
|
|
492
|
-
* @Autowired() \u2014 injects dependencies lazily from the DI container
|
|
493
|
-
* @Middleware(...handlers) \u2014 attach middleware at class or method level
|
|
494
|
-
*
|
|
495
|
-
* Add Swagger decorators (@ApiTags, @ApiOperation, @ApiResponse) from @forinda/kickjs-swagger
|
|
496
|
-
* for automatic OpenAPI documentation.
|
|
497
|
-
*/
|
|
498
|
-
import { Controller, Get, Post, Put, Delete, Autowired } from '@forinda/kickjs-core'
|
|
499
|
-
import { RequestContext } from '@forinda/kickjs-http'
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
__name(generateModuleIndex, "generateModuleIndex");
|
|
476
|
+
|
|
477
|
+
// src/generators/templates/controller.ts
|
|
478
|
+
function generateController(pascal, kebab, plural, pluralPascal) {
|
|
479
|
+
return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
|
|
480
|
+
import type { RequestContext } from '@forinda/kickjs-http'
|
|
481
|
+
import { ApiTags } from '@forinda/kickjs-swagger'
|
|
500
482
|
import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
|
|
501
483
|
import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
|
|
502
484
|
import { List${pluralPascal}UseCase } from '../application/use-cases/list-${plural}.use-case'
|
|
@@ -504,6 +486,7 @@ import { Update${pascal}UseCase } from '../application/use-cases/update-${kebab}
|
|
|
504
486
|
import { Delete${pascal}UseCase } from '../application/use-cases/delete-${kebab}.use-case'
|
|
505
487
|
import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
|
|
506
488
|
import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
|
|
489
|
+
import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../constants'
|
|
507
490
|
|
|
508
491
|
@Controller()
|
|
509
492
|
export class ${pascal}Controller {
|
|
@@ -513,39 +496,65 @@ export class ${pascal}Controller {
|
|
|
513
496
|
@Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
|
|
514
497
|
@Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
|
|
515
498
|
|
|
516
|
-
@Post('/', { body: create${pascal}Schema })
|
|
517
|
-
async create(ctx: RequestContext) {
|
|
518
|
-
const result = await this.create${pascal}UseCase.execute(ctx.body)
|
|
519
|
-
ctx.created(result)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
499
|
@Get('/')
|
|
500
|
+
@ApiTags('${pascal}')
|
|
501
|
+
@ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
|
|
523
502
|
async list(ctx: RequestContext) {
|
|
524
|
-
|
|
525
|
-
|
|
503
|
+
return ctx.paginate(
|
|
504
|
+
(parsed) => this.list${pluralPascal}UseCase.execute(parsed),
|
|
505
|
+
${pascal.toUpperCase()}_QUERY_CONFIG,
|
|
506
|
+
)
|
|
526
507
|
}
|
|
527
508
|
|
|
528
509
|
@Get('/:id')
|
|
510
|
+
@ApiTags('${pascal}')
|
|
529
511
|
async getById(ctx: RequestContext) {
|
|
530
512
|
const result = await this.get${pascal}UseCase.execute(ctx.params.id)
|
|
531
513
|
if (!result) return ctx.notFound('${pascal} not found')
|
|
532
514
|
ctx.json(result)
|
|
533
515
|
}
|
|
534
516
|
|
|
535
|
-
@
|
|
517
|
+
@Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
|
|
518
|
+
@ApiTags('${pascal}')
|
|
519
|
+
async create(ctx: RequestContext) {
|
|
520
|
+
const result = await this.create${pascal}UseCase.execute(ctx.body)
|
|
521
|
+
ctx.created(result)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
|
|
525
|
+
@ApiTags('${pascal}')
|
|
536
526
|
async update(ctx: RequestContext) {
|
|
537
527
|
const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
|
|
538
528
|
ctx.json(result)
|
|
539
529
|
}
|
|
540
530
|
|
|
541
531
|
@Delete('/:id')
|
|
532
|
+
@ApiTags('${pascal}')
|
|
542
533
|
async remove(ctx: RequestContext) {
|
|
543
534
|
await this.delete${pascal}UseCase.execute(ctx.params.id)
|
|
544
535
|
ctx.noContent()
|
|
545
536
|
}
|
|
546
537
|
}
|
|
547
|
-
|
|
548
|
-
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
__name(generateController, "generateController");
|
|
541
|
+
|
|
542
|
+
// src/generators/templates/constants.ts
|
|
543
|
+
function generateConstants(pascal) {
|
|
544
|
+
return `import type { QueryParamsConfig } from '@forinda/kickjs-core'
|
|
545
|
+
|
|
546
|
+
export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryParamsConfig = {
|
|
547
|
+
filterable: ['name'],
|
|
548
|
+
sortable: ['name', 'createdAt'],
|
|
549
|
+
searchable: ['name'],
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
__name(generateConstants, "generateConstants");
|
|
554
|
+
|
|
555
|
+
// src/generators/templates/dtos.ts
|
|
556
|
+
function generateCreateDTO(pascal, kebab) {
|
|
557
|
+
return `import { z } from 'zod'
|
|
549
558
|
|
|
550
559
|
/**
|
|
551
560
|
* Create ${pascal} DTO \u2014 Zod schema for validating POST request bodies.
|
|
@@ -561,23 +570,34 @@ export const create${pascal}Schema = z.object({
|
|
|
561
570
|
})
|
|
562
571
|
|
|
563
572
|
export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
|
|
564
|
-
|
|
565
|
-
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
__name(generateCreateDTO, "generateCreateDTO");
|
|
576
|
+
function generateUpdateDTO(pascal, kebab) {
|
|
577
|
+
return `import { z } from 'zod'
|
|
566
578
|
|
|
567
579
|
export const update${pascal}Schema = z.object({
|
|
568
580
|
name: z.string().min(1).max(200).optional(),
|
|
569
581
|
})
|
|
570
582
|
|
|
571
583
|
export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
|
|
572
|
-
|
|
573
|
-
|
|
584
|
+
`;
|
|
585
|
+
}
|
|
586
|
+
__name(generateUpdateDTO, "generateUpdateDTO");
|
|
587
|
+
function generateResponseDTO(pascal, kebab) {
|
|
588
|
+
return `export interface ${pascal}ResponseDTO {
|
|
574
589
|
id: string
|
|
575
590
|
name: string
|
|
576
591
|
createdAt: string
|
|
577
592
|
updatedAt: string
|
|
578
593
|
}
|
|
579
|
-
|
|
580
|
-
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
__name(generateResponseDTO, "generateResponseDTO");
|
|
597
|
+
|
|
598
|
+
// src/generators/templates/use-cases.ts
|
|
599
|
+
function generateUseCases(pascal, kebab, plural, pluralPascal) {
|
|
600
|
+
return [
|
|
581
601
|
{
|
|
582
602
|
file: `create-${kebab}.use-case.ts`,
|
|
583
603
|
content: `/**
|
|
@@ -626,7 +646,7 @@ export class Get${pascal}UseCase {
|
|
|
626
646
|
file: `list-${plural}.use-case.ts`,
|
|
627
647
|
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
628
648
|
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
|
|
629
|
-
import type {
|
|
649
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
630
650
|
|
|
631
651
|
@Service()
|
|
632
652
|
export class List${pluralPascal}UseCase {
|
|
@@ -634,8 +654,8 @@ export class List${pluralPascal}UseCase {
|
|
|
634
654
|
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
635
655
|
) {}
|
|
636
656
|
|
|
637
|
-
async execute(
|
|
638
|
-
return this.repo.
|
|
657
|
+
async execute(parsed: ParsedQuery) {
|
|
658
|
+
return this.repo.findPaginated(parsed)
|
|
639
659
|
}
|
|
640
660
|
}
|
|
641
661
|
`
|
|
@@ -677,10 +697,12 @@ export class Delete${pascal}UseCase {
|
|
|
677
697
|
`
|
|
678
698
|
}
|
|
679
699
|
];
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
700
|
+
}
|
|
701
|
+
__name(generateUseCases, "generateUseCases");
|
|
702
|
+
|
|
703
|
+
// src/generators/templates/repository.ts
|
|
704
|
+
function generateRepositoryInterface(pascal, kebab) {
|
|
705
|
+
return `/**
|
|
684
706
|
* ${pascal} Repository Interface
|
|
685
707
|
*
|
|
686
708
|
* Domain layer \u2014 defines the contract for data access.
|
|
@@ -693,43 +715,23 @@ export class Delete${pascal}UseCase {
|
|
|
693
715
|
import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
|
|
694
716
|
import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
|
|
695
717
|
import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
|
|
718
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
696
719
|
|
|
697
720
|
export interface I${pascal}Repository {
|
|
698
721
|
findById(id: string): Promise<${pascal}ResponseDTO | null>
|
|
699
722
|
findAll(): Promise<${pascal}ResponseDTO[]>
|
|
723
|
+
findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }>
|
|
700
724
|
create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO>
|
|
701
725
|
update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO>
|
|
702
726
|
delete(id: string): Promise<void>
|
|
703
727
|
}
|
|
704
728
|
|
|
705
729
|
export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
|
|
706
|
-
|
|
707
|
-
await write(`domain/services/${kebab}-domain.service.ts`, `/**
|
|
708
|
-
* ${pascal} Domain Service
|
|
709
|
-
*
|
|
710
|
-
* Domain layer \u2014 contains business rules that don't belong to a single entity.
|
|
711
|
-
* Use this for cross-entity logic, validation rules, and domain invariants.
|
|
712
|
-
* Keep it free of HTTP/framework concerns.
|
|
713
|
-
*/
|
|
714
|
-
import { Service, Inject, HttpException } from '@forinda/kickjs-core'
|
|
715
|
-
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
|
|
716
|
-
|
|
717
|
-
@Service()
|
|
718
|
-
export class ${pascal}DomainService {
|
|
719
|
-
constructor(
|
|
720
|
-
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
721
|
-
) {}
|
|
722
|
-
|
|
723
|
-
async ensureExists(id: string): Promise<void> {
|
|
724
|
-
const entity = await this.repo.findById(id)
|
|
725
|
-
if (!entity) {
|
|
726
|
-
throw HttpException.notFound('${pascal} not found')
|
|
727
|
-
}
|
|
728
|
-
}
|
|
730
|
+
`;
|
|
729
731
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
732
|
+
__name(generateRepositoryInterface, "generateRepositoryInterface");
|
|
733
|
+
function generateInMemoryRepository(pascal, kebab) {
|
|
734
|
+
return `/**
|
|
733
735
|
* In-Memory ${pascal} Repository
|
|
734
736
|
*
|
|
735
737
|
* Infrastructure layer \u2014 implements the repository interface using a Map.
|
|
@@ -740,6 +742,7 @@ export class ${pascal}DomainService {
|
|
|
740
742
|
*/
|
|
741
743
|
import { randomUUID } from 'node:crypto'
|
|
742
744
|
import { Repository, HttpException } from '@forinda/kickjs-core'
|
|
745
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
743
746
|
import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
|
|
744
747
|
import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
|
|
745
748
|
import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
|
|
@@ -757,6 +760,12 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
|
757
760
|
return Array.from(this.store.values())
|
|
758
761
|
}
|
|
759
762
|
|
|
763
|
+
async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
|
|
764
|
+
const all = Array.from(this.store.values())
|
|
765
|
+
const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
|
|
766
|
+
return { data, total: all.length }
|
|
767
|
+
}
|
|
768
|
+
|
|
760
769
|
async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
761
770
|
const now = new Date().toISOString()
|
|
762
771
|
const entity: ${pascal}ResponseDTO = {
|
|
@@ -782,10 +791,40 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
|
782
791
|
this.store.delete(id)
|
|
783
792
|
}
|
|
784
793
|
}
|
|
785
|
-
|
|
794
|
+
`;
|
|
795
|
+
}
|
|
796
|
+
__name(generateInMemoryRepository, "generateInMemoryRepository");
|
|
797
|
+
|
|
798
|
+
// src/generators/templates/domain.ts
|
|
799
|
+
function generateDomainService(pascal, kebab) {
|
|
800
|
+
return `/**
|
|
801
|
+
* ${pascal} Domain Service
|
|
802
|
+
*
|
|
803
|
+
* Domain layer \u2014 contains business rules that don't belong to a single entity.
|
|
804
|
+
* Use this for cross-entity logic, validation rules, and domain invariants.
|
|
805
|
+
* Keep it free of HTTP/framework concerns.
|
|
806
|
+
*/
|
|
807
|
+
import { Service, Inject, HttpException } from '@forinda/kickjs-core'
|
|
808
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
|
|
809
|
+
|
|
810
|
+
@Service()
|
|
811
|
+
export class ${pascal}DomainService {
|
|
812
|
+
constructor(
|
|
813
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
814
|
+
) {}
|
|
815
|
+
|
|
816
|
+
async ensureExists(id: string): Promise<void> {
|
|
817
|
+
const entity = await this.repo.findById(id)
|
|
818
|
+
if (!entity) {
|
|
819
|
+
throw HttpException.notFound('${pascal} not found')
|
|
820
|
+
}
|
|
786
821
|
}
|
|
787
|
-
|
|
788
|
-
|
|
822
|
+
}
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
__name(generateDomainService, "generateDomainService");
|
|
826
|
+
function generateEntity(pascal, kebab) {
|
|
827
|
+
return `/**
|
|
789
828
|
* ${pascal} Entity
|
|
790
829
|
*
|
|
791
830
|
* Domain layer \u2014 the core business object.
|
|
@@ -854,8 +893,11 @@ export class ${pascal} {
|
|
|
854
893
|
}
|
|
855
894
|
}
|
|
856
895
|
}
|
|
857
|
-
|
|
858
|
-
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
__name(generateEntity, "generateEntity");
|
|
899
|
+
function generateValueObject(pascal, kebab) {
|
|
900
|
+
return `/**
|
|
859
901
|
* ${pascal} ID Value Object
|
|
860
902
|
*
|
|
861
903
|
* Domain layer \u2014 wraps a primitive ID with type safety and validation.
|
|
@@ -889,7 +931,171 @@ export class ${pascal}Id {
|
|
|
889
931
|
return this.value === other.value
|
|
890
932
|
}
|
|
891
933
|
}
|
|
892
|
-
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
__name(generateValueObject, "generateValueObject");
|
|
937
|
+
|
|
938
|
+
// src/generators/templates/tests.ts
|
|
939
|
+
function generateControllerTest(pascal, kebab, plural) {
|
|
940
|
+
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
941
|
+
import { Container } from '@forinda/kickjs-core'
|
|
942
|
+
|
|
943
|
+
describe('${pascal}Controller', () => {
|
|
944
|
+
beforeEach(() => {
|
|
945
|
+
Container.reset()
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
it('should be defined', () => {
|
|
949
|
+
expect(true).toBe(true)
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
describe('POST /${plural}', () => {
|
|
953
|
+
it('should create a new ${kebab}', async () => {
|
|
954
|
+
// TODO: Set up test module, call create endpoint, assert 201
|
|
955
|
+
expect(true).toBe(true)
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
describe('GET /${plural}', () => {
|
|
960
|
+
it('should return paginated ${plural}', async () => {
|
|
961
|
+
// TODO: Set up test module, call list endpoint, assert { data, meta }
|
|
962
|
+
expect(true).toBe(true)
|
|
963
|
+
})
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
describe('GET /${plural}/:id', () => {
|
|
967
|
+
it('should return a ${kebab} by id', async () => {
|
|
968
|
+
// TODO: Create a ${kebab}, then fetch by id, assert match
|
|
969
|
+
expect(true).toBe(true)
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('should return 404 for non-existent ${kebab}', async () => {
|
|
973
|
+
// TODO: Fetch non-existent id, assert 404
|
|
974
|
+
expect(true).toBe(true)
|
|
975
|
+
})
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
describe('PUT /${plural}/:id', () => {
|
|
979
|
+
it('should update an existing ${kebab}', async () => {
|
|
980
|
+
// TODO: Create, update, assert changes
|
|
981
|
+
expect(true).toBe(true)
|
|
982
|
+
})
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
describe('DELETE /${plural}/:id', () => {
|
|
986
|
+
it('should delete a ${kebab}', async () => {
|
|
987
|
+
// TODO: Create, delete, assert gone
|
|
988
|
+
expect(true).toBe(true)
|
|
989
|
+
})
|
|
990
|
+
})
|
|
991
|
+
})
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
__name(generateControllerTest, "generateControllerTest");
|
|
995
|
+
function generateRepositoryTest(pascal, kebab, plural) {
|
|
996
|
+
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
997
|
+
import { InMemory${pascal}Repository } from '../infrastructure/repositories/in-memory-${kebab}.repository'
|
|
998
|
+
|
|
999
|
+
describe('InMemory${pascal}Repository', () => {
|
|
1000
|
+
let repo: InMemory${pascal}Repository
|
|
1001
|
+
|
|
1002
|
+
beforeEach(() => {
|
|
1003
|
+
repo = new InMemory${pascal}Repository()
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
it('should create and retrieve a ${kebab}', async () => {
|
|
1007
|
+
const created = await repo.create({ name: 'Test ${pascal}' })
|
|
1008
|
+
expect(created).toBeDefined()
|
|
1009
|
+
expect(created.name).toBe('Test ${pascal}')
|
|
1010
|
+
expect(created.id).toBeDefined()
|
|
1011
|
+
|
|
1012
|
+
const found = await repo.findById(created.id)
|
|
1013
|
+
expect(found).toEqual(created)
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
it('should return null for non-existent id', async () => {
|
|
1017
|
+
const found = await repo.findById('non-existent')
|
|
1018
|
+
expect(found).toBeNull()
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
it('should list all ${plural}', async () => {
|
|
1022
|
+
await repo.create({ name: '${pascal} 1' })
|
|
1023
|
+
await repo.create({ name: '${pascal} 2' })
|
|
1024
|
+
|
|
1025
|
+
const all = await repo.findAll()
|
|
1026
|
+
expect(all).toHaveLength(2)
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
it('should return paginated results', async () => {
|
|
1030
|
+
await repo.create({ name: '${pascal} 1' })
|
|
1031
|
+
await repo.create({ name: '${pascal} 2' })
|
|
1032
|
+
await repo.create({ name: '${pascal} 3' })
|
|
1033
|
+
|
|
1034
|
+
const result = await repo.findPaginated({
|
|
1035
|
+
filters: [],
|
|
1036
|
+
sort: [],
|
|
1037
|
+
search: '',
|
|
1038
|
+
pagination: { page: 1, limit: 2, offset: 0 },
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
expect(result.data).toHaveLength(2)
|
|
1042
|
+
expect(result.total).toBe(3)
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
it('should update a ${kebab}', async () => {
|
|
1046
|
+
const created = await repo.create({ name: 'Original' })
|
|
1047
|
+
const updated = await repo.update(created.id, { name: 'Updated' })
|
|
1048
|
+
expect(updated.name).toBe('Updated')
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
it('should delete a ${kebab}', async () => {
|
|
1052
|
+
const created = await repo.create({ name: 'To Delete' })
|
|
1053
|
+
await repo.delete(created.id)
|
|
1054
|
+
const found = await repo.findById(created.id)
|
|
1055
|
+
expect(found).toBeNull()
|
|
1056
|
+
})
|
|
1057
|
+
})
|
|
1058
|
+
`;
|
|
1059
|
+
}
|
|
1060
|
+
__name(generateRepositoryTest, "generateRepositoryTest");
|
|
1061
|
+
|
|
1062
|
+
// src/generators/module.ts
|
|
1063
|
+
async function generateModule(options) {
|
|
1064
|
+
const { name, modulesDir, noEntity, noTests, repo = "inmemory", minimal } = options;
|
|
1065
|
+
const kebab = toKebabCase(name);
|
|
1066
|
+
const pascal = toPascalCase(name);
|
|
1067
|
+
const camel = toCamelCase(name);
|
|
1068
|
+
const plural = pluralize(kebab);
|
|
1069
|
+
const pluralPascal = pluralizePascal(pascal);
|
|
1070
|
+
const moduleDir = join2(modulesDir, plural);
|
|
1071
|
+
const files = [];
|
|
1072
|
+
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1073
|
+
const fullPath = join2(moduleDir, relativePath);
|
|
1074
|
+
await writeFileSafe(fullPath, content);
|
|
1075
|
+
files.push(fullPath);
|
|
1076
|
+
}, "write");
|
|
1077
|
+
await write("index.ts", generateModuleIndex(pascal, kebab, plural, repo));
|
|
1078
|
+
await write("constants.ts", generateConstants(pascal));
|
|
1079
|
+
await write(`presentation/${kebab}.controller.ts`, generateController(pascal, kebab, plural, pluralPascal));
|
|
1080
|
+
await write(`application/dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
|
|
1081
|
+
await write(`application/dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
|
|
1082
|
+
await write(`application/dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
|
|
1083
|
+
const useCases = generateUseCases(pascal, kebab, plural, pluralPascal);
|
|
1084
|
+
for (const uc of useCases) {
|
|
1085
|
+
await write(`application/use-cases/${uc.file}`, uc.content);
|
|
1086
|
+
}
|
|
1087
|
+
await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab));
|
|
1088
|
+
await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService(pascal, kebab));
|
|
1089
|
+
if (repo === "inmemory") {
|
|
1090
|
+
await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, generateInMemoryRepository(pascal, kebab));
|
|
1091
|
+
}
|
|
1092
|
+
if (!noEntity && !minimal) {
|
|
1093
|
+
await write(`domain/entities/${kebab}.entity.ts`, generateEntity(pascal, kebab));
|
|
1094
|
+
await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject(pascal, kebab));
|
|
1095
|
+
}
|
|
1096
|
+
if (!noTests) {
|
|
1097
|
+
await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
|
|
1098
|
+
await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural));
|
|
893
1099
|
}
|
|
894
1100
|
await autoRegisterModule(modulesDir, pascal, plural);
|
|
895
1101
|
return files;
|
|
@@ -1143,7 +1349,7 @@ __name(generateService, "generateService");
|
|
|
1143
1349
|
|
|
1144
1350
|
// src/generators/controller.ts
|
|
1145
1351
|
import { join as join7 } from "path";
|
|
1146
|
-
async function
|
|
1352
|
+
async function generateController2(options) {
|
|
1147
1353
|
const { name, outDir } = options;
|
|
1148
1354
|
const kebab = toKebabCase(name);
|
|
1149
1355
|
const pascal = toPascalCase(name);
|
|
@@ -1170,7 +1376,7 @@ export class ${pascal}Controller {
|
|
|
1170
1376
|
files.push(filePath);
|
|
1171
1377
|
return files;
|
|
1172
1378
|
}
|
|
1173
|
-
__name(
|
|
1379
|
+
__name(generateController2, "generateController");
|
|
1174
1380
|
|
|
1175
1381
|
// src/generators/dto.ts
|
|
1176
1382
|
import { join as join8 } from "path";
|
|
@@ -1313,7 +1519,7 @@ function registerGenerateCommand(program) {
|
|
|
1313
1519
|
printGenerated(files);
|
|
1314
1520
|
});
|
|
1315
1521
|
gen.command("controller <name>").description("Generate a @Controller() class with basic routes").option("-o, --out <dir>", "Output directory", "src/controllers").action(async (name, opts) => {
|
|
1316
|
-
const files = await
|
|
1522
|
+
const files = await generateController2({
|
|
1317
1523
|
name,
|
|
1318
1524
|
outDir: resolve2(opts.out)
|
|
1319
1525
|
});
|