@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/index.js
CHANGED
|
@@ -57,21 +57,12 @@ __name(pluralizePascal, "pluralizePascal");
|
|
|
57
57
|
|
|
58
58
|
// src/generators/module.ts
|
|
59
59
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const pluralPascal = pluralizePascal(pascal);
|
|
67
|
-
const moduleDir = join(modulesDir, plural);
|
|
68
|
-
const files = [];
|
|
69
|
-
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
70
|
-
const fullPath = join(moduleDir, relativePath);
|
|
71
|
-
await writeFileSafe(fullPath, content);
|
|
72
|
-
files.push(fullPath);
|
|
73
|
-
}, "write");
|
|
74
|
-
await write("index.ts", `/**
|
|
60
|
+
|
|
61
|
+
// src/generators/templates/module-index.ts
|
|
62
|
+
function generateModuleIndex(pascal, kebab, plural, repo) {
|
|
63
|
+
const repoClass = repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`;
|
|
64
|
+
const repoFile = repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`;
|
|
65
|
+
return `/**
|
|
75
66
|
* ${pascal} Module
|
|
76
67
|
*
|
|
77
68
|
* Self-contained feature module following Domain-Driven Design (DDD).
|
|
@@ -86,7 +77,7 @@ async function generateModule(options) {
|
|
|
86
77
|
import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
|
|
87
78
|
import { buildRoutes } from '@forinda/kickjs-http'
|
|
88
79
|
import { ${pascal.toUpperCase()}_REPOSITORY } from './domain/repositories/${kebab}.repository'
|
|
89
|
-
import { ${
|
|
80
|
+
import { ${repoClass} } from './infrastructure/repositories/${repoFile}.repository'
|
|
90
81
|
import { ${pascal}Controller } from './presentation/${kebab}.controller'
|
|
91
82
|
|
|
92
83
|
// Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
|
|
@@ -103,7 +94,7 @@ export class ${pascal}Module implements AppModule {
|
|
|
103
94
|
*/
|
|
104
95
|
register(container: Container): void {
|
|
105
96
|
container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
|
|
106
|
-
container.resolve(${
|
|
97
|
+
container.resolve(${repoClass}),
|
|
107
98
|
)
|
|
108
99
|
}
|
|
109
100
|
|
|
@@ -120,24 +111,15 @@ export class ${pascal}Module implements AppModule {
|
|
|
120
111
|
}
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* @Get/@Post/@Put/@Delete(path?, validation?) \u2014 defines routes with optional Zod validation
|
|
133
|
-
* @Autowired() \u2014 injects dependencies lazily from the DI container
|
|
134
|
-
* @Middleware(...handlers) \u2014 attach middleware at class or method level
|
|
135
|
-
*
|
|
136
|
-
* Add Swagger decorators (@ApiTags, @ApiOperation, @ApiResponse) from @forinda/kickjs-swagger
|
|
137
|
-
* for automatic OpenAPI documentation.
|
|
138
|
-
*/
|
|
139
|
-
import { Controller, Get, Post, Put, Delete, Autowired } from '@forinda/kickjs-core'
|
|
140
|
-
import { RequestContext } from '@forinda/kickjs-http'
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
__name(generateModuleIndex, "generateModuleIndex");
|
|
117
|
+
|
|
118
|
+
// src/generators/templates/controller.ts
|
|
119
|
+
function generateController(pascal, kebab, plural, pluralPascal) {
|
|
120
|
+
return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
|
|
121
|
+
import type { RequestContext } from '@forinda/kickjs-http'
|
|
122
|
+
import { ApiTags } from '@forinda/kickjs-swagger'
|
|
141
123
|
import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
|
|
142
124
|
import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
|
|
143
125
|
import { List${pluralPascal}UseCase } from '../application/use-cases/list-${plural}.use-case'
|
|
@@ -145,6 +127,7 @@ import { Update${pascal}UseCase } from '../application/use-cases/update-${kebab}
|
|
|
145
127
|
import { Delete${pascal}UseCase } from '../application/use-cases/delete-${kebab}.use-case'
|
|
146
128
|
import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
|
|
147
129
|
import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
|
|
130
|
+
import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../constants'
|
|
148
131
|
|
|
149
132
|
@Controller()
|
|
150
133
|
export class ${pascal}Controller {
|
|
@@ -154,39 +137,65 @@ export class ${pascal}Controller {
|
|
|
154
137
|
@Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
|
|
155
138
|
@Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
|
|
156
139
|
|
|
157
|
-
@Post('/', { body: create${pascal}Schema })
|
|
158
|
-
async create(ctx: RequestContext) {
|
|
159
|
-
const result = await this.create${pascal}UseCase.execute(ctx.body)
|
|
160
|
-
ctx.created(result)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
140
|
@Get('/')
|
|
141
|
+
@ApiTags('${pascal}')
|
|
142
|
+
@ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
|
|
164
143
|
async list(ctx: RequestContext) {
|
|
165
|
-
|
|
166
|
-
|
|
144
|
+
return ctx.paginate(
|
|
145
|
+
(parsed) => this.list${pluralPascal}UseCase.execute(parsed),
|
|
146
|
+
${pascal.toUpperCase()}_QUERY_CONFIG,
|
|
147
|
+
)
|
|
167
148
|
}
|
|
168
149
|
|
|
169
150
|
@Get('/:id')
|
|
151
|
+
@ApiTags('${pascal}')
|
|
170
152
|
async getById(ctx: RequestContext) {
|
|
171
153
|
const result = await this.get${pascal}UseCase.execute(ctx.params.id)
|
|
172
154
|
if (!result) return ctx.notFound('${pascal} not found')
|
|
173
155
|
ctx.json(result)
|
|
174
156
|
}
|
|
175
157
|
|
|
176
|
-
@
|
|
158
|
+
@Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
|
|
159
|
+
@ApiTags('${pascal}')
|
|
160
|
+
async create(ctx: RequestContext) {
|
|
161
|
+
const result = await this.create${pascal}UseCase.execute(ctx.body)
|
|
162
|
+
ctx.created(result)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
|
|
166
|
+
@ApiTags('${pascal}')
|
|
177
167
|
async update(ctx: RequestContext) {
|
|
178
168
|
const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
|
|
179
169
|
ctx.json(result)
|
|
180
170
|
}
|
|
181
171
|
|
|
182
172
|
@Delete('/:id')
|
|
173
|
+
@ApiTags('${pascal}')
|
|
183
174
|
async remove(ctx: RequestContext) {
|
|
184
175
|
await this.delete${pascal}UseCase.execute(ctx.params.id)
|
|
185
176
|
ctx.noContent()
|
|
186
177
|
}
|
|
187
178
|
}
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
__name(generateController, "generateController");
|
|
182
|
+
|
|
183
|
+
// src/generators/templates/constants.ts
|
|
184
|
+
function generateConstants(pascal) {
|
|
185
|
+
return `import type { QueryParamsConfig } from '@forinda/kickjs-core'
|
|
186
|
+
|
|
187
|
+
export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryParamsConfig = {
|
|
188
|
+
filterable: ['name'],
|
|
189
|
+
sortable: ['name', 'createdAt'],
|
|
190
|
+
searchable: ['name'],
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
__name(generateConstants, "generateConstants");
|
|
195
|
+
|
|
196
|
+
// src/generators/templates/dtos.ts
|
|
197
|
+
function generateCreateDTO(pascal, kebab) {
|
|
198
|
+
return `import { z } from 'zod'
|
|
190
199
|
|
|
191
200
|
/**
|
|
192
201
|
* Create ${pascal} DTO \u2014 Zod schema for validating POST request bodies.
|
|
@@ -202,23 +211,34 @@ export const create${pascal}Schema = z.object({
|
|
|
202
211
|
})
|
|
203
212
|
|
|
204
213
|
export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
__name(generateCreateDTO, "generateCreateDTO");
|
|
217
|
+
function generateUpdateDTO(pascal, kebab) {
|
|
218
|
+
return `import { z } from 'zod'
|
|
207
219
|
|
|
208
220
|
export const update${pascal}Schema = z.object({
|
|
209
221
|
name: z.string().min(1).max(200).optional(),
|
|
210
222
|
})
|
|
211
223
|
|
|
212
224
|
export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
__name(generateUpdateDTO, "generateUpdateDTO");
|
|
228
|
+
function generateResponseDTO(pascal, kebab) {
|
|
229
|
+
return `export interface ${pascal}ResponseDTO {
|
|
215
230
|
id: string
|
|
216
231
|
name: string
|
|
217
232
|
createdAt: string
|
|
218
233
|
updatedAt: string
|
|
219
234
|
}
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
__name(generateResponseDTO, "generateResponseDTO");
|
|
238
|
+
|
|
239
|
+
// src/generators/templates/use-cases.ts
|
|
240
|
+
function generateUseCases(pascal, kebab, plural, pluralPascal) {
|
|
241
|
+
return [
|
|
222
242
|
{
|
|
223
243
|
file: `create-${kebab}.use-case.ts`,
|
|
224
244
|
content: `/**
|
|
@@ -267,7 +287,7 @@ export class Get${pascal}UseCase {
|
|
|
267
287
|
file: `list-${plural}.use-case.ts`,
|
|
268
288
|
content: `import { Service, Inject } from '@forinda/kickjs-core'
|
|
269
289
|
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
|
|
270
|
-
import type {
|
|
290
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
271
291
|
|
|
272
292
|
@Service()
|
|
273
293
|
export class List${pluralPascal}UseCase {
|
|
@@ -275,8 +295,8 @@ export class List${pluralPascal}UseCase {
|
|
|
275
295
|
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
276
296
|
) {}
|
|
277
297
|
|
|
278
|
-
async execute(
|
|
279
|
-
return this.repo.
|
|
298
|
+
async execute(parsed: ParsedQuery) {
|
|
299
|
+
return this.repo.findPaginated(parsed)
|
|
280
300
|
}
|
|
281
301
|
}
|
|
282
302
|
`
|
|
@@ -318,10 +338,12 @@ export class Delete${pascal}UseCase {
|
|
|
318
338
|
`
|
|
319
339
|
}
|
|
320
340
|
];
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
341
|
+
}
|
|
342
|
+
__name(generateUseCases, "generateUseCases");
|
|
343
|
+
|
|
344
|
+
// src/generators/templates/repository.ts
|
|
345
|
+
function generateRepositoryInterface(pascal, kebab) {
|
|
346
|
+
return `/**
|
|
325
347
|
* ${pascal} Repository Interface
|
|
326
348
|
*
|
|
327
349
|
* Domain layer \u2014 defines the contract for data access.
|
|
@@ -334,43 +356,23 @@ export class Delete${pascal}UseCase {
|
|
|
334
356
|
import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
|
|
335
357
|
import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
|
|
336
358
|
import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
|
|
359
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
337
360
|
|
|
338
361
|
export interface I${pascal}Repository {
|
|
339
362
|
findById(id: string): Promise<${pascal}ResponseDTO | null>
|
|
340
363
|
findAll(): Promise<${pascal}ResponseDTO[]>
|
|
364
|
+
findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }>
|
|
341
365
|
create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO>
|
|
342
366
|
update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO>
|
|
343
367
|
delete(id: string): Promise<void>
|
|
344
368
|
}
|
|
345
369
|
|
|
346
370
|
export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
|
|
347
|
-
|
|
348
|
-
await write(`domain/services/${kebab}-domain.service.ts`, `/**
|
|
349
|
-
* ${pascal} Domain Service
|
|
350
|
-
*
|
|
351
|
-
* Domain layer \u2014 contains business rules that don't belong to a single entity.
|
|
352
|
-
* Use this for cross-entity logic, validation rules, and domain invariants.
|
|
353
|
-
* Keep it free of HTTP/framework concerns.
|
|
354
|
-
*/
|
|
355
|
-
import { Service, Inject, HttpException } from '@forinda/kickjs-core'
|
|
356
|
-
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
|
|
357
|
-
|
|
358
|
-
@Service()
|
|
359
|
-
export class ${pascal}DomainService {
|
|
360
|
-
constructor(
|
|
361
|
-
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
362
|
-
) {}
|
|
363
|
-
|
|
364
|
-
async ensureExists(id: string): Promise<void> {
|
|
365
|
-
const entity = await this.repo.findById(id)
|
|
366
|
-
if (!entity) {
|
|
367
|
-
throw HttpException.notFound('${pascal} not found')
|
|
368
|
-
}
|
|
369
|
-
}
|
|
371
|
+
`;
|
|
370
372
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
__name(generateRepositoryInterface, "generateRepositoryInterface");
|
|
374
|
+
function generateInMemoryRepository(pascal, kebab) {
|
|
375
|
+
return `/**
|
|
374
376
|
* In-Memory ${pascal} Repository
|
|
375
377
|
*
|
|
376
378
|
* Infrastructure layer \u2014 implements the repository interface using a Map.
|
|
@@ -381,6 +383,7 @@ export class ${pascal}DomainService {
|
|
|
381
383
|
*/
|
|
382
384
|
import { randomUUID } from 'node:crypto'
|
|
383
385
|
import { Repository, HttpException } from '@forinda/kickjs-core'
|
|
386
|
+
import type { ParsedQuery } from '@forinda/kickjs-http'
|
|
384
387
|
import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
|
|
385
388
|
import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
|
|
386
389
|
import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
|
|
@@ -398,6 +401,12 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
|
398
401
|
return Array.from(this.store.values())
|
|
399
402
|
}
|
|
400
403
|
|
|
404
|
+
async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
|
|
405
|
+
const all = Array.from(this.store.values())
|
|
406
|
+
const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
|
|
407
|
+
return { data, total: all.length }
|
|
408
|
+
}
|
|
409
|
+
|
|
401
410
|
async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
|
|
402
411
|
const now = new Date().toISOString()
|
|
403
412
|
const entity: ${pascal}ResponseDTO = {
|
|
@@ -423,10 +432,40 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
|
|
|
423
432
|
this.store.delete(id)
|
|
424
433
|
}
|
|
425
434
|
}
|
|
426
|
-
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
__name(generateInMemoryRepository, "generateInMemoryRepository");
|
|
438
|
+
|
|
439
|
+
// src/generators/templates/domain.ts
|
|
440
|
+
function generateDomainService(pascal, kebab) {
|
|
441
|
+
return `/**
|
|
442
|
+
* ${pascal} Domain Service
|
|
443
|
+
*
|
|
444
|
+
* Domain layer \u2014 contains business rules that don't belong to a single entity.
|
|
445
|
+
* Use this for cross-entity logic, validation rules, and domain invariants.
|
|
446
|
+
* Keep it free of HTTP/framework concerns.
|
|
447
|
+
*/
|
|
448
|
+
import { Service, Inject, HttpException } from '@forinda/kickjs-core'
|
|
449
|
+
import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
|
|
450
|
+
|
|
451
|
+
@Service()
|
|
452
|
+
export class ${pascal}DomainService {
|
|
453
|
+
constructor(
|
|
454
|
+
@Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
|
|
455
|
+
) {}
|
|
456
|
+
|
|
457
|
+
async ensureExists(id: string): Promise<void> {
|
|
458
|
+
const entity = await this.repo.findById(id)
|
|
459
|
+
if (!entity) {
|
|
460
|
+
throw HttpException.notFound('${pascal} not found')
|
|
461
|
+
}
|
|
427
462
|
}
|
|
428
|
-
|
|
429
|
-
|
|
463
|
+
}
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
__name(generateDomainService, "generateDomainService");
|
|
467
|
+
function generateEntity(pascal, kebab) {
|
|
468
|
+
return `/**
|
|
430
469
|
* ${pascal} Entity
|
|
431
470
|
*
|
|
432
471
|
* Domain layer \u2014 the core business object.
|
|
@@ -495,8 +534,11 @@ export class ${pascal} {
|
|
|
495
534
|
}
|
|
496
535
|
}
|
|
497
536
|
}
|
|
498
|
-
|
|
499
|
-
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
__name(generateEntity, "generateEntity");
|
|
540
|
+
function generateValueObject(pascal, kebab) {
|
|
541
|
+
return `/**
|
|
500
542
|
* ${pascal} ID Value Object
|
|
501
543
|
*
|
|
502
544
|
* Domain layer \u2014 wraps a primitive ID with type safety and validation.
|
|
@@ -530,7 +572,171 @@ export class ${pascal}Id {
|
|
|
530
572
|
return this.value === other.value
|
|
531
573
|
}
|
|
532
574
|
}
|
|
533
|
-
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
__name(generateValueObject, "generateValueObject");
|
|
578
|
+
|
|
579
|
+
// src/generators/templates/tests.ts
|
|
580
|
+
function generateControllerTest(pascal, kebab, plural) {
|
|
581
|
+
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
582
|
+
import { Container } from '@forinda/kickjs-core'
|
|
583
|
+
|
|
584
|
+
describe('${pascal}Controller', () => {
|
|
585
|
+
beforeEach(() => {
|
|
586
|
+
Container.reset()
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('should be defined', () => {
|
|
590
|
+
expect(true).toBe(true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
describe('POST /${plural}', () => {
|
|
594
|
+
it('should create a new ${kebab}', async () => {
|
|
595
|
+
// TODO: Set up test module, call create endpoint, assert 201
|
|
596
|
+
expect(true).toBe(true)
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
describe('GET /${plural}', () => {
|
|
601
|
+
it('should return paginated ${plural}', async () => {
|
|
602
|
+
// TODO: Set up test module, call list endpoint, assert { data, meta }
|
|
603
|
+
expect(true).toBe(true)
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
describe('GET /${plural}/:id', () => {
|
|
608
|
+
it('should return a ${kebab} by id', async () => {
|
|
609
|
+
// TODO: Create a ${kebab}, then fetch by id, assert match
|
|
610
|
+
expect(true).toBe(true)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('should return 404 for non-existent ${kebab}', async () => {
|
|
614
|
+
// TODO: Fetch non-existent id, assert 404
|
|
615
|
+
expect(true).toBe(true)
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
describe('PUT /${plural}/:id', () => {
|
|
620
|
+
it('should update an existing ${kebab}', async () => {
|
|
621
|
+
// TODO: Create, update, assert changes
|
|
622
|
+
expect(true).toBe(true)
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
describe('DELETE /${plural}/:id', () => {
|
|
627
|
+
it('should delete a ${kebab}', async () => {
|
|
628
|
+
// TODO: Create, delete, assert gone
|
|
629
|
+
expect(true).toBe(true)
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
__name(generateControllerTest, "generateControllerTest");
|
|
636
|
+
function generateRepositoryTest(pascal, kebab, plural) {
|
|
637
|
+
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
638
|
+
import { InMemory${pascal}Repository } from '../infrastructure/repositories/in-memory-${kebab}.repository'
|
|
639
|
+
|
|
640
|
+
describe('InMemory${pascal}Repository', () => {
|
|
641
|
+
let repo: InMemory${pascal}Repository
|
|
642
|
+
|
|
643
|
+
beforeEach(() => {
|
|
644
|
+
repo = new InMemory${pascal}Repository()
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('should create and retrieve a ${kebab}', async () => {
|
|
648
|
+
const created = await repo.create({ name: 'Test ${pascal}' })
|
|
649
|
+
expect(created).toBeDefined()
|
|
650
|
+
expect(created.name).toBe('Test ${pascal}')
|
|
651
|
+
expect(created.id).toBeDefined()
|
|
652
|
+
|
|
653
|
+
const found = await repo.findById(created.id)
|
|
654
|
+
expect(found).toEqual(created)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('should return null for non-existent id', async () => {
|
|
658
|
+
const found = await repo.findById('non-existent')
|
|
659
|
+
expect(found).toBeNull()
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('should list all ${plural}', async () => {
|
|
663
|
+
await repo.create({ name: '${pascal} 1' })
|
|
664
|
+
await repo.create({ name: '${pascal} 2' })
|
|
665
|
+
|
|
666
|
+
const all = await repo.findAll()
|
|
667
|
+
expect(all).toHaveLength(2)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('should return paginated results', async () => {
|
|
671
|
+
await repo.create({ name: '${pascal} 1' })
|
|
672
|
+
await repo.create({ name: '${pascal} 2' })
|
|
673
|
+
await repo.create({ name: '${pascal} 3' })
|
|
674
|
+
|
|
675
|
+
const result = await repo.findPaginated({
|
|
676
|
+
filters: [],
|
|
677
|
+
sort: [],
|
|
678
|
+
search: '',
|
|
679
|
+
pagination: { page: 1, limit: 2, offset: 0 },
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
expect(result.data).toHaveLength(2)
|
|
683
|
+
expect(result.total).toBe(3)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('should update a ${kebab}', async () => {
|
|
687
|
+
const created = await repo.create({ name: 'Original' })
|
|
688
|
+
const updated = await repo.update(created.id, { name: 'Updated' })
|
|
689
|
+
expect(updated.name).toBe('Updated')
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
it('should delete a ${kebab}', async () => {
|
|
693
|
+
const created = await repo.create({ name: 'To Delete' })
|
|
694
|
+
await repo.delete(created.id)
|
|
695
|
+
const found = await repo.findById(created.id)
|
|
696
|
+
expect(found).toBeNull()
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
__name(generateRepositoryTest, "generateRepositoryTest");
|
|
702
|
+
|
|
703
|
+
// src/generators/module.ts
|
|
704
|
+
async function generateModule(options) {
|
|
705
|
+
const { name, modulesDir, noEntity, noTests, repo = "inmemory", minimal } = options;
|
|
706
|
+
const kebab = toKebabCase(name);
|
|
707
|
+
const pascal = toPascalCase(name);
|
|
708
|
+
const camel = toCamelCase(name);
|
|
709
|
+
const plural = pluralize(kebab);
|
|
710
|
+
const pluralPascal = pluralizePascal(pascal);
|
|
711
|
+
const moduleDir = join(modulesDir, plural);
|
|
712
|
+
const files = [];
|
|
713
|
+
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
714
|
+
const fullPath = join(moduleDir, relativePath);
|
|
715
|
+
await writeFileSafe(fullPath, content);
|
|
716
|
+
files.push(fullPath);
|
|
717
|
+
}, "write");
|
|
718
|
+
await write("index.ts", generateModuleIndex(pascal, kebab, plural, repo));
|
|
719
|
+
await write("constants.ts", generateConstants(pascal));
|
|
720
|
+
await write(`presentation/${kebab}.controller.ts`, generateController(pascal, kebab, plural, pluralPascal));
|
|
721
|
+
await write(`application/dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
|
|
722
|
+
await write(`application/dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
|
|
723
|
+
await write(`application/dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
|
|
724
|
+
const useCases = generateUseCases(pascal, kebab, plural, pluralPascal);
|
|
725
|
+
for (const uc of useCases) {
|
|
726
|
+
await write(`application/use-cases/${uc.file}`, uc.content);
|
|
727
|
+
}
|
|
728
|
+
await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab));
|
|
729
|
+
await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService(pascal, kebab));
|
|
730
|
+
if (repo === "inmemory") {
|
|
731
|
+
await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, generateInMemoryRepository(pascal, kebab));
|
|
732
|
+
}
|
|
733
|
+
if (!noEntity && !minimal) {
|
|
734
|
+
await write(`domain/entities/${kebab}.entity.ts`, generateEntity(pascal, kebab));
|
|
735
|
+
await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject(pascal, kebab));
|
|
736
|
+
}
|
|
737
|
+
if (!noTests) {
|
|
738
|
+
await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
|
|
739
|
+
await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural));
|
|
534
740
|
}
|
|
535
741
|
await autoRegisterModule(modulesDir, pascal, plural);
|
|
536
742
|
return files;
|
|
@@ -784,7 +990,7 @@ __name(generateService, "generateService");
|
|
|
784
990
|
|
|
785
991
|
// src/generators/controller.ts
|
|
786
992
|
import { join as join6 } from "path";
|
|
787
|
-
async function
|
|
993
|
+
async function generateController2(options) {
|
|
788
994
|
const { name, outDir } = options;
|
|
789
995
|
const kebab = toKebabCase(name);
|
|
790
996
|
const pascal = toPascalCase(name);
|
|
@@ -811,7 +1017,7 @@ export class ${pascal}Controller {
|
|
|
811
1017
|
files.push(filePath);
|
|
812
1018
|
return files;
|
|
813
1019
|
}
|
|
814
|
-
__name(
|
|
1020
|
+
__name(generateController2, "generateController");
|
|
815
1021
|
|
|
816
1022
|
// src/generators/dto.ts
|
|
817
1023
|
import { join as join7 } from "path";
|
|
@@ -1116,7 +1322,7 @@ __name(loadKickConfig, "loadKickConfig");
|
|
|
1116
1322
|
export {
|
|
1117
1323
|
defineConfig,
|
|
1118
1324
|
generateAdapter,
|
|
1119
|
-
generateController,
|
|
1325
|
+
generateController2 as generateController,
|
|
1120
1326
|
generateDto,
|
|
1121
1327
|
generateGuard,
|
|
1122
1328
|
generateMiddleware,
|