@koalarx/nest 1.18.21 → 1.19.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.
@@ -0,0 +1,1671 @@
1
+ # Documentação Completa - Exemplo de Aplicação com Koala Libs
2
+
3
+ Esta documentação descreve a arquitetura e implementação de uma API REST completa usando a biblioteca **@koalarx/nest**, seguindo o padrão **DDD (Domain-Driven Design)**.
4
+
5
+ ## 📋 Tabela de Conteúdos
6
+
7
+ 1. [Visão Geral](#visão-geral)
8
+ 2. [Camada Domain](#camada-domain)
9
+ 3. [Camada Application](#camada-application)
10
+ 4. [Camada Host](#camada-host)
11
+ 5. [Camada Infra](#camada-infra)
12
+ 6. [Testes](#testes)
13
+ 7. [Jobs e Eventos](#jobs-e-eventos)
14
+
15
+ ---
16
+
17
+ ## Visão Geral
18
+
19
+ A aplicação exemplo demonstra um **CRUD de Pessoa** com a seguinte estrutura:
20
+
21
+ - **Domain**: Definição de entidades, DTOs e interfaces de repositório
22
+ - **Application**: Handlers que implementam a lógica de negócio
23
+ - **Host**: Controllers que expõem endpoints HTTP
24
+ - **Infra**: Implementação de repositórios e acesso ao banco de dados
25
+ - **Tests**: Testes unitários e E2E
26
+
27
+ ### Fluxo de Requisição
28
+
29
+ O fluxo padrão de uma operação CRUD segue este padrão:
30
+
31
+ ```
32
+ Cliente HTTP → Controller (Host) → Handler (Application)
33
+ → AutoMapper → Repository (Infra) → Database → Response
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Camada Domain
39
+
40
+ A camada **Domain** contém as definições core da aplicação: entidades, DTOs e interfaces de repositório.
41
+
42
+ ### Entidades
43
+
44
+ As entidades representam os objetos de negócio principais.
45
+
46
+ #### Person.ts
47
+
48
+ ```typescript
49
+ import { EntityBase } from '@koalarx/nest/core/database/entity.base'
50
+ import { Entity } from '@koalarx/nest/core/database/entity.decorator'
51
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
52
+ import { List } from '@koalarx/nest/core/utils/list'
53
+ import { PersonAddress } from './person-address'
54
+ import { PersonPhone } from './person-phone'
55
+
56
+ @Entity()
57
+ export class Person extends EntityBase<Person> {
58
+ @AutoMap()
59
+ id: number
60
+
61
+ @AutoMap()
62
+ name: string
63
+
64
+ @AutoMap({ type: () => List })
65
+ phones = new List(PersonPhone)
66
+
67
+ @AutoMap({ type: () => PersonAddress })
68
+ address: PersonAddress
69
+
70
+ @AutoMap()
71
+ active: boolean
72
+ }
73
+ ```
74
+
75
+ **Característica**: A entidade estende `EntityBase` e utiliza o decorador `@AutoMap()` para permitir mapeamento automático.
76
+
77
+ #### PersonAddress.ts
78
+
79
+ ```typescript
80
+ import { EntityBase } from '@koalarx/nest/core/database/entity.base'
81
+ import { Entity } from '@koalarx/nest/core/database/entity.decorator'
82
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
83
+
84
+ @Entity()
85
+ export class PersonAddress extends EntityBase<PersonAddress> {
86
+ @AutoMap()
87
+ id: number
88
+
89
+ @AutoMap()
90
+ address: string
91
+ }
92
+ ```
93
+
94
+ #### PersonPhone.ts
95
+
96
+ ```typescript
97
+ import { EntityBase } from '@koalarx/nest/core/database/entity.base'
98
+ import { Entity } from '@koalarx/nest/core/database/entity.decorator'
99
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
100
+
101
+ @Entity()
102
+ export class PersonPhone extends EntityBase<PersonPhone> {
103
+ @AutoMap()
104
+ id: number
105
+
106
+ @AutoMap()
107
+ phone: string
108
+ }
109
+ ```
110
+
111
+ ### DTOs (Data Transfer Objects)
112
+
113
+ Os DTOs são utilizados para transferência de dados em queries e filtros.
114
+
115
+ #### ReadManyPersonDto.ts
116
+
117
+ ```typescript
118
+ import { PaginatedRequestProps } from '@koalarx/nest/core/controllers/pagination.request'
119
+ import { PaginationDto } from '@koalarx/nest/core/dtos/pagination.dto'
120
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
121
+
122
+ export class ReadManyPersonDto extends PaginationDto {
123
+ @AutoMap()
124
+ name?: string
125
+
126
+ @AutoMap()
127
+ active?: boolean
128
+
129
+ constructor(props?: PaginatedRequestProps<ReadManyPersonDto>) {
130
+ super()
131
+ Object.assign(this, props)
132
+ }
133
+ }
134
+ ```
135
+
136
+ ### Interface de Repositório
137
+
138
+ A interface de repositório define o contrato para operações de persistência.
139
+
140
+ #### IPersonRepository.ts
141
+
142
+ ```typescript
143
+ import { ListResponseBase } from '@koalarx/nest/core/controllers/list-response.base'
144
+ import { ReadManyPersonDto } from '../dtos/read-many-person.dto'
145
+ import { Person } from '../entities/person/person'
146
+
147
+ export abstract class IPersonRepository {
148
+ abstract save(person: Person): Promise<any>
149
+ abstract read(id: number): Promise<Person | null>
150
+ abstract readMany(query: ReadManyPersonDto): Promise<ListResponseBase<Person>>
151
+ abstract delete(id: number): Promise<void>
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Camada Application
158
+
159
+ A camada **Application** contém a lógica de negócio através de Handlers, Validators e Mappings.
160
+
161
+ ### Mapeamento (AutoMapping)
162
+
163
+ O AutoMapping facilita a conversão entre Request, Entity e Response de forma transparente.
164
+
165
+ #### PersonMapping.ts
166
+
167
+ ```typescript
168
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
169
+ import { Person } from '@/domain/entities/person/person'
170
+ import { PersonAddress } from '@/domain/entities/person/person-address'
171
+ import { PersonPhone } from '@/domain/entities/person/person-phone'
172
+ import { createMap } from '@koalarx/nest/core/mapping/create-map'
173
+ import {
174
+ CreatePersonAddressRequest,
175
+ CreatePersonPhoneRequest,
176
+ CreatePersonRequest,
177
+ } from '../person/create/create-person.request'
178
+ import { ReadManyPersonRequest } from '../person/read-many/read-many-person.request'
179
+ import {
180
+ ReadPersonAddressResponse,
181
+ ReadPersonPhoneResponse,
182
+ ReadPersonResponse,
183
+ } from '../person/read/read-person.response'
184
+ import {
185
+ UpdatePersonAddressRequest,
186
+ UpdatePersonPhoneRequest,
187
+ UpdatePersonRequest,
188
+ } from '../person/update/update-person.request'
189
+
190
+ export class PersonMapping {
191
+ static createMap() {
192
+ // Mapeamentos de Create
193
+ createMap(CreatePersonAddressRequest, PersonAddress)
194
+ createMap(CreatePersonPhoneRequest, PersonPhone)
195
+ createMap(CreatePersonRequest, Person)
196
+
197
+ // Mapeamentos de Read (Entity para Response)
198
+ createMap(PersonAddress, ReadPersonAddressResponse)
199
+ createMap(PersonPhone, ReadPersonPhoneResponse)
200
+ createMap(Person, ReadPersonResponse)
201
+
202
+ // Mapeamentos de ReadMany
203
+ createMap(ReadManyPersonRequest, ReadManyPersonDto)
204
+
205
+ // Mapeamentos de Update
206
+ createMap(UpdatePersonAddressRequest, PersonAddress)
207
+ createMap(UpdatePersonPhoneRequest, PersonPhone)
208
+ createMap(UpdatePersonRequest, Person)
209
+ }
210
+ }
211
+ ```
212
+
213
+ ### Requests e Validators
214
+
215
+ Requests definem a estrutura de dados recebida e Validators realizam validação e transformação.
216
+
217
+ #### Common - PersistPersonRequest.ts
218
+
219
+ Base compartilhada entre Create e Update:
220
+
221
+ ```typescript
222
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
223
+ import { ApiProperty } from '@nestjs/swagger'
224
+
225
+ export class PersistPersonAddressRequest {
226
+ @ApiProperty({ example: 'Street 1' })
227
+ @AutoMap()
228
+ address: string
229
+ }
230
+
231
+ export class PersistPersonPhoneRequest {
232
+ @ApiProperty({ example: '22999999999' })
233
+ @AutoMap()
234
+ phone: string
235
+ }
236
+
237
+ export class PersistPersonRequest {
238
+ @ApiProperty({ example: 'John Doe' })
239
+ @AutoMap()
240
+ name: string
241
+
242
+ @ApiProperty({ type: [PersistPersonPhoneRequest] })
243
+ @AutoMap({ type: () => PersistPersonPhoneRequest, isArray: { addTo: true } })
244
+ phones: Array<PersistPersonPhoneRequest>
245
+
246
+ @ApiProperty({ type: PersistPersonAddressRequest })
247
+ @AutoMap({ type: () => PersistPersonAddressRequest })
248
+ address: PersistPersonAddressRequest
249
+ }
250
+ ```
251
+
252
+ #### Create - CreatePersonRequest.ts
253
+
254
+ ```typescript
255
+ import {
256
+ PersistPersonAddressRequest,
257
+ PersistPersonPhoneRequest,
258
+ PersistPersonRequest,
259
+ } from '../common/persist-person.request'
260
+
261
+ export class CreatePersonAddressRequest extends PersistPersonAddressRequest {}
262
+
263
+ export class CreatePersonPhoneRequest extends PersistPersonPhoneRequest {}
264
+
265
+ export class CreatePersonRequest extends PersistPersonRequest {}
266
+ ```
267
+
268
+ #### Create - CreatePersonValidator.ts
269
+
270
+ ```typescript
271
+ import { RequestValidatorBase } from '@koalarx/nest/core/request-overflow/request-validator.base'
272
+ import { z, ZodType, ZodTypeDef } from 'zod'
273
+ import { CreatePersonRequest } from './create-person.request'
274
+
275
+ export class CreatePersonValidator extends RequestValidatorBase<CreatePersonRequest> {
276
+ protected get schema(): ZodType<any, ZodTypeDef, any> {
277
+ return z.object({
278
+ name: z.string(),
279
+ phones: z.array(
280
+ z.object({
281
+ phone: z.string(),
282
+ }),
283
+ ),
284
+ address: z.object({
285
+ address: z.string(),
286
+ }),
287
+ })
288
+ }
289
+ }
290
+ ```
291
+
292
+ #### ReadMany - ReadManyPersonRequest.ts
293
+
294
+ ```typescript
295
+ import { PaginatedRequest } from '@koalarx/nest/core/controllers/pagination.request'
296
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
297
+
298
+ export class ReadManyPersonRequest extends PaginatedRequest {
299
+ @AutoMap()
300
+ name?: string
301
+
302
+ @AutoMap()
303
+ active?: boolean
304
+ }
305
+ ```
306
+
307
+ #### ReadMany - ReadManyValidator.ts
308
+
309
+ ```typescript
310
+ import { RequestValidatorBase } from '@koalarx/nest/core/request-overflow/request-validator.base'
311
+ import { z, ZodType, ZodTypeDef } from 'zod'
312
+ import { ReadManyPersonRequest } from './read-many-person.request'
313
+
314
+ export class ReadManyPersonValidator extends RequestValidatorBase<ReadManyPersonRequest> {
315
+ protected get schema(): ZodType<any, ZodTypeDef, any> {
316
+ return z.object({
317
+ name: z.string().optional(),
318
+ active: z.boolean().optional(),
319
+ page: z.number().optional(),
320
+ pageSize: z.number().optional(),
321
+ })
322
+ }
323
+ }
324
+ ```
325
+
326
+ #### Update - UpdatePersonRequest.ts e UpdatePersonValidator.ts
327
+
328
+ ```typescript
329
+ import {
330
+ PersistPersonAddressRequest,
331
+ PersistPersonPhoneRequest,
332
+ PersistPersonRequest,
333
+ } from '../common/persist-person.request'
334
+
335
+ export class UpdatePersonAddressRequest extends PersistPersonAddressRequest {
336
+ id?: number
337
+ }
338
+
339
+ export class UpdatePersonPhoneRequest extends PersistPersonPhoneRequest {
340
+ id?: number
341
+ }
342
+
343
+ export class UpdatePersonRequest extends PersistPersonRequest {}
344
+ ```
345
+
346
+ ```typescript
347
+ import { RequestValidatorBase } from '@koalarx/nest/core/request-overflow/request-validator.base'
348
+ import { z, ZodType, ZodTypeDef } from 'zod'
349
+ import { UpdatePersonRequest } from './update-person.request'
350
+
351
+ export class UpdatePersonValidator extends RequestValidatorBase<UpdatePersonRequest> {
352
+ protected get schema(): ZodType<any, ZodTypeDef, any> {
353
+ return z.object({
354
+ name: z.string().optional(),
355
+ phones: z.array(
356
+ z.object({
357
+ id: z.number().optional(),
358
+ phone: z.string(),
359
+ }),
360
+ ).optional(),
361
+ address: z.object({
362
+ id: z.number().optional(),
363
+ address: z.string(),
364
+ }).optional(),
365
+ })
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### Responses
371
+
372
+ As Responses definem a estrutura de dados retornada para o cliente.
373
+
374
+ #### CreatePersonResponse.ts
375
+
376
+ ```typescript
377
+ import { CreatedRegistreWithIdResponse } from '@koalarx/nest/core/controllers/created-registre-response.base'
378
+
379
+ export class CreatePersonResponse extends CreatedRegistreWithIdResponse {}
380
+ ```
381
+
382
+ #### ReadPersonResponse.ts
383
+
384
+ ```typescript
385
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
386
+ import { ApiProperty } from '@nestjs/swagger'
387
+
388
+ export class ReadPersonAddressResponse {
389
+ @ApiProperty()
390
+ @AutoMap()
391
+ id: number
392
+
393
+ @ApiProperty({ example: 'Street 1' })
394
+ @AutoMap()
395
+ address: string
396
+ }
397
+
398
+ export class ReadPersonPhoneResponse {
399
+ @ApiProperty()
400
+ @AutoMap()
401
+ id: number
402
+
403
+ @ApiProperty({ example: '22999999999' })
404
+ @AutoMap()
405
+ phone: string
406
+ }
407
+
408
+ export class ReadPersonResponse {
409
+ @ApiProperty()
410
+ @AutoMap()
411
+ id: number
412
+
413
+ @ApiProperty({ example: 'John Doe' })
414
+ @AutoMap()
415
+ name: string
416
+
417
+ @ApiProperty({ type: [ReadPersonPhoneResponse] })
418
+ @AutoMap({ type: () => ReadPersonPhoneResponse, isArray: true })
419
+ phones: Array<ReadPersonPhoneResponse>
420
+
421
+ @ApiProperty({ type: ReadPersonAddressResponse })
422
+ @AutoMap({ type: () => ReadPersonAddressResponse })
423
+ address: ReadPersonAddressResponse
424
+
425
+ @ApiProperty()
426
+ @AutoMap()
427
+ active: boolean
428
+ }
429
+ ```
430
+
431
+ #### ReadManyPersonResponse.ts
432
+
433
+ ```typescript
434
+ import { ListResponse } from '@koalarx/nest/core'
435
+ import { AutoMap } from '@koalarx/nest/core/mapping/auto-mapping.decorator'
436
+ import { ApiProperty } from '@nestjs/swagger'
437
+ import { ReadPersonResponse } from '../read/read-person.response'
438
+
439
+ export class ReadManyPersonResponse
440
+ implements ListResponse<ReadPersonResponse>
441
+ {
442
+ @ApiProperty({ type: [ReadPersonResponse] })
443
+ @AutoMap()
444
+ items: ReadPersonResponse[]
445
+
446
+ @ApiProperty()
447
+ @AutoMap()
448
+ count: number
449
+ }
450
+ ```
451
+
452
+ ### Handlers
453
+
454
+ Os Handlers implementam a lógica de negócio de cada operação.
455
+
456
+ #### CreatePersonHandler.ts
457
+
458
+ **Fluxo**: Request → Validar → Mapear para Entidade → Persistir → Retornar ID
459
+
460
+ ```typescript
461
+ import { Person } from '@/domain/entities/person/person'
462
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
463
+ import { AutoMappingService } from '@koalarx/nest/core/mapping/auto-mapping.service'
464
+ import { RequestHandlerBase } from '@koalarx/nest/core/request-overflow/request-handler.base'
465
+ import {
466
+ ok,
467
+ RequestResult,
468
+ } from '@koalarx/nest/core/request-overflow/request-result'
469
+ import { Injectable } from '@nestjs/common'
470
+ import { CreatePersonRequest } from './create-person.request'
471
+ import { CreatePersonResponse } from './create-person.response'
472
+ import { CreatePersonValidator } from './create-person.validator'
473
+
474
+ @Injectable()
475
+ export class CreatePersonHandler extends RequestHandlerBase<
476
+ CreatePersonRequest,
477
+ RequestResult<Error, CreatePersonResponse>
478
+ > {
479
+ constructor(
480
+ private readonly mapper: AutoMappingService,
481
+ private readonly repository: IPersonRepository,
482
+ ) {
483
+ super()
484
+ }
485
+
486
+ async handle(
487
+ req: CreatePersonRequest,
488
+ ): Promise<RequestResult<Error, CreatePersonResponse>> {
489
+ // 1. Validar dados
490
+ const person = this.mapper.map(
491
+ new CreatePersonValidator(req).validate(),
492
+ CreatePersonRequest,
493
+ Person,
494
+ )
495
+
496
+ // 2. Persistir no banco
497
+ const result = await this.repository.save(person)
498
+
499
+ // 3. Retornar resultado
500
+ return ok({ id: result.id })
501
+ }
502
+ }
503
+ ```
504
+
505
+ #### ReadPersonHandler.ts
506
+
507
+ **Fluxo**: ID → Buscar no banco → Mapear para Response → Retornar
508
+
509
+ ```typescript
510
+ import { Person } from '@/domain/entities/person/person'
511
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
512
+ import { ResourceNotFoundError } from '@koalarx/nest/core/errors/resource-not-found.error'
513
+ import { AutoMappingService } from '@koalarx/nest/core/mapping/auto-mapping.service'
514
+ import { RequestHandlerBase } from '@koalarx/nest/core/request-overflow/request-handler.base'
515
+ import {
516
+ failure,
517
+ ok,
518
+ RequestResult,
519
+ } from '@koalarx/nest/core/request-overflow/request-result'
520
+ import { Injectable } from '@nestjs/common'
521
+ import { ReadPersonResponse } from './read-person.response'
522
+
523
+ @Injectable()
524
+ export class ReadPersonHandler extends RequestHandlerBase<
525
+ number,
526
+ RequestResult<ResourceNotFoundError, ReadPersonResponse>
527
+ > {
528
+ constructor(
529
+ private readonly mapper: AutoMappingService,
530
+ private readonly repository: IPersonRepository,
531
+ ) {
532
+ super()
533
+ }
534
+
535
+ async handle(
536
+ id: number,
537
+ ): Promise<RequestResult<ResourceNotFoundError, ReadPersonResponse>> {
538
+ // 1. Buscar no banco
539
+ const person = await this.repository.read(id)
540
+
541
+ // 2. Validar existência
542
+ if (!person) {
543
+ return failure(new ResourceNotFoundError('Pessoa'))
544
+ }
545
+
546
+ // 3. Mapear para Response
547
+ return ok(this.mapper.map(person, Person, ReadPersonResponse))
548
+ }
549
+ }
550
+ ```
551
+
552
+ #### ReadManyPersonHandler.ts
553
+
554
+ **Fluxo**: Query → Mapear para DTO → Buscar no banco → Mapear para Response → Retornar com paginação
555
+
556
+ ```typescript
557
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
558
+ import { Person } from '@/domain/entities/person/person'
559
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
560
+ import { ResourceNotFoundError } from '@koalarx/nest/core/errors/resource-not-found.error'
561
+ import { AutoMappingService } from '@koalarx/nest/core/mapping/auto-mapping.service'
562
+ import { RequestHandlerBase } from '@koalarx/nest/core/request-overflow/request-handler.base'
563
+ import {
564
+ ok,
565
+ RequestResult,
566
+ } from '@koalarx/nest/core/request-overflow/request-result'
567
+ import { Injectable } from '@nestjs/common'
568
+ import { ReadPersonResponse } from '../read/read-person.response'
569
+ import { ReadManyPersonRequest } from './read-many-person.request'
570
+ import { ReadManyPersonResponse } from './read-many-person.response'
571
+ import { ReadManyPersonValidator } from './read-many.validator'
572
+
573
+ @Injectable()
574
+ export class ReadManyPersonHandler extends RequestHandlerBase<
575
+ ReadManyPersonRequest,
576
+ RequestResult<ResourceNotFoundError, ReadManyPersonResponse>
577
+ > {
578
+ constructor(
579
+ private readonly mapper: AutoMappingService,
580
+ private readonly repository: IPersonRepository,
581
+ ) {
582
+ super()
583
+ }
584
+
585
+ async handle(
586
+ query: ReadManyPersonRequest,
587
+ ): Promise<RequestResult<ResourceNotFoundError, ReadManyPersonResponse>> {
588
+ // 1. Validar e mapear query para DTO
589
+ const listOfPerson = await this.repository.readMany(
590
+ this.mapper.map(
591
+ new ReadManyPersonValidator(query).validate(),
592
+ ReadManyPersonRequest,
593
+ ReadManyPersonDto,
594
+ ),
595
+ )
596
+
597
+ // 2. Mapear entidades para responses
598
+ return ok({
599
+ ...listOfPerson,
600
+ items: listOfPerson.items.map((person) =>
601
+ this.mapper.map(person, Person, ReadPersonResponse),
602
+ ),
603
+ })
604
+ }
605
+ }
606
+ ```
607
+
608
+ #### UpdatePersonHandler.ts
609
+
610
+ **Fluxo**: ID + Request → Validar → Buscar entidade → Atualizar → Persistir
611
+
612
+ ```typescript
613
+ import { Person } from '@/domain/entities/person/person'
614
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
615
+ import { ResourceNotFoundError } from '@koalarx/nest/core/errors/resource-not-found.error'
616
+ import { AutoMappingService } from '@koalarx/nest/core/mapping/auto-mapping.service'
617
+ import { RequestHandlerBase } from '@koalarx/nest/core/request-overflow/request-handler.base'
618
+ import {
619
+ failure,
620
+ ok,
621
+ RequestResult,
622
+ } from '@koalarx/nest/core/request-overflow/request-result'
623
+ import { Injectable } from '@nestjs/common'
624
+ import { UpdatePersonRequest } from './update-person.request'
625
+ import { UpdatePersonValidator } from './update-person.validator'
626
+
627
+ type UpdatePersonHandleRequest = {
628
+ id: number
629
+ data: UpdatePersonRequest
630
+ }
631
+
632
+ @Injectable()
633
+ export class UpdatePersonHandler extends RequestHandlerBase<
634
+ UpdatePersonHandleRequest,
635
+ RequestResult<ResourceNotFoundError, null>
636
+ > {
637
+ constructor(
638
+ private readonly mapper: AutoMappingService,
639
+ private readonly repository: IPersonRepository,
640
+ ) {
641
+ super()
642
+ }
643
+
644
+ async handle({
645
+ id,
646
+ data,
647
+ }: UpdatePersonHandleRequest): Promise<RequestResult<Error, null>> {
648
+ // 1. Buscar entidade existente
649
+ const personInBd = await this.repository.read(id)
650
+
651
+ if (!personInBd) {
652
+ return failure(new ResourceNotFoundError('Person'))
653
+ }
654
+
655
+ // 2. Validar e mapear dados recebidos
656
+ const person = this.mapper.map(
657
+ new UpdatePersonValidator(data).validate(),
658
+ UpdatePersonRequest,
659
+ Person,
660
+ )
661
+
662
+ // 3. Atualizar propriedades
663
+ personInBd.name = person.name
664
+ personInBd.active = person.active
665
+ personInBd.address.address = person.address.address
666
+ personInBd.phones.update(person.phones.toArray())
667
+
668
+ // 4. Persistir
669
+ await this.repository.save(personInBd)
670
+
671
+ return ok(null)
672
+ }
673
+ }
674
+ ```
675
+
676
+ #### DeletePersonHandler.ts
677
+
678
+ **Fluxo**: ID → Validar existência → Deletar
679
+
680
+ ```typescript
681
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
682
+ import { ResourceNotFoundError } from '@koalarx/nest/core/errors/resource-not-found.error'
683
+ import { RequestHandlerBase } from '@koalarx/nest/core/request-overflow/request-handler.base'
684
+ import {
685
+ failure,
686
+ ok,
687
+ RequestResult,
688
+ } from '@koalarx/nest/core/request-overflow/request-result'
689
+ import { Injectable } from '@nestjs/common'
690
+
691
+ @Injectable()
692
+ export class DeletePersonHandler extends RequestHandlerBase<
693
+ number,
694
+ RequestResult<ResourceNotFoundError, null>
695
+ > {
696
+ constructor(private readonly repository: IPersonRepository) {
697
+ super()
698
+ }
699
+
700
+ async handle(
701
+ id: number,
702
+ ): Promise<RequestResult<ResourceNotFoundError, null>> {
703
+ // 1. Validar existência
704
+ const person = await this.repository.read(id)
705
+
706
+ if (!person) {
707
+ return failure(new ResourceNotFoundError('Pessoa'))
708
+ }
709
+
710
+ // 2. Deletar
711
+ await this.repository.delete(id)
712
+
713
+ return ok(null)
714
+ }
715
+ }
716
+ ```
717
+
718
+ ---
719
+
720
+ ## Camada Host
721
+
722
+ A camada **Host** contém os Controllers que expõem os endpoints HTTP.
723
+
724
+ ### Controllers
725
+
726
+ Os Controllers recebem a requisição HTTP, delegam ao Handler e retornam a resposta.
727
+
728
+ #### CreatePersonController.ts
729
+
730
+ ```typescript
731
+ import { CreatePersonHandler } from '@/application/person/create/create-person.handler'
732
+ import { CreatePersonRequest } from '@/application/person/create/create-person.request'
733
+ import { CreatePersonResponse } from '@/application/person/create/create-person.response'
734
+ import { IController } from '@koalarx/nest/core/controllers/base.controller'
735
+ import { Controller } from '@koalarx/nest/core/controllers/controller.decorator'
736
+ import { Body, HttpCode, HttpStatus, Post } from '@nestjs/common'
737
+ import { ApiCreatedResponse } from '@nestjs/swagger'
738
+ import { PERSON_ROUTER_CONFIG } from './router.config'
739
+
740
+ @Controller(PERSON_ROUTER_CONFIG)
741
+ export class CreatePersonController
742
+ implements IController<CreatePersonRequest, CreatePersonResponse>
743
+ {
744
+ constructor(private readonly handler: CreatePersonHandler) {}
745
+
746
+ @Post()
747
+ @ApiCreatedResponse({ type: CreatePersonResponse })
748
+ @HttpCode(HttpStatus.CREATED)
749
+ async handle(
750
+ @Body() request: CreatePersonRequest,
751
+ ): Promise<CreatePersonResponse> {
752
+ const response = await this.handler.handle(request)
753
+
754
+ if (response.isFailure()) {
755
+ throw response.value
756
+ }
757
+
758
+ return response.value
759
+ }
760
+ }
761
+ ```
762
+
763
+ **Endpoint**: `POST /person`
764
+
765
+ #### ReadPersonController.ts
766
+
767
+ ```typescript
768
+ import { ReadPersonHandler } from '@/application/person/read/read-person.handler'
769
+ import { ReadPersonResponse } from '@/application/person/read/read-person.response'
770
+ import { IController } from '@koalarx/nest/core/controllers/base.controller'
771
+ import { Controller } from '@koalarx/nest/core/controllers/controller.decorator'
772
+ import { Get, Param } from '@nestjs/common'
773
+ import { ApiOkResponse } from '@nestjs/swagger'
774
+ import { PERSON_ROUTER_CONFIG } from './router.config'
775
+
776
+ @Controller(PERSON_ROUTER_CONFIG)
777
+ export class ReadPersonController
778
+ implements IController<null, ReadPersonResponse, string>
779
+ {
780
+ constructor(private readonly handler: ReadPersonHandler) {}
781
+
782
+ @Get(':id')
783
+ @ApiOkResponse({ type: ReadPersonResponse })
784
+ async handle(_, @Param('id') id: string): Promise<ReadPersonResponse> {
785
+ const response = await this.handler.handle(+id)
786
+
787
+ if (response.isFailure()) {
788
+ throw response.value
789
+ }
790
+
791
+ return response.value
792
+ }
793
+ }
794
+ ```
795
+
796
+ **Endpoint**: `GET /person/:id`
797
+
798
+ #### ReadManyPersonController.ts
799
+
800
+ ```typescript
801
+ import { ReadManyPersonHandler } from '@/application/person/read-many/read-many-person.handler'
802
+ import { ReadManyPersonRequest } from '@/application/person/read-many/read-many-person.request'
803
+ import { ReadManyPersonResponse } from '@/application/person/read-many/read-many-person.response'
804
+ import { IController } from '@koalarx/nest/core/controllers/base.controller'
805
+ import { Controller } from '@koalarx/nest/core/controllers/controller.decorator'
806
+ import { Get, Query } from '@nestjs/common'
807
+ import { ApiOkResponse } from '@nestjs/swagger'
808
+ import { PERSON_ROUTER_CONFIG } from './router.config'
809
+
810
+ @Controller(PERSON_ROUTER_CONFIG)
811
+ export class ReadManyPersonController
812
+ implements IController<ReadManyPersonRequest, ReadManyPersonResponse>
813
+ {
814
+ constructor(private readonly handler: ReadManyPersonHandler) {}
815
+
816
+ @Get()
817
+ @ApiOkResponse({ type: ReadManyPersonResponse })
818
+ async handle(
819
+ @Query() query: ReadManyPersonRequest,
820
+ ): Promise<ReadManyPersonResponse> {
821
+ const response = await this.handler.handle(query)
822
+
823
+ if (response.isFailure()) {
824
+ throw response.value
825
+ }
826
+
827
+ return response.value
828
+ }
829
+ }
830
+ ```
831
+
832
+ **Endpoint**: `GET /person?name=value&active=true&page=1&pageSize=10`
833
+
834
+ #### UpdatePersonController.ts
835
+
836
+ ```typescript
837
+ import { UpdatePersonHandler } from '@/application/person/update/update-person.handler'
838
+ import { UpdatePersonRequest } from '@/application/person/update/update-person.request'
839
+ import { IController } from '@koalarx/nest/core/controllers/base.controller'
840
+ import { Controller } from '@koalarx/nest/core/controllers/controller.decorator'
841
+ import { Body, Param, Put } from '@nestjs/common'
842
+ import { ApiOkResponse } from '@nestjs/swagger'
843
+ import { PERSON_ROUTER_CONFIG } from './router.config'
844
+
845
+ @Controller(PERSON_ROUTER_CONFIG)
846
+ export class UpdatePersonController
847
+ implements IController<UpdatePersonRequest, void>
848
+ {
849
+ constructor(private readonly handler: UpdatePersonHandler) {}
850
+
851
+ @Put(':id')
852
+ @ApiOkResponse()
853
+ async handle(
854
+ @Body() request: UpdatePersonRequest,
855
+ @Param('id') id: string,
856
+ ): Promise<void> {
857
+ const response = await this.handler.handle({
858
+ id: +id,
859
+ data: request,
860
+ })
861
+
862
+ if (response.isFailure()) {
863
+ throw response.value
864
+ }
865
+ }
866
+ }
867
+ ```
868
+
869
+ **Endpoint**: `PUT /person/:id`
870
+
871
+ #### DeletePersonController.ts
872
+
873
+ ```typescript
874
+ import { DeletePersonHandler } from '@/application/person/delete/delete-person.handler'
875
+ import { IController } from '@koalarx/nest/core/controllers/base.controller'
876
+ import { Controller } from '@koalarx/nest/core/controllers/controller.decorator'
877
+ import { Delete, HttpCode, HttpStatus, Param } from '@nestjs/common'
878
+ import { ApiNoContentResponse } from '@nestjs/swagger'
879
+ import { PERSON_ROUTER_CONFIG } from './router.config'
880
+
881
+ @Controller(PERSON_ROUTER_CONFIG)
882
+ export class DeletePersonController implements IController<null, void, string> {
883
+ constructor(private readonly handler: DeletePersonHandler) {}
884
+
885
+ @Delete(':id')
886
+ @ApiNoContentResponse()
887
+ @HttpCode(HttpStatus.NO_CONTENT)
888
+ async handle(_, @Param('id') id: string): Promise<void> {
889
+ const response = await this.handler.handle(+id)
890
+
891
+ if (response.isFailure()) {
892
+ throw response.value
893
+ }
894
+ }
895
+ }
896
+ ```
897
+
898
+ **Endpoint**: `DELETE /person/:id`
899
+
900
+ ### Configuração de Rotas
901
+
902
+ #### RouterConfig.ts
903
+
904
+ ```typescript
905
+ import { RouterConfigBase } from '@koalarx/nest/core/controllers/router-config.base'
906
+
907
+ class PersonRouterConfig extends RouterConfigBase {
908
+ constructor() {
909
+ super('Person', '/person')
910
+ }
911
+ }
912
+
913
+ export const PERSON_ROUTER_CONFIG = new PersonRouterConfig()
914
+ ```
915
+
916
+ **Características**:
917
+ - Estende `RouterConfigBase` que centraliza a configuração de rotas
918
+ - Primeiro parâmetro: nome do recurso (`'Person'`)
919
+ - Segundo parâmetro: path base dos endpoints (`'/person'`)
920
+ - A instância é usada nos controllers via decorador `@Controller(PERSON_ROUTER_CONFIG)`
921
+
922
+ ---
923
+
924
+ ## Camada Infra
925
+
926
+ A camada **Infra** implementa o acesso aos dados através de repositórios concretos.
927
+
928
+ ### Repositório
929
+
930
+ #### PersonRepository.ts
931
+
932
+ ```typescript
933
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
934
+ import { Person } from '@/domain/entities/person/person'
935
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
936
+ import { CreatedRegistreWithIdResponse } from '@koalarx/nest/core/controllers/created-registre-response.base'
937
+ import { ListResponseBase } from '@koalarx/nest/core/controllers/list-response.base'
938
+ import { RepositoryBase } from '@koalarx/nest/core/database/repository.base'
939
+ import { PRISMA_TOKEN } from '@koalarx/nest/core/koala-nest-database.module'
940
+ import { Inject, Injectable } from '@nestjs/common'
941
+ import { Prisma } from 'prisma/generated/client'
942
+ import { DbTransactionContext } from '../db-transaction-context'
943
+
944
+ @Injectable()
945
+ export class PersonRepository
946
+ extends RepositoryBase<Person>
947
+ implements IPersonRepository
948
+ {
949
+ constructor(
950
+ @Inject(PRISMA_TOKEN)
951
+ prisma: DbTransactionContext,
952
+ ) {
953
+ super({
954
+ modelName: Person,
955
+ context: prisma,
956
+ include: {
957
+ phones: true,
958
+ address: true,
959
+ },
960
+ })
961
+ }
962
+
963
+ async save(person: Person): Promise<CreatedRegistreWithIdResponse | null> {
964
+ return this.saveChanges(person)
965
+ }
966
+
967
+ read(id: number): Promise<Person | null> {
968
+ return this.findById(id)
969
+ }
970
+
971
+ readMany(query: ReadManyPersonDto): Promise<ListResponseBase<Person>> {
972
+ return this.findManyAndCount<Prisma.PersonWhereInput>(
973
+ {
974
+ name: {
975
+ contains: query.name,
976
+ },
977
+ active: query.active,
978
+ },
979
+ query,
980
+ )
981
+ }
982
+
983
+ delete(id: number): Promise<void> {
984
+ return this.remove<Prisma.PersonWhereUniqueInput>({ id })
985
+ }
986
+ }
987
+ ```
988
+
989
+ **Características**:
990
+ - Estende `RepositoryBase` que fornece operações CRUD prontas
991
+ - Implementa `IPersonRepository` do contrato de domain
992
+ - Injeção de `DbTransactionContext` para gerenciar transações
993
+ - Métodos utilizam internals da classe base para buscar/salvar/deletar
994
+
995
+ #### Comportamento do Método `remove()` com Orphan Removal
996
+
997
+ O método `remove()` (utilizado no método `delete()`) possui internamente uma função de `orphanRemoval` que remove automaticamente todas as entidades associadas (relacionamentos) quando a entidade principal é deletada.
998
+
999
+ ```typescript
1000
+ // Exemplo: Deletar uma Pessoa
1001
+ await this.repository.delete(personId)
1002
+
1003
+ // Internamente, o RepositoryBase.remove() executará:
1004
+ // 1. Remove PersonPhones associados (orphanRemoval)
1005
+ // 2. Remove PersonAddress associado (orphanRemoval)
1006
+ // 3. Remove Person
1007
+ ```
1008
+
1009
+ **Para evitar deletar entidades associadas**, passe um array de relacionamentos que devem ser **preservados** como segundo parâmetro:
1010
+
1011
+ ```typescript
1012
+ // Exemplo: Deletar Person mas manter o Address
1013
+ delete(id: number): Promise<void> {
1014
+ // 'address' não será deletado, apenas desvínculado
1015
+ return this.remove<Prisma.PersonWhereUniqueInput>({ id }, ['address'])
1016
+ }
1017
+ ```
1018
+
1019
+ **Sintaxe completa**:
1020
+ ```typescript
1021
+ // Método remove com orphanRemoval seletivo
1022
+ remove<T extends Prisma.Args>(
1023
+ where: T,
1024
+ skipOrphanRemovalOn?: string[] // Relacionamentos a preservar
1025
+ ): Promise<void>
1026
+ ```
1027
+
1028
+ **Exemplos práticos**:
1029
+
1030
+ ```typescript
1031
+ // ❌ Deleta tudo (Person, Phones, Address)
1032
+ await this.remove({ id: 1 })
1033
+
1034
+ // ✅ Deleta Person e Phones, mas preserva Address
1035
+ await this.remove({ id: 1 }, ['address'])
1036
+
1037
+ // ✅ Deleta Person, mas preserva Phones e Address
1038
+ await this.remove({ id: 1 }, ['phones', 'address'])
1039
+
1040
+ // ✅ Deleta Person e Address, mas preserva Phones
1041
+ await this.remove({ id: 1 }, ['phones'])
1042
+ ```
1043
+
1044
+ **Caso de Uso**:
1045
+ Use `skipOrphanRemovalOn` quando você quer transferir relacionamentos para outro registro ou manter histórico antes de deletar a entidade principal.
1046
+
1047
+ ### Contexto de Transação
1048
+
1049
+ #### DbTransactionContext.ts
1050
+
1051
+ Gerencia transações com o Prisma, permitindo operações ACID em múltiplas tabelas:
1052
+
1053
+ ```typescript
1054
+ import { PrismaClientWithCustomTransaction } from '@koalarx/nest/core/database/prisma-client-with-custom-transaction.interface'
1055
+ import { PrismaTransactionalClient } from '@koalarx/nest/core/database/prisma-transactional-client'
1056
+ import { DefaultArgs } from '@prisma/client/runtime/client'
1057
+ import { Prisma } from 'prisma/generated/client'
1058
+
1059
+ export class DbTransactionContext
1060
+ extends PrismaTransactionalClient
1061
+ implements PrismaClientWithCustomTransaction
1062
+ {
1063
+ get person(): Prisma.PersonDelegate<DefaultArgs> {
1064
+ return this.transactionalClient.person
1065
+ }
1066
+
1067
+ get personPhone(): Prisma.PersonPhoneDelegate<DefaultArgs> {
1068
+ return this.transactionalClient.personPhone
1069
+ }
1070
+
1071
+ get personAddress(): Prisma.PersonAddressDelegate<
1072
+ DefaultArgs,
1073
+ Prisma.PrismaClientOptions
1074
+ > {
1075
+ return this.transactionalClient.personAddress
1076
+ }
1077
+ }
1078
+ ```
1079
+
1080
+ **Características**:
1081
+ - Estende `PrismaTransactionalClient` que gerencia o ciclo de vida da transação
1082
+ - Implementa `PrismaClientWithCustomTransaction` interface para type-safety
1083
+ - Getters para cada modelo (`person`, `personPhone`, `personAddress`) que retornam delegates Prisma
1084
+ - Esses delegates são usados pelo `RepositoryBase` para operações CRUD dentro de transações
1085
+ - Automaticamente injetado nos repositórios via `DbTransactionContext` token
1086
+
1087
+ ---
1088
+
1089
+ ## Testes
1090
+
1091
+ A biblioteca oferece suporte para testes unitários e E2E com setup facilitado.
1092
+
1093
+ ### Testes Unitários
1094
+
1095
+ #### Mockups
1096
+
1097
+ [create-person-request.mockup.ts](./test/mockup/person/create-person-request.mockup.ts):
1098
+
1099
+ ```typescript
1100
+ import { CreatePersonRequest } from '@/application/person/create/create-person.request'
1101
+ import { faker } from '@faker-js/faker'
1102
+ import { assignObject } from '@koalarx/nest/core/utils/assing-object'
1103
+
1104
+ export const createPersonRequestMockup = assignObject(CreatePersonRequest, {
1105
+ name: faker.person.fullName(),
1106
+ phones: [{ phone: faker.phone.number() }],
1107
+ address: { address: faker.location.streetAddress() },
1108
+ })
1109
+ ```
1110
+
1111
+ #### Setup do App de Testes
1112
+
1113
+ [create-unit-test-app.ts](./test/create-unit-test-app.ts):
1114
+
1115
+ ```typescript
1116
+ import { CreatePersonHandler } from '@/application/person/create/create-person.handler'
1117
+ import { MappingProfile } from '@/application/mapping/mapping.profile'
1118
+ import { AutoMappingService } from '@koalarx/nest/core/mapping/auto-mapping.service'
1119
+ import { KoalaAppTestDependencies } from '@koalarx/nest/test/koala-app-test-dependencies'
1120
+ import { PersonRepository } from './repositories/person.repository'
1121
+ import { DeletePersonHandler } from '@/application/person/delete/delete-person.handler'
1122
+ import { ReadPersonHandler } from '@/application/person/read/read-person.handler'
1123
+ import { ReadManyPersonHandler } from '@/application/person/read-many/read-many-person.handler'
1124
+ import { UpdatePersonHandler } from '@/application/person/update/update-person.handler'
1125
+
1126
+ export function createUnitTestApp() {
1127
+ const automapService = new AutoMappingService(new MappingProfile())
1128
+ const personRepository = new PersonRepository()
1129
+
1130
+ return new KoalaAppTestDependencies({
1131
+ dependencies: [
1132
+ new CreatePersonHandler(automapService, personRepository),
1133
+ new ReadPersonHandler(automapService, personRepository),
1134
+ new ReadManyPersonHandler(automapService, personRepository),
1135
+ new UpdatePersonHandler(automapService, personRepository),
1136
+ new DeletePersonHandler(personRepository),
1137
+ ],
1138
+ })
1139
+ }
1140
+ ```
1141
+
1142
+ **Nota sobre PersonRepository em Testes**: A classe `PersonRepository` usada aqui é uma implementação **fake/mock** para testes. Ela estende `InMemoryBaseRepository<Person>` (armazenamento em memória) ao invés de `RepositoryBase` (que usa Prisma). Já possui **abstrações prontas** herdadas de `InMemoryBaseRepository`:
1143
+ - `saveChanges()`: Persiste em memória
1144
+ - `findById()`: Busca por ID em memória
1145
+ - `findManyAndCount()`: Lista e conta registros em memória
1146
+ - `remove()`: Remove de memória
1147
+
1148
+ Essa implementação fake permite testes rápidos sem dependência de banco de dados real.
1149
+
1150
+ #### PersonRepository.ts (Fake para Testes)
1151
+
1152
+ ```typescript
1153
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
1154
+ import { Person } from '@/domain/entities/person/person'
1155
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
1156
+ import { ListResponseBase } from '@koalarx/nest/core/controllers/list-response.base'
1157
+ import { EntityActionType } from '@koalarx/nest/core/database/entity.base'
1158
+ import { InMemoryBaseRepository } from '@koalarx/nest/test/repositories/in-memory-base.repository'
1159
+
1160
+ export class PersonRepository
1161
+ extends InMemoryBaseRepository<Person>
1162
+ implements IPersonRepository
1163
+ {
1164
+ save(person: Person): Promise<any> {
1165
+ // Lógica específica de teste: marcar como inativo ao criar
1166
+ if (person._action === EntityActionType.create) {
1167
+ person.active = false
1168
+ }
1169
+
1170
+ return this.saveChanges(person, (item) => item.id === person.id)
1171
+ }
1172
+
1173
+ read(id: number): Promise<Person | null> {
1174
+ return this.findById(id)
1175
+ }
1176
+
1177
+ readMany(query: ReadManyPersonDto): Promise<ListResponseBase<Person>> {
1178
+ return this.findManyAndCount<ReadManyPersonDto>(
1179
+ query,
1180
+ (person) =>
1181
+ (!query.name || person.name.includes(query.name)) &&
1182
+ (query.active === undefined || person.active === query.active),
1183
+ )
1184
+ }
1185
+
1186
+ delete(id: number): Promise<void> {
1187
+ return this.remove((person) => person.id === id)
1188
+ }
1189
+ }
1190
+ ```
1191
+
1192
+ **Características**:
1193
+ - Estende `InMemoryBaseRepository<Person>` para armazenamento em memória (não usa banco de dados)
1194
+ - Implementa `IPersonRepository` mantendo o mesmo contrato da versão real
1195
+ - Métodos utilizam abstrações herdadas (`saveChanges`, `findById`, `findManyAndCount`, `remove`)
1196
+ - Pode adicionar lógica específica de teste (ex: `person.active = false` no `save()`)
1197
+ - Usado apenas em testes unitários com `createUnitTestApp()
1198
+
1199
+ #### Exemplo de Teste Unitário
1200
+
1201
+ [create-person.handler.spec.ts](./application/person/create/create-person.handler.spec.ts):
1202
+
1203
+ ```typescript
1204
+ import { createUnitTestApp } from '@/test/create-unit-test-app'
1205
+ import { createPersonRequestMockup } from '@/test/mockup/person/create-person-request.mockup'
1206
+ import { CreatePersonHandler } from './create-person.handler'
1207
+
1208
+ describe('CreatePersonHandler', () => {
1209
+ const app = createUnitTestApp()
1210
+
1211
+ it('should create a person', async () => {
1212
+ const handler = app.get(CreatePersonHandler)
1213
+ const request = createPersonRequestMockup
1214
+
1215
+ const result = await handler.handle(request)
1216
+
1217
+ expect(result.isOk()).toBeTruthy()
1218
+
1219
+ if (result.isOk()) {
1220
+ expect(result.value).toEqual({
1221
+ id: expect.any(Number),
1222
+ })
1223
+ }
1224
+ })
1225
+ })
1226
+ ```
1227
+
1228
+ [read-person.handler.spec.ts](./application/person/read/read-person.handler.spec.ts):
1229
+
1230
+ ```typescript
1231
+ import { createUnitTestApp } from '@/test/create-unit-test-app'
1232
+ import { createPersonRequestMockup } from '@/test/mockup/person/create-person-request.mockup'
1233
+ import { CreatePersonHandler } from '../create/create-person.handler'
1234
+ import { ReadPersonHandler } from './read-person.handler'
1235
+
1236
+ describe('ReadPersonHandler', () => {
1237
+ const app = createUnitTestApp()
1238
+
1239
+ it('should get a person by id', async () => {
1240
+ const createResult = await app
1241
+ .get(CreatePersonHandler)
1242
+ .handle(createPersonRequestMockup)
1243
+
1244
+ expect(createResult.isOk()).toBeTruthy()
1245
+
1246
+ if (createResult.isOk()) {
1247
+ const result = await app
1248
+ .get(ReadPersonHandler)
1249
+ .handle(createResult.value.id)
1250
+
1251
+ expect(result.isOk()).toBeTruthy()
1252
+
1253
+ if (result.isOk()) {
1254
+ expect(result.value.id).toBe(createResult.value.id)
1255
+ }
1256
+ }
1257
+ })
1258
+ })
1259
+ ```
1260
+
1261
+ ### Testes E2E
1262
+
1263
+ #### Setup do App E2E
1264
+
1265
+ [create-e2e-test-app.ts](./test/create-e2e-test-app.ts):
1266
+
1267
+ ```typescript
1268
+ import { AppModule } from '@/host/app.module'
1269
+ import { DbTransactionContext } from '@/infra/database/db-transaction-context'
1270
+ import { setPrismaClientOptions } from '@koalarx/nest/core/database/prisma.service'
1271
+ import { KoalaAppTest } from '@koalarx/nest/test/koala-app-test'
1272
+ import { Test } from '@nestjs/testing'
1273
+ import { PrismaPg } from '@prisma/adapter-pg'
1274
+ import 'dotenv/config'
1275
+ import { Pool } from 'pg'
1276
+
1277
+ export async function createE2ETestApp() {
1278
+ const pool = new Pool({
1279
+ connectionString: process.env.DATABASE_URL,
1280
+ })
1281
+ const adapter = new PrismaPg(pool)
1282
+ setPrismaClientOptions({ adapter })
1283
+
1284
+ return Test.createTestingModule({ imports: [AppModule] })
1285
+ .compile()
1286
+ .then((moduleRef) => moduleRef.createNestApplication())
1287
+ .then((app) =>
1288
+ new KoalaAppTest(app)
1289
+ .setDbTransactionContext(DbTransactionContext)
1290
+ .enableCors()
1291
+ .build(),
1292
+ )
1293
+ .then((app) => app.init())
1294
+ }
1295
+ ```
1296
+
1297
+ #### Exemplo de Teste E2E
1298
+
1299
+ [person.controller.e2e-spec.ts](./host/controllers/person/person.controller.e2e-spec.ts):
1300
+
1301
+ ```typescript
1302
+ import { createE2ETestApp } from '@/test/create-e2e-test-app'
1303
+ import { INestApplication } from '@nestjs/common'
1304
+ import request from 'supertest'
1305
+ import { PERSON_ROUTER_CONFIG } from './router.config'
1306
+
1307
+ describe(`CRUD OF PERSON`, () => {
1308
+ let app: INestApplication
1309
+ let personId: number
1310
+ let addressId: number
1311
+
1312
+ beforeAll(async () => {
1313
+ app = await createE2ETestApp()
1314
+ })
1315
+
1316
+ it('should create a person', async () => {
1317
+ const response = await request(app.getHttpServer())
1318
+ .post(PERSON_ROUTER_CONFIG.group)
1319
+ .send({
1320
+ name: 'John Doe',
1321
+ phones: [],
1322
+ address: {
1323
+ address: 'Streat 1',
1324
+ },
1325
+ })
1326
+
1327
+ personId = response.body.id
1328
+
1329
+ expect(response.statusCode).toBe(201)
1330
+ expect(response.body).toStrictEqual({
1331
+ id: expect.any(Number),
1332
+ })
1333
+ })
1334
+
1335
+ it('should get the created person', async () => {
1336
+ const response = await request(app.getHttpServer()).get(
1337
+ `${PERSON_ROUTER_CONFIG.group}/${personId}`,
1338
+ )
1339
+
1340
+ addressId = response.body.address.id
1341
+
1342
+ expect(response.statusCode).toBe(200)
1343
+ expect(response.body).toStrictEqual({
1344
+ id: personId,
1345
+ name: 'John Doe',
1346
+ phones: [],
1347
+ address: {
1348
+ id: expect.any(Number),
1349
+ address: 'Streat 1',
1350
+ },
1351
+ active: true,
1352
+ })
1353
+ })
1354
+
1355
+ it('should get all persons', async () => {
1356
+ const response = await request(app.getHttpServer()).get(
1357
+ PERSON_ROUTER_CONFIG.group,
1358
+ )
1359
+
1360
+ expect(response.statusCode).toBe(200)
1361
+ expect(response.body).toStrictEqual({
1362
+ items: [
1363
+ {
1364
+ id: personId,
1365
+ name: 'John Doe',
1366
+ phones: [],
1367
+ address: {
1368
+ id: addressId,
1369
+ address: 'Streat 1',
1370
+ },
1371
+ active: true,
1372
+ },
1373
+ ],
1374
+ count: 1,
1375
+ })
1376
+ })
1377
+
1378
+ it('should get all inactive persons', async () => {
1379
+ const response = await request(app.getHttpServer()).get(
1380
+ `${PERSON_ROUTER_CONFIG.group}?active=false`,
1381
+ )
1382
+
1383
+ expect(response.statusCode).toBe(200)
1384
+ expect(response.body).toStrictEqual({
1385
+ items: [],
1386
+ count: 0,
1387
+ })
1388
+ })
1389
+
1390
+ it('should get persons by name', async () => {
1391
+ const response = await request(app.getHttpServer()).get(
1392
+ `${PERSON_ROUTER_CONFIG.group}?name=John`,
1393
+ )
1394
+
1395
+ expect(response.statusCode).toBe(200)
1396
+ expect(response.body).toStrictEqual({
1397
+ items: [
1398
+ {
1399
+ id: personId,
1400
+ name: 'John Doe',
1401
+ phones: [],
1402
+ address: {
1403
+ id: addressId,
1404
+ address: 'Streat 1',
1405
+ },
1406
+ active: true,
1407
+ },
1408
+ ],
1409
+ count: 1,
1410
+ })
1411
+ })
1412
+
1413
+ it('should update the created person', async () => {
1414
+ const updateResponse = await request(app.getHttpServer())
1415
+ .put(`${PERSON_ROUTER_CONFIG.group}/${personId}`)
1416
+ .send({
1417
+ name: 'John Doe Updated',
1418
+ phones: [],
1419
+ address: {
1420
+ id: addressId,
1421
+ address: 'Streat 2',
1422
+ },
1423
+ active: true,
1424
+ })
1425
+
1426
+ expect(updateResponse.statusCode).toBe(200)
1427
+
1428
+ const response = await request(app.getHttpServer()).get(
1429
+ `${PERSON_ROUTER_CONFIG.group}/${personId}`,
1430
+ )
1431
+
1432
+ expect(response.body).toStrictEqual({
1433
+ id: personId,
1434
+ name: 'John Doe Updated',
1435
+ phones: [],
1436
+ address: {
1437
+ id: addressId,
1438
+ address: 'Streat 2',
1439
+ },
1440
+ active: true,
1441
+ })
1442
+ })
1443
+
1444
+ it('should delete the created person', async () => {
1445
+ const deleteResponse = await request(app.getHttpServer()).delete(
1446
+ `${PERSON_ROUTER_CONFIG.group}/${personId}`,
1447
+ )
1448
+
1449
+ expect(deleteResponse).toBeTruthy()
1450
+ expect(deleteResponse.statusCode).toBe(204)
1451
+ })
1452
+ })
1453
+ ```
1454
+
1455
+ ---
1456
+
1457
+ ## Jobs e Eventos
1458
+
1459
+ A biblioteca oferece abstração para **CronJobs** (tarefas agendadas) e **EventJobs** (processamento de eventos).
1460
+
1461
+ ### CronJobs
1462
+
1463
+ CronJobs são tarefas executadas em intervalo de tempo. A biblioteca gerencia automaticamente locks com Redis para evitar duplicação em ambientes com múltiplos pods.
1464
+
1465
+ #### CreatePersonJob.ts
1466
+
1467
+ ```typescript
1468
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
1469
+ import {
1470
+ CronJobHandlerBase,
1471
+ CronJobResponse,
1472
+ CronJobSettings,
1473
+ } from '@koalarx/nest/core/backgroud-services/cron-service/cron-job.handler.base'
1474
+ import { EventQueue } from '@koalarx/nest/core/backgroud-services/event-service/event-queue'
1475
+ import { ok } from '@koalarx/nest/core/request-overflow/request-result'
1476
+ import { ILoggingService } from '@koalarx/nest/services/logging/ilogging.service'
1477
+ import { IRedLockService } from '@koalarx/nest/services/redlock/ired-lock.service'
1478
+ import { Injectable } from '@nestjs/common'
1479
+ import { CreatePersonHandler } from '../create/create-person.handler'
1480
+ import { InactivePersonEvent } from '../events/inactive-person/inactive-person-event'
1481
+ import { PersonEventJob } from '../events/person-event.job'
1482
+
1483
+ @Injectable()
1484
+ export class CreatePersonJob extends CronJobHandlerBase {
1485
+ constructor(
1486
+ redlockService: IRedLockService,
1487
+ loggingService: ILoggingService,
1488
+ private readonly createPerson: CreatePersonHandler,
1489
+ private readonly repository: IPersonRepository,
1490
+ ) {
1491
+ super(redlockService, loggingService)
1492
+ }
1493
+
1494
+ protected async settings(): Promise<CronJobSettings> {
1495
+ return {
1496
+ isActive: true,
1497
+ timeInMinutes: 1,
1498
+ }
1499
+ }
1500
+
1501
+ protected async run(): Promise<CronJobResponse> {
1502
+ const result = await this.createPerson.handle({
1503
+ name: 'John Doe',
1504
+ phones: [{ phone: '22999999999' }],
1505
+ address: { address: 'Street 1' },
1506
+ })
1507
+
1508
+ if (result.isOk()) {
1509
+ const person = await this.repository.read(result.value.id)
1510
+
1511
+ if (person) {
1512
+ const jobs = new PersonEventJob()
1513
+ jobs.addEvent(new InactivePersonEvent())
1514
+
1515
+ // Dispatch: Enfilera os eventos para processamento assíncrono
1516
+ EventQueue.dispatchEventsForAggregate(jobs._id)
1517
+ }
1518
+
1519
+ console.log('Person created with id:', result.value.id)
1520
+ } else {
1521
+ console.error('Error creating person:', result.value)
1522
+ }
1523
+
1524
+ return ok(null)
1525
+ }
1526
+ }
1527
+ ```
1528
+
1529
+ #### DeleteInactiveJob.ts
1530
+
1531
+ ```typescript
1532
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
1533
+ import {
1534
+ CronJobHandlerBase,
1535
+ CronJobResponse,
1536
+ CronJobSettings,
1537
+ } from '@koalarx/nest/core/backgroud-services/cron-service/cron-job.handler.base'
1538
+ import { ok } from '@koalarx/nest/core/request-overflow/request-result'
1539
+ import { ILoggingService } from '@koalarx/nest/services/logging/ilogging.service'
1540
+ import { IRedLockService } from '@koalarx/nest/services/redlock/ired-lock.service'
1541
+ import { Injectable } from '@nestjs/common'
1542
+ import { DeletePersonHandler } from '../delete/delete-person.handler'
1543
+ import { ReadManyPersonHandler } from '../read-many/read-many-person.handler'
1544
+
1545
+ @Injectable()
1546
+ export class DeleteInactiveJob extends CronJobHandlerBase {
1547
+ constructor(
1548
+ redlockService: IRedLockService,
1549
+ loggingService: ILoggingService,
1550
+ private readonly readManyPerson: ReadManyPersonHandler,
1551
+ private readonly deletePerson: DeletePersonHandler,
1552
+ ) {
1553
+ super(redlockService, loggingService)
1554
+ }
1555
+
1556
+ protected async settings(): Promise<CronJobSettings> {
1557
+ return {
1558
+ isActive: true,
1559
+ timeInMinutes: 1,
1560
+ }
1561
+ }
1562
+
1563
+ protected async run(): Promise<CronJobResponse> {
1564
+ const result = await this.readManyPerson.handle(
1565
+ new ReadManyPersonDto({ active: false }),
1566
+ )
1567
+
1568
+ if (result.isOk()) {
1569
+ for (const person of result.value.items) {
1570
+ await this.deletePerson.handle(person.id)
1571
+
1572
+ console.log('Person with id was deleted:', person.id)
1573
+ }
1574
+ } else {
1575
+ console.error('Error to search inactive people:', result.value)
1576
+ }
1577
+
1578
+ return ok(null)
1579
+ }
1580
+ }
1581
+ ```
1582
+
1583
+ ### EventJobs
1584
+
1585
+ EventJobs permitem processar eventos de domínio de forma assíncrona. O fluxo é:
1586
+
1587
+ 1. **CronJob/Handler** cria evento e chama `EventQueue.dispatchEventsForAggregate(jobId)`
1588
+ 2. **EventQueue** enfilera os eventos para processamento
1589
+ 3. **EventHandlers** processam os eventos de forma assíncrona
1590
+ 4. **Resultado**: Lógica de negócio executada sem bloquear a requisição
1591
+
1592
+ #### PersonEventJob.ts
1593
+
1594
+ Define quais handlers processarão os eventos da Pessoa:
1595
+
1596
+ ```typescript
1597
+ import { Person } from '@/domain/entities/person/person'
1598
+ import { EventHandlerBase } from '@koalarx/nest/core/backgroud-services/event-service/event-handler.base'
1599
+ import { EventJob } from '@koalarx/nest/core/backgroud-services/event-service/event-job'
1600
+ import { Type } from '@nestjs/common'
1601
+ import { InactivePersonHandler } from './inactive-person/inactive-person-handler'
1602
+
1603
+ export class PersonEventJob extends EventJob<Person> {
1604
+ defineHandlers(): Type<EventHandlerBase>[] {
1605
+ return [InactivePersonHandler]
1606
+ }
1607
+ }
1608
+ ```
1609
+
1610
+ **Características**:
1611
+ - Estende `EventJob<Person>` tipando a entidade agregada
1612
+ - Método `defineHandlers()` retorna array de handlers que processarão os eventos
1613
+ - Cada handler é responsável por uma ação específica
1614
+
1615
+ #### InactivePersonEvent.ts
1616
+
1617
+ ```typescript
1618
+ import { EventClass } from '@koalarx/nest/core/backgroud-services/event-service/event-class'
1619
+
1620
+ export class InactivePersonEvent extends EventClass {}
1621
+ ```
1622
+
1623
+ **Características**:
1624
+ - Estende `EventClass` como marcador de evento de domínio
1625
+ - Pode ser utilizado em múltiplos handlers
1626
+ - Pode ser disparado de qualquer handler ou CronJob
1627
+
1628
+ #### InactivePersonHandler.ts
1629
+
1630
+ ```typescript
1631
+ import { ReadManyPersonDto } from '@/domain/dtos/read-many-person.dto'
1632
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
1633
+ import { EventHandlerBase } from '@koalarx/nest/core/backgroud-services/event-service/event-handler.base'
1634
+ import { Injectable } from '@nestjs/common'
1635
+ import { InactivePersonEvent } from './inactive-person-event'
1636
+
1637
+ @Injectable()
1638
+ export class InactivePersonHandler extends EventHandlerBase {
1639
+ constructor(private readonly repository: IPersonRepository) {
1640
+ super(InactivePersonEvent)
1641
+ }
1642
+
1643
+ async handleEvent(): Promise<void> {
1644
+ const result = await this.repository.readMany(
1645
+ new ReadManyPersonDto({ active: true }),
1646
+ )
1647
+
1648
+ for (const person of result.items) {
1649
+ person.active = false
1650
+ await this.repository.save(person)
1651
+ }
1652
+
1653
+ console.log(
1654
+ 'InactivePersonHandler: Registros ativos inativados com sucesso!',
1655
+ )
1656
+ }
1657
+ }
1658
+ ```
1659
+
1660
+ ---
1661
+
1662
+ ## Resumo de Boas Práticas
1663
+
1664
+ 1. **Separação de Responsabilidades**: Cada camada tem responsabilidade bem definida
1665
+ 2. **Injeção de Dependência**: Use NestJS DI para injetar repositórios e serviços
1666
+ 3. **AutoMapping**: Aproveite o decorador `@AutoMap()` para reduzir código boilerplate
1667
+ 4. **Validação**: Use `RequestValidatorBase` com Zod para validar e transformar dados
1668
+ 5. **Error Handling**: Retorne `RequestResult` com sucesso ou falha
1669
+ 6. **Testes**: Crie mocks e use a estrutura de testes fornecida
1670
+ 7. **CronJobs e EventJobs**: Use para processar tarefas assincronamente com segurança em clusters
1671
+