@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,487 @@
1
+ # Integração com Prisma
2
+
3
+ ## Visão Geral
4
+
5
+ A biblioteca `@koalarx/nest` foi projetada para funcionar de forma **totalmente automática e transparente** com o Prisma Client do seu projeto. Não é necessária nenhuma configuração adicional além da geração padrão do cliente Prisma.
6
+
7
+ ## Configuração Automática
8
+
9
+ ### Pré-requisitos
10
+
11
+ 1. Você deve ter um esquema Prisma configurado (`prisma/schema.prisma`)
12
+ 2. O cliente Prisma deve ser gerado:
13
+
14
+ ```bash
15
+ bunx prisma generate
16
+ ```
17
+
18
+ ### Como Funciona
19
+
20
+ A biblioteca **resolve automaticamente** o `PrismaClient` do seu projeto através de um sistema inteligente de busca:
21
+
22
+ #### 1. Busca Automática de Caminhos
23
+
24
+ O `PrismaService` procura o cliente Prisma em múltiplos locais padrão:
25
+
26
+ - `prisma/generated/client.js` (ou `.ts`)
27
+ - `prisma/generated/index.js` (ou `.ts`)
28
+ - Diretórios relativos ao `process.cwd()`
29
+ - Diretórios relativos ao arquivo principal da aplicação
30
+
31
+ #### 2. Suporte Transparente
32
+
33
+ Uma vez encontrado, o cliente é carregado **dinamicamente** e exposto através de um `Proxy` transparente que:
34
+
35
+ - Permite acesso direto às models (ex: `prisma.person.findFirst()`)
36
+ - Preserva todos os métodos do PrismaClient (ex: `$queryRaw`, `$transaction`)
37
+ - Mantém compatibilidade total com transações
38
+
39
+ #### 3. Sem Configuração Necessária
40
+
41
+ ✅ Não é necessário:
42
+ - Configurar path mappings
43
+ - Definir variáveis de ambiente de caminho
44
+ - Registrar o cliente manualmente
45
+
46
+ Tudo funciona **out-of-the-box**!
47
+
48
+ ## Transações
49
+
50
+ ### Usando withTransaction
51
+
52
+ ```typescript
53
+ async executeTransaction() {
54
+ return this.prisma.withTransaction(async (client) => {
55
+ // client é um Prisma.TransactionClient
56
+ await client.person.create({ data: {...} })
57
+ await client.address.create({ data: {...} })
58
+ })
59
+ }
60
+ ```
61
+
62
+ ### Em Repositórios
63
+
64
+ O `RepositoryBase` fornece suporte built-in para transações:
65
+
66
+ ```typescript
67
+ async saveMultiple(entities: Person[]) {
68
+ return this.withTransaction(async (client) => {
69
+ for (const entity of entities) {
70
+ await client.person.create({ data: this.entityToPrisma(entity) })
71
+ }
72
+ })
73
+ }
74
+ ```
75
+
76
+ ### Acessar Tipos do DBContext em Repositórios
77
+
78
+ Ao estender `RepositoryBase`, você pode acessar o `DBContext` (que pode ser `PrismaClient` ou `DbTransactionContext` em transações) de duas formas:
79
+
80
+ #### Via Construtor com Configuração
81
+
82
+ Passe o DBContext e uma string indicando qual model Prisma usar. Para ter type safety completo com os tipos do DBContext, use 3 type parameters:
83
+
84
+ ```typescript
85
+ import { RepositoryBase } from '@koalarx/nest/core/database/repository.base'
86
+ import { Person } from '@/domain/entities/person/person'
87
+ import { IPersonRepository } from '@/domain/repositories/iperson.repository'
88
+ import { DbTransactionContext } from '../db-transaction-context'
89
+ import { PRISMA_TOKEN } from '@koalarx/nest/core/koala-nest-database.module'
90
+ import { Inject, Injectable } from '@nestjs/common'
91
+
92
+ @Injectable()
93
+ export class PersonRepository
94
+ extends RepositoryBase<Person, DbTransactionContext, 'person'>
95
+ implements IPersonRepository
96
+ {
97
+ constructor(
98
+ @Inject(PRISMA_TOKEN)
99
+ prisma: DbTransactionContext,
100
+ ) {
101
+ super({
102
+ modelName: Person, // Seu modelo/entidade
103
+ context: prisma, // DBContext injetado
104
+ include: { // Includes opcionais
105
+ phones: true,
106
+ address: true,
107
+ },
108
+ })
109
+ }
110
+
111
+ // Seus métodos de repositório
112
+ }
113
+ ```
114
+
115
+ **Type Parameters do RepositoryBase**:
116
+ - `Person` - Tipo da entidade
117
+ - `DbTransactionContext` - Tipo do DBContext (PrismaClient ou DbTransactionContext)
118
+ - `'person'` - String literal do model Prisma (deve corresponder ao nome da tabela/model)
119
+
120
+ #### Acessar o Contexto dentro de Métodos
121
+
122
+ Use `this.context()` para acessar o DBContext dentro de qualquer método. Com os 3 type parameters, você terá type safety completo:
123
+
124
+ ```typescript
125
+ @Injectable()
126
+ export class PersonRepository
127
+ extends RepositoryBase<Person, DbTransactionContext, 'person'>
128
+ implements IPersonRepository
129
+ {
130
+ constructor(
131
+ @Inject(PRISMA_TOKEN)
132
+ prisma: DbTransactionContext,
133
+ ) {
134
+ super({
135
+ modelName: Person,
136
+ context: prisma,
137
+ include: { phones: true, address: true },
138
+ })
139
+ }
140
+
141
+ async findByNameWithDetails(name: string): Promise<Person | null> {
142
+ // this.context() retorna DbTransactionContext com type safety
143
+ // Intellisense mostrará: person, personPhone, personAddress
144
+ const person = await this.context().person.findFirst({
145
+ where: { name },
146
+ include: { phones: true, address: true },
147
+ })
148
+
149
+ return person ? this.mapToDomain(person) : null
150
+ }
151
+
152
+ async complexOperation(): Promise<void> {
153
+ // Dentro de transações, this.context() retorna o client transacional
154
+ // Os tipos são preservados automáticamente
155
+ const result = await this.context().personPhone.createMany({
156
+ data: [
157
+ { personId: 1, phone: '123456' },
158
+ { personId: 1, phone: '789012' },
159
+ ],
160
+ })
161
+ }
162
+ }
163
+ ```
164
+
165
+ **Comportamento Automático com Type Safety**:
166
+ - **Fora de transações**: `this.context()` retorna `DbTransactionContext` com acesso tipado aos models
167
+ - **Dentro de transações**: `this.context()` retorna o cliente transacional preservando os tipos
168
+ - **Intellisense**: Com os 3 type parameters, o IDE autocompleta todos os models disponíveis
169
+
170
+ Isso garante que suas queries sempre executem no contexto correto com segurança de tipos.
171
+
172
+ ### Método `remove()` com Orphan Removal
173
+
174
+ O método `remove()` herdado de `RepositoryBase` possui internamente uma função de `orphanRemoval` que remove automaticamente todas as entidades associadas (relacionamentos) quando a entidade principal é deletada.
175
+
176
+ ```typescript
177
+ // Exemplo: Deletar uma Pessoa
178
+ await this.repository.delete(personId)
179
+
180
+ // Internamente, o RepositoryBase.remove() executará:
181
+ // 1. Remove PersonPhones associados (orphanRemoval)
182
+ // 2. Remove PersonAddress associado (orphanRemoval)
183
+ // 3. Remove Person
184
+ ```
185
+
186
+ **Para evitar deletar entidades associadas**, passe um array de relacionamentos que devem ser **preservados** como segundo parâmetro:
187
+
188
+ ```typescript
189
+ @Injectable()
190
+ export class PersonRepository
191
+ extends RepositoryBase<Person, DbTransactionContext, 'person'>
192
+ implements IPersonRepository
193
+ {
194
+ // ... constructor ...
195
+
196
+ delete(id: number): Promise<void> {
197
+ // 'address' não será deletado, apenas desvínculado
198
+ return this.remove<Prisma.PersonWhereUniqueInput>(
199
+ { id },
200
+ ['address'] // Preservar relacionamento
201
+ )
202
+ }
203
+ }
204
+ ```
205
+
206
+ **Sintaxe completa do método `remove()`**:
207
+ ```typescript
208
+ protected remove<TWhere = any>(
209
+ where: TWhere,
210
+ notCascadeEntityProps?: Array<keyof TEntity>, // Relacionamentos a preservar
211
+ externalServices?: Promise<any> // Promises dentro da transação
212
+ ): Promise<void>
213
+ ```
214
+
215
+ **Exemplos práticos**:
216
+
217
+ ```typescript
218
+ // ❌ Deleta tudo (Person, Phones, Address)
219
+ await this.remove({ id: 1 })
220
+
221
+ // ✅ Deleta Person e Phones, mas preserva Address
222
+ await this.remove({ id: 1 }, ['address'])
223
+
224
+ // ✅ Deleta Person, mas preserva Phones e Address
225
+ await this.remove({ id: 1 }, ['phones', 'address'])
226
+
227
+ // ✅ Deleta Person e Address, mas preserva Phones
228
+ await this.remove({ id: 1 }, ['phones'])
229
+ ```
230
+
231
+ ### Método `removeMany()` com Orphan Removal
232
+
233
+ Similar ao `remove()`, mas deleta múltiplas entidades em uma única operação transacional.
234
+
235
+ ```typescript
236
+ @Injectable()
237
+ export class PersonRepository
238
+ extends RepositoryBase<Person, DbTransactionContext, 'person'>
239
+ implements IPersonRepository
240
+ {
241
+ // ... constructor ...
242
+
243
+ async deleteInactive(): Promise<void> {
244
+ // Deleta múltiplas pessoas inativas
245
+ return this.removeMany<Prisma.PersonWhereInput>(
246
+ { active: false },
247
+ ['address'] // Preservar addresses mesmo ao deletar múltiplas pessoas
248
+ )
249
+ }
250
+ }
251
+ ```
252
+
253
+ **Sintaxe completa do método `removeMany()`**:
254
+ ```typescript
255
+ protected removeMany<TWhere = any>(
256
+ where: TWhere,
257
+ notCascadeEntityProps?: Array<keyof TEntity>, // Relacionamentos a preservar
258
+ externalServices?: Promise<any> // Promises dentro da transação
259
+ ): Promise<void>
260
+ ```
261
+
262
+ ### Parâmetro `externalServices` para Transações
263
+
264
+ Ambos os métodos `remove()` e `removeMany()` possuem um terceiro parâmetro `externalServices` que aceita uma `Promise`. Essa promise é **executada dentro da transação aberta**, permitindo que você execute operações externas de forma atômica.
265
+
266
+ ```typescript
267
+ @Injectable()
268
+ export class PersonRepository
269
+ extends RepositoryBase<Person, DbTransactionContext, 'person'>
270
+ implements IPersonRepository
271
+ {
272
+ constructor(
273
+ @Inject(PRISMA_TOKEN) prisma: DbTransactionContext,
274
+ private readonly auditService: AuditService,
275
+ private readonly notificationService: NotificationService,
276
+ ) {
277
+ super({
278
+ modelName: Person,
279
+ context: prisma,
280
+ })
281
+ }
282
+
283
+ async deleteWithAudit(id: number, userId: string): Promise<void> {
284
+ // Executar auditoria dentro da transação
285
+ const auditPromise = this.auditService.logDeletion(id, userId)
286
+
287
+ return this.remove<Prisma.PersonWhereUniqueInput>(
288
+ { id },
289
+ [],
290
+ auditPromise // Executado dentro da transação
291
+ )
292
+ }
293
+
294
+ async deleteInactiveWithNotification(): Promise<void> {
295
+ // Executar múltiplas operações externas
296
+ const externalOps = Promise.all([
297
+ this.auditService.logBulkDeletion('inactive_persons'),
298
+ this.notificationService.notifyAdmins('Deleted inactive persons'),
299
+ ])
300
+
301
+ return this.removeMany<Prisma.PersonWhereInput>(
302
+ { active: false },
303
+ ['address'],
304
+ externalOps // Ambas as operações dentro da transação
305
+ )
306
+ }
307
+ }
308
+ ```
309
+
310
+ **Comportamento do `externalServices`**:
311
+ - A promise é executada **antes** do delete/deleteMany acontecer
312
+ - Se a promise falhar, a transação inteira é revertida
313
+ - Todas as operações (incluindo a promise) são executadas de forma **atômica**
314
+ - Se não informar `externalServices`, o delete acontece normalmente sem dependências
315
+
316
+ **Caso de Uso**:
317
+ Use `externalServices` para operações que devem ser garantidas atomicamente com a deleção:
318
+ - Auditoria de exclusões
319
+ - Notificações
320
+ - Atualização de contadores
321
+ - Invalidação de cache
322
+ - Sincronização com sistemas externos
323
+
324
+ **Caso de Uso**:
325
+ Use `skipOrphanRemovalOn` quando você quer transferir relacionamentos para outro registro ou manter histórico antes de deletar a entidade principal.
326
+
327
+ ## Configuração Avançada
328
+
329
+ ### Configurar Opções do PrismaClient (Opcional)
330
+
331
+ Se você precisar personalizar opções do PrismaClient (como adapters, logging, etc.):
332
+
333
+ ```typescript
334
+ import { setPrismaClientOptions } from '@koalarx/nest'
335
+ import type { PrismaClientOptions } from 'prisma/generated/internal/prismaNamespace'
336
+
337
+ // Configure ANTES de inicializar a aplicação
338
+ setPrismaClientOptions({
339
+ log: [{ emit: 'event', level: 'query' }],
340
+ // outras opções...
341
+ } as PrismaClientOptions)
342
+
343
+ // Depois crie seu módulo
344
+ const app = await NestFactory.create(AppModule)
345
+ ```
346
+
347
+ ### Registrar Cliente Manualmente (Fallback)
348
+
349
+ Se por algum motivo a busca automática não encontrar o cliente, você pode registrá-lo manualmente:
350
+
351
+ ```typescript
352
+ import { setPrismaClient } from '@koalarx/nest'
353
+ import { PrismaClient } from './path/to/prisma/generated/client'
354
+
355
+ // Registra manualmente como fallback
356
+ setPrismaClient(PrismaClient)
357
+ ```
358
+
359
+ ## Query Logging
360
+
361
+ ### Ativar Logs de Queries
362
+
363
+ Configure a variável de ambiente:
364
+
365
+ ```bash
366
+ PRISMA_QUERY_LOG=true
367
+ ```
368
+
369
+ Ou no arquivo `.env`:
370
+
371
+ ```env
372
+ PRISMA_QUERY_LOG=true
373
+ NODE_ENV=development
374
+ DATABASE_URL=postgresql://...
375
+ ```
376
+
377
+ Quando ativado, todas as queries SQL executadas serão logadas no console:
378
+
379
+ ```
380
+ SELECT "public"."person"."id", "public"."person"."name" FROM "public"."person" WHERE "public"."person"."id" = $1
381
+ ```
382
+
383
+ ## Resolução de Problemas
384
+
385
+ ### Erro: "Cannot find module 'prisma/generated/client'"
386
+
387
+ **Causa**: O cliente Prisma não foi gerado.
388
+
389
+ **Solução**:
390
+ ```bash
391
+ # 1. Verifique se tem schema.prisma
392
+ ls prisma/schema.prisma
393
+
394
+ # 2. Gere o cliente
395
+ bunx prisma generate
396
+
397
+ # 3. Confirme que a pasta foi criada
398
+ ls prisma/generated/
399
+ ```
400
+
401
+ ### Erro: "PrismaClient is not a constructor"
402
+
403
+ **Causa**: O módulo Prisma foi importado antes do NestJS inicializar.
404
+
405
+ **Solução**: Certifique-se de que `PrismaService` é injetado via NestJS dependency injection, não importado diretamente:
406
+
407
+ ```typescript
408
+ // ❌ ERRADO
409
+ import { PrismaService } from '@koalarx/nest'
410
+ const prisma = new PrismaService()
411
+
412
+ // ✅ CORRETO
413
+ @Injectable()
414
+ export class MyService {
415
+ constructor(private prisma: PrismaService) {}
416
+ }
417
+ ```
418
+
419
+ ### Erro: "PrismaService não está registrado"
420
+
421
+ **Causa**: O módulo `KoalaNestModule` não foi importado.
422
+
423
+ **Solução**: Importe o módulo principal:
424
+
425
+ ```typescript
426
+ import { KoalaNestModule } from '@koalarx/nest'
427
+ import { Module } from '@nestjs/common'
428
+
429
+ @Module({
430
+ imports: [
431
+ KoalaNestModule.register({
432
+ env: yourEnvSchema,
433
+ // ...
434
+ }),
435
+ ],
436
+ })
437
+ export class AppModule {}
438
+ ```
439
+
440
+ ### Prisma Client não encontra a pasta gerada
441
+
442
+ Se a busca automática não encontrar seu cliente (por exemplo, em monorepos complexos):
443
+
444
+ 1. Verifique o caminho exato onde o cliente foi gerado
445
+ 2. Use `setPrismaClient()` com o caminho explícito
446
+ 3. Ou configure `PRISMA_GENERATED_PATH` se a lib suportar
447
+
448
+ ## Arquitetura Interna
449
+
450
+ ### PrismaService e Proxy Transparente
451
+
452
+ `PrismaService` usa um `Proxy` JavaScript para fornecer acesso transparente:
453
+
454
+ ```typescript
455
+ // Internamente
456
+ private prismaInstance: PrismaClient
457
+
458
+ // O Proxy permite:
459
+ this.prisma.person.findMany() // ✅ Redireciona para prismaInstance
460
+ this.prisma.$transaction(fn) // ✅ Preserva métodos especiais
461
+ this.prisma.withTransaction(fn) // ✅ Métodos adicionados pela lib
462
+ ```
463
+
464
+ ### Busca de Resolução
465
+
466
+ O processo de descoberta (`prisma-resolver.ts`) funciona em etapas:
467
+
468
+ 1. Tenta `process.cwd()` + `prisma/generated/client`
469
+ 2. Tenta diretório do arquivo main
470
+ 3. Tenta caminhos relativos diversos
471
+ 4. Se nada funcionar, checa se foi registrado manualmente via `setPrismaClient()`
472
+
473
+ ## Exemplo Completo
474
+
475
+ Veja a pasta `apps/example/` no repositório para um exemplo completo integrando:
476
+
477
+ - ✅ Prisma com schema definido
478
+ - ✅ Repositórios usando `RepositoryBase`
479
+ - ✅ Serviços injetando `PrismaService`
480
+ - ✅ Transações com `withTransaction`
481
+ - ✅ Query logging ativado
482
+
483
+ ```bash
484
+ cd apps/example
485
+ bunx prisma generate
486
+ bun run start:debug
487
+ ```