@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.
- package/README.md +14 -0
- package/docs/00-cli-reference.md +201 -0
- package/docs/01-guia-instalacao.md +113 -0
- package/docs/02-configuracao-inicial.md +176 -0
- package/docs/04-tratamento-erros.md +303 -0
- package/docs/05-features-avancadas.md +969 -0
- package/docs/06-decoradores.md +220 -0
- package/docs/07-guia-bun.md +176 -0
- package/docs/08-prisma-client.md +487 -0
- package/docs/09-mcp-vscode-extension.md +437 -0
- package/docs/EXAMPLE.md +1671 -0
- package/docs/README.md +59 -0
- package/mcp-server/mcp.json.example +27 -0
- package/mcp-server/server.d.ts +2 -0
- package/mcp-server/server.d.ts.map +1 -0
- package/mcp-server/server.js +248 -0
- package/mcp-server/server.js.map +1 -0
- package/package.json +2 -1
- package/tsconfig.lib.tsbuildinfo +1 -1
package/docs/EXAMPLE.md
ADDED
|
@@ -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
|
+
|