vsrepo 1.4.0 → 2.0.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.
Files changed (143) hide show
  1. package/README.md +723 -1338
  2. package/README.pt-BR.md +729 -1341
  3. package/dist/VSRepoAdapter.d.ts +71 -0
  4. package/dist/VSRepoAdapter.js +18 -0
  5. package/dist/VSRepository.d.ts +135 -1201
  6. package/dist/VSRepository.js +273 -237
  7. package/dist/decorators/dynamic-method.decorator.d.ts +25 -0
  8. package/dist/decorators/dynamic-method.decorator.js +38 -0
  9. package/dist/decorators/query-method.decorator.d.ts +27 -0
  10. package/dist/decorators/query-method.decorator.js +45 -0
  11. package/dist/errors/VSRepoAdapterError.d.ts +22 -0
  12. package/dist/errors/VSRepoAdapterError.js +31 -0
  13. package/dist/errors/VSRepoError.d.ts +15 -0
  14. package/dist/errors/VSRepoError.js +21 -0
  15. package/dist/index.d.ts +34 -1
  16. package/dist/index.js +28 -15
  17. package/dist/internal/constants/debug-arg-symbol.constant.d.ts +1 -0
  18. package/dist/internal/constants/debug-arg-symbol.constant.js +4 -0
  19. package/dist/internal/constants/dynamic-methods-key.constant.d.ts +1 -0
  20. package/dist/internal/constants/query-methods-key.constant.d.ts +1 -0
  21. package/dist/internal/constants/query-methods-key.constant.js +4 -0
  22. package/dist/internal/enums/adapter-error-code.enum.d.ts +125 -0
  23. package/dist/internal/enums/adapter-error-code.enum.js +129 -0
  24. package/dist/internal/enums/transaction-isolation-level.enum.d.ts +16 -0
  25. package/dist/internal/enums/transaction-isolation-level.enum.js +20 -0
  26. package/dist/internal/enums/vs-log-level.enum.d.ts +18 -0
  27. package/dist/internal/enums/vs-log-level.enum.js +22 -0
  28. package/dist/internal/enums/vsrepo-error-type.enum.d.ts +19 -0
  29. package/dist/internal/enums/vsrepo-error-type.enum.js +23 -0
  30. package/dist/internal/resolvers/dynamic-methods.resolver.d.ts +23 -0
  31. package/dist/internal/resolvers/dynamic-methods.resolver.js +910 -0
  32. package/dist/internal/resolvers/merge-wheres.resolver.d.ts +7 -0
  33. package/dist/internal/resolvers/merge-wheres.resolver.js +26 -0
  34. package/dist/internal/utils/uncapitalize.util.d.ts +1 -0
  35. package/dist/internal/utils/vs-logger.util.d.ts +25 -0
  36. package/dist/internal/utils/vs-logger.util.js +139 -0
  37. package/dist/internal/validators/decorators.validator.d.ts +8 -0
  38. package/dist/internal/validators/decorators.validator.js +75 -0
  39. package/dist/internal/validators/schemas/ordering.schema.d.ts +3 -0
  40. package/dist/internal/validators/schemas/ordering.schema.js +38 -0
  41. package/dist/internal/validators/schemas/pagination.schema.d.ts +6 -0
  42. package/dist/internal/validators/schemas/pagination.schema.js +40 -0
  43. package/dist/internal/validators/schemas/where.schema.d.ts +7 -0
  44. package/dist/internal/validators/schemas/where.schema.js +42 -0
  45. package/dist/internal/validators/vsrepo.validator.d.ts +33 -0
  46. package/dist/internal/validators/vsrepo.validator.js +160 -0
  47. package/dist/types/adapter/adapter-method-options.type.d.ts +24 -0
  48. package/dist/types/adapter/adapter-query-options.type.d.ts +5 -0
  49. package/dist/types/decorators/dynamic-method-options.type.d.ts +14 -0
  50. package/dist/types/decorators/query-method-options.type.d.ts +15 -0
  51. package/dist/types/dynamic-methods/dynamic-method-customization.type.d.ts +7 -0
  52. package/dist/types/dynamic-methods/dynamic-method-info.type.d.ts +18 -0
  53. package/dist/types/dynamic-methods/dynamic-method-where-ops.type.d.ts +6 -0
  54. package/dist/types/utils/count-result.type.d.ts +9 -0
  55. package/dist/types/utils/deep-partial.type.d.ts +14 -0
  56. package/dist/types/utils/keys-of-type.type.d.ts +20 -0
  57. package/dist/types/utils/methods-options.type.d.ts +23 -0
  58. package/dist/types/utils/ordering.type.d.ts +39 -0
  59. package/dist/types/utils/pagination.type.d.ts +11 -0
  60. package/dist/types/utils/perform-data.type.d.ts +4 -0
  61. package/dist/types/utils/primitive.type.d.ts +6 -0
  62. package/dist/types/utils/query-method-arg.type.d.ts +27 -0
  63. package/dist/types/utils/see-mode.type.d.ts +12 -0
  64. package/dist/types/vsrepo/vsrepo-args.type.d.ts +9 -0
  65. package/dist/types/vsrepo/vsrepo-method.type.d.ts +4 -0
  66. package/dist/types/vsrepo/vsrepo-options.type.d.ts +34 -0
  67. package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
  68. package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -0
  69. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +17 -0
  70. package/dist/types/vsrepo/vsrepo-query-options.type.js +2 -0
  71. package/dist/types/vsrepo/vsrepo-query.type.d.ts +5 -0
  72. package/dist/types/vsrepo/vsrepo-query.type.js +2 -0
  73. package/dist/types/vsrepo/vsrepo-relations.type.d.ts +26 -0
  74. package/dist/types/vsrepo/vsrepo-relations.type.js +2 -0
  75. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.d.ts +19 -0
  76. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.js +2 -0
  77. package/dist/types/vsrepo/vsrepo-select.type.d.ts +15 -0
  78. package/dist/types/vsrepo/vsrepo-select.type.js +2 -0
  79. package/dist/types/vsrepo/vsrepo-transaction-options.type.d.ts +12 -0
  80. package/dist/types/vsrepo/vsrepo-transaction-options.type.js +2 -0
  81. package/dist/types/vsrepo/vsrepo-ugly-where.type.d.ts +9 -0
  82. package/dist/types/vsrepo/vsrepo-ugly-where.type.js +2 -0
  83. package/dist/types/vsrepo/vsrepo-where.type.d.ts +99 -0
  84. package/dist/types/vsrepo/vsrepo-where.type.js +2 -0
  85. package/package.json +16 -37
  86. package/README-DynamicRepo.md +0 -625
  87. package/README-DynamicRepo.pt-BR.md +0 -625
  88. package/dist/DynamicRepository.d.ts +0 -497
  89. package/dist/DynamicRepository.js +0 -26
  90. package/dist/VSRepoError.d.ts +0 -83
  91. package/dist/VSRepoError.js +0 -17
  92. package/dist/internal/decorators/dynamic-method.decorator.js +0 -14
  93. package/dist/internal/decorators/query-method.decorator.js +0 -20
  94. package/dist/internal/entities/dynamic-method-metadata.entity.js +0 -26
  95. package/dist/internal/errors/vs-repo.error.js +0 -31
  96. package/dist/internal/resolvers/base-methods.resolve.js +0 -536
  97. package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +0 -143
  98. package/dist/internal/resolvers/data-payload-with-relations.resolve.js +0 -60
  99. package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +0 -63
  100. package/dist/internal/resolvers/dynamic-method-customization.resolve.js +0 -57
  101. package/dist/internal/resolvers/dynamic-method-info.resolve.js +0 -279
  102. package/dist/internal/resolvers/dynamic-methods-metadata.resolve.js +0 -15
  103. package/dist/internal/resolvers/merge-wheres.resolve.js +0 -22
  104. package/dist/internal/resolvers/pretty-wheres.resolve.js +0 -87
  105. package/dist/internal/resolvers/select.resolve.js +0 -7
  106. package/dist/internal/resolvers/specific-where.resolve.js +0 -84
  107. package/dist/internal/resolvers/ugly-where.resolve.js +0 -178
  108. package/dist/internal/utils/logger.util.js +0 -21
  109. package/dist/internal/utils/schemas.util.js +0 -31
  110. package/dist/internal/validation/build-config.validate.js +0 -84
  111. package/dist/internal/validation/constructor-config.validate.js +0 -64
  112. package/dist/internal/validation/dynamic-method-config.validate.js +0 -19
  113. package/dist/internal/validation/extension.validate.js +0 -15
  114. package/dist/internal/validation/is-object.validate.js +0 -6
  115. package/dist/internal/validation/method-options.validate.js +0 -42
  116. package/dist/internal/validation/obj-with-relations.validate.js +0 -37
  117. package/dist/internal/validation/prisma-client.validate.js +0 -10
  118. package/dist/internal/validation/query-method-arg.validate.js +0 -22
  119. package/dist/internal/validation/query-method-options.validate.js +0 -24
  120. package/scripts/configure-prisma-import.mjs +0 -283
  121. package/scripts/copy-types.mjs +0 -24
  122. /package/dist/{internal/decorators/types/dynamic-method-config.type.js → types/adapter/adapter-method-options.type.js} +0 -0
  123. /package/dist/{internal/errors/types/vs-repo-error-type.type.js → types/adapter/adapter-query-options.type.js} +0 -0
  124. /package/dist/{internal/errors/types/vs-repo-runtime-error-code.type.js → types/decorators/dynamic-method-options.type.js} +0 -0
  125. /package/dist/{internal/validation/types → types/decorators}/query-method-options.type.js +0 -0
  126. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-customization.type.js +0 -0
  127. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-info.type.js +0 -0
  128. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-where-ops.type.js +0 -0
  129. /package/dist/{internal/resolvers/types/base-method-function.type.js → types/utils/count-result.type.js} +0 -0
  130. /package/dist/{internal/resolvers/types/pretty-where.type.js → types/utils/deep-partial.type.js} +0 -0
  131. /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/keys-of-type.type.js} +0 -0
  132. /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/methods-options.type.js} +0 -0
  133. /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/ordering.type.js} +0 -0
  134. /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
  135. /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/perform-data.type.js} +0 -0
  136. /package/dist/{internal/validation/types/base-methods.type.js → types/utils/primitive.type.js} +0 -0
  137. /package/dist/{internal/validation/types → types/utils}/query-method-arg.type.js +0 -0
  138. /package/dist/{internal/validation/types → types/utils}/see-mode.type.js +0 -0
  139. /package/dist/{internal/validation/types/build-config.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
  140. /package/dist/{internal/validation/types/constructor-config.type.js → types/vsrepo/vsrepo-method.type.js} +0 -0
  141. /package/dist/{internal/validation/types/method-options.type.js → types/vsrepo/vsrepo-options.type.js} +0 -0
  142. /package/dist/{internal/validation/types/method.type.js → types/vsrepo/vsrepo-orm-types.type.js} +0 -0
  143. /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-pretty-where.type.js} +0 -0
package/README.pt-BR.md CHANGED
@@ -9,139 +9,124 @@
9
9
  </p>
10
10
  </div>
11
11
 
12
- # VSRepository
12
+ # VSRepository v2
13
13
 
14
14
  🇧🇷 Você está lendo a versão em português. [🇺🇸 Read in English](./README.md)
15
15
 
16
- Biblioteca de repository pattern para projetos que usam **Prisma**, com suporte completo a **TypeScript** e **inferência de tipos** automática.
16
+ > ✅ **Lançado.** O VSRepository v2.0.0 (o core agnóstico de ORM) e o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já foram publicados e estão prontos para uso. O Prisma 7 é o primeiro adapter totalmente suportado; outros ORMs (TypeORM, Drizzle, etc.) ainda estão em desenvolvimento — veja [Status dos adapters](#status-dos-adapters). Se você precisa da versão anterior, somente Prisma, use o código/docs da [`v1`](https://github.com/jaobrabo123/VSRepository/tree/v1).
17
17
 
18
- O VSRepository permite criar repositórios fortemente tipados com:
18
+ Biblioteca de repository pattern **agnóstica de ORM**, com suporte completo a **TypeScript** e **type inference** automático. O VSRepository v2 é uma reescrita da biblioteca [v1](./v1): em vez de falar diretamente com o Prisma, o núcleo agora delega toda operação a um **adapter** plugável, permitindo que a mesma API de repository funcione com Prisma, TypeORM ou qualquer outro ORM/banco que implemente o contrato de adapter.
19
19
 
20
- - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
20
+ O VSRepository permite criar repositories fortemente tipados com:
21
+
22
+ - **Métodos base** automáticos: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
21
23
  - **Soft-delete nativo**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
22
- - **Métodos dinâmicos** inferidos pelo nome: `findOneByEmail`, `findManyPaginated`, `updateById`, `deleteManyByNameStartsWith`
23
- - **Select models** reutilizáveis para diferentes projeções de dados
24
+ - **Métodos dinâmicos** inferidos a partir do nome de um campo `declare` via o decorador `@DynamicMethod`: `findByEmail`, `findManyByStatusPaginated`, `updateById`
25
+ - **Métodos de query SQL raw** através do novo decorador `@QueryMethod`, ignorando totalmente o engine de parsing por nome
26
+ - **`select`/`relations`** ad-hoc em cada chamada — sem mais projeções nomeadas pré-declaradas
24
27
  - **Type safety** em 100% das operações
25
- - **Transações** nativas do Prisma (automáticas em `saveList` e `patchList`)
26
- - **Extensibilidade** com métodos customizados
27
-
28
- > 💡 Quer ver tudo isso na prática? A pasta [`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples) do repositório tem exemplos comentados e executáveis para cada funcionalidade — veja a seção [Exemplos práticos](#exemplos-práticos) mais abaixo.
28
+ - **Transações** nativas do ORM, compartilhadas entre repositories
29
+ - Um **núcleo agnóstico de ORM** — a mesma classe de repository funciona com qualquer implementação de `VSRepoAdapter`
29
30
 
30
31
  ---
31
32
 
32
33
  ## Sumário
33
34
 
35
+ - [O que mudou da v1](#o-que-mudou-da-v1)
36
+ - [Status dos adapters](#status-dos-adapters)
34
37
  - [Instalação](#instalação)
35
- - [Gerando os tipos](#gerando-os-tipos)
36
38
  - [Uso básico](#uso-básico)
37
- - [Abordagem baseada em classes (DynamicRepository)](#abordagem-baseada-em-classes-dynamicrepository)
38
- - [Integração com NestJS](#integração-com-nestjs)
39
+ - [Options do construtor](#options-do-construtor)
39
40
  - [Métodos base](#métodos-base)
40
- - [Soft-delete](#soft-delete)
41
- - [Operações em lote](#operações-em-lote)
42
- - [Merge](#merge)
43
- - [Configurando os métodos base](#configurando-os-métodos-base)
44
- - [Select Models](#select-models)
45
- - [Select bruto (options.select)](#select-bruto-optionsselect)
46
- - [Include Models](#include-models)
47
- - [Include bruto (options.include)](#include-bruto-optionsinclude)
48
- - [Required Where](#required-where)
49
- - [Ordenação padrão (Default Ordering)](#ordenação-padrão-default-ordering)
50
- - [Opção `see`](#opção-see)
41
+ - [Soft-delete](#soft-delete)
42
+ - [`select` e `relations`](#select-e-relations)
51
43
  - [Métodos dinâmicos](#métodos-dinâmicos)
52
- - [Prefixos disponíveis](#prefixos-disponíveis)
53
- - [Filtros de campo](#filtros-de-campo)
54
- - [Operadores lógicos](#operadores-lógicos)
55
- - [Filtros de relação](#filtros-de-relação)
56
- - [Sufixos de paginação e ordenação](#sufixos-de-paginação-e-ordenação)
57
- - [Distinct](#distinct)
58
- - [Configuração de método](#configuração-de-método)
59
- - [Aggregate e GroupBy](#aggregate-e-groupby)
60
- - [Query Methods](#query-methods)
61
- - [Relações no save](#relações-no-save)
44
+ - [Prefixos disponíveis](#prefixos-disponíveis)
45
+ - [Filtros de campo](#filtros-de-campo)
46
+ - [Operadores lógicos](#operadores-lógicos)
47
+ - [Filtros de relação](#filtros-de-relação)
48
+ - [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct)
49
+ - [Options do decorador](#options-do-decorador)
50
+ - [Query methods (SQL raw)](#query-methods-sql-raw)
51
+ - [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query)
62
52
  - [Transações](#transações)
63
- - [Estendendo um repositório](#estendendo-um-repositório)
64
- - [Tratamento de erros](#tratamento-de-erros)
65
53
  - [Tipos utilitários](#tipos-utilitários)
66
- - [Referência da API](#referência-da-api)
67
- - [Exemplos práticos](#exemplos-práticos)
68
- - [Contribuindo](#contribuindo)
54
+ - [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)
55
+ - [Tratamento de erros](#tratamento-de-erros)
56
+ - [`VSRepoAdapterError` e `AdapterErrorCode`](#vsrepoadaptererror-e-adaptererrorcode)
57
+ - [Logging](#logging)
58
+ - [Desenvolvimento](#desenvolvimento)
69
59
  - [Requisitos](#requisitos)
70
- - [Solução de problemas](#solução-de-problemas)
60
+ - [Contribuindo](#contribuindo)
71
61
 
72
62
  ---
73
63
 
74
- ## Instalação
64
+ ## O que mudou da v1
75
65
 
76
- ```bash
77
- npm i vsrepo @prisma/client
78
- ```
66
+ Se você vem do código/docs da [v1](./v1), aqui está o resumo. Veja cada seção linkada para detalhes.
79
67
 
80
- Gere o Prisma Client:
81
-
82
- ```bash
83
- npx prisma generate
84
- ```
68
+ | Área | v1 | v2 |
69
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | Acesso ao banco | Fala diretamente com o **Prisma**, embutido no pacote core | Fala com um **`VSRepoAdapter`**; o suporte a cada ORM é distribuído em pacotes separados (`@vsrepo/prisma7-adapter`, `@vsrepo/typeorm-adapter`, ...) em vez de vir embutido no pacote core `vsrepo` |
71
+ | Definindo um repository | `setupVSRepo<T, M>()({...}).build(prisma)` funcional, **ou** uma classe `DynamicRepository` | Uma única API **baseada em classes**: `extends VSRepository<Entity, PKType, OrmTypes>` |
72
+ | Métodos dinâmicos | Objeto de config `methods: { findByEmail: { map: true } }` | Decorador `@DynamicMethod()` em um campo `declare` |
73
+ | Projeções de dados | `selectModels` + `defaultSelectModel` nomeados e reutilizáveis | `select`/`relations` ad-hoc passados em cada chamada (sem modelos nomeados) |
74
+ | Eager loading | `include`/`includeModels` (específico do Prisma) | Option `relations` agnóstica de ORM |
75
+ | Filtros globais | `requiredWhere` (qualquer filtro arbitrário, sempre aplicado) | **Removido**; Agora aceita apenas `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
76
+ | Sufixo de filtro case-insensitive | `Insensitive` | `IgnoreCase` |
77
+ | Ordenação inline no nome do método | Não suportado (`order` tinha que ser passado como argumento via `Ordered`/`Paginated`) | Cadeias `OrderBy<Campo>Asc`/`OrderBy<Campo>Desc` embutidas diretamente no nome do método |
78
+ | Tratamento de duplicatas no `createMany` | Sufixo `SkipDuplicates` | Sufixo `IgnoreConflicts` |
79
+ | `aggregate` / `groupBy` | Suportado (passthrough nativo do Prisma) | **Ainda não implementado** |
80
+ | Tipos de erro | `VSRepoError` + subclasses (`VSRepoConfigError`, `VSRepoBuildError`, `VSRepoExtendError`, `VSRepoRuntimeError`) | Uma classe base `VSRepoError` com um campo `type: VSRepoErrorType` (`DECORATOR`, `RESOLVER`, `DYNAMIC`, `VALIDATOR`, `BASE`, `ADAPTER`), além de uma subclasse `VSRepoAdapterError` que carrega um `AdapterErrorCode` e o erro original do ORM |
81
+ | Log de debug | Boolean `showWorking: true` | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` para avisos de queries lentas |
82
+ | CLI `vsrepo generate` (etapa de geração de tipos) | Obrigatória antes de usar | Não faz parte do núcleo da v2 — os tipos vêm diretamente das suas entidades/tipos do ORM |
83
+ | Extras de CRUD | `patchList`, `options.select`/`options.include` raw | `select`/`relations` já são o padrão (sempre "raw"); `patch`/`merge` mantêm a mesma semântica. **`patchList` foi removido** — para uma atualização parcial em lote, use um dynamic method `updateManyBy`/`updateManyWhere` |
85
84
 
86
85
  ---
87
86
 
88
- ## Gerando os tipos
89
-
90
- O VSRepository precisa saber o caminho real do seu Prisma Client para gerar as tipagens corretamente.
91
-
92
- ```bash
93
- npx vsrepo generate
94
- ```
87
+ ## Status dos adapters
95
88
 
96
- Equivalente a:
89
+ O VSRepository v2 é **agnóstico de ORM por design**. O pacote core (`vsrepo`) traz apenas a classe de repository, os decoradores, o engine de parsing de nomes, o tratamento de erros e o logging — ele **não** inclui um adapter de produção. O suporte de fato a cada ORM/banco deve viver em **pacotes separados, versionados de forma independente**, um por ORM (e, quando fizer sentido, um por versão principal do ORM), por exemplo:
97
90
 
98
- ```bash
99
- npx vsrepo generate \
100
- --output generated/vsrepo \
101
- --prisma generated/prisma
102
- ```
91
+ - `@vsrepo/prisma7-adapter`
92
+ - `@vsrepo/prisma8-adapter`
93
+ - `@vsrepo/typeorm-adapter`
94
+ - `@vsrepo/drizzle-adapter`
103
95
 
104
- **Flags disponíveis:**
96
+ O adapter do Prisma 7 já foi publicado no npm como `@vsrepo/prisma7-adapter` — por enquanto é o **único** adapter publicado. Os adapters para os outros ORMs listados acima (Prisma 8, TypeORM, Drizzle) estão **planejados**; eles só ainda não foram publicados. Até que exista um pacote `@vsrepo/*-adapter` oficial para o seu ORM, você pode escrever o seu próprio para o seu projeto e, se quiser, publicá-lo e abrir um PR para ajudar a fazer o ecossistema crescer — contribuições nesse sentido são muito bem-vindas.
105
97
 
106
- | Flag | Atalho | Padrão |
107
- | ---------- | ------ | -------------------- |
108
- | `--output` | `-o` | `generated/vsrepo` |
109
- | `--prisma` | `-p` | `generated/prisma` |
98
+ | Adapter | Status |
99
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Lançado** — publicado no npm, implementa todo o contrato de `VSRepoAdapter` (CRUD, relations, transactions, `merge`, logging) com testes; veja o [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) para o código-fonte e docs. |
101
+ | TypeORM (`@vsrepo/typeorm-adapter`) | 🟡 **Planejado, ainda não publicado.** Só foi escrito um parser de referência da cláusula `where` (`parseVSRepoWhere`) para validar o design; é o ponto de partida planejado do futuro pacote `@vsrepo/typeorm-adapter`. Contribuições da comunidade nessa frente são bem-vindas. |
102
+ | Outros ORMs (Prisma 8, Drizzle, etc.) | 🟡 **Planejados, ainda não publicados.** Nenhum pacote oficial existe ainda — por enquanto, escreva o seu próprio adapter (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) e considere publicá-lo/contribuir de volta com o projeto. |
103
+ | Adapters customizados | 🟢 Totalmente suportados hoje — implemente você mesmo a classe abstrata [`VSRepoAdapter`](#escrevendo-seu-próprio-adapter) para qualquer ORM/banco que precisar, no seu próprio projeto ou pacote, seguindo o mesmo formato esperado dos `@vsrepo/*-adapter`. |
110
104
 
111
- **Arquivos gerados:**
105
+ Resumindo: a classe de repository, os decoradores `@DynamicMethod`/`@QueryMethod`, o engine de parsing de nomes, o tratamento de erros e o logging já funcionam de ponta a ponta, e o suporte ao Prisma 7 agora é um adapter lançado e publicado. Adapters oficiais para os demais ORMs estão no roadmap e serão distribuídos como pacotes `@vsrepo/*-adapter` separados, e não como parte do pacote core `vsrepo` — mas você não precisa esperar por isso: escrever (e opcionalmente publicar) o seu próprio adapter enquanto isso é uma forma totalmente suportada de usar a v2 hoje e de contribuir de volta com o projeto.
112
106
 
113
- ```text
114
- generated/vsrepo/
115
- ├── DynamicRepository.ts
116
- ├── DynamicRepository.types.d.ts
117
- ├── VSRepoError.ts
118
- ├── VSRepoError.types.d.ts
119
- ├── VSRepository.ts
120
- ├── VSRepository.types.d.ts
121
- └── index.ts
122
- ```
107
+ ---
123
108
 
124
- Depois de gerar, sempre importe a partir da pasta gerada:
109
+ ## Instalação
125
110
 
126
- ```ts
127
- // CORRETO ✅
128
- import { setupVSRepo } from "../../generated/vsrepo";
111
+ A v2 é instalada como o pacote core mais um pacote de adapter para o seu ORM, por exemplo:
129
112
 
130
- // ERRADO ❌
131
- import { setupVSRepo } from "vsrepo";
113
+ ```bash
114
+ npm i vsrepo @vsrepo/prisma7-adapter
132
115
  ```
133
116
 
117
+ > O `vsrepo` v2.0.0 e o `@vsrepo/prisma7-adapter` já foram publicados no npm e estão prontos para uso. Para qualquer ORM além do Prisma 7, ainda não existe um pacote de adapter — instale o core e escreva o seu próprio (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)).
118
+
134
119
  ---
135
120
 
136
121
  ## Uso básico
137
122
 
138
- ### Configurando o Prisma Client
123
+ ### Implementando/escolhendo um adapter
139
124
 
140
- ```ts
125
+ ```typescript
141
126
  // src/configs/db.ts
142
- import { PrismaClient } from '../../generated/prisma/client';
143
- import { PrismaPg } from '@prisma/adapter-pg';
144
- import 'dotenv/config';
127
+ import { PrismaClient } from "../../generated/prisma/client";
128
+ import { PrismaPg } from "@prisma/adapter-pg";
129
+ import "dotenv/config";
145
130
 
146
131
  const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
147
132
  const prisma = new PrismaClient({ adapter });
@@ -149,1459 +134,862 @@ const prisma = new PrismaClient({ adapter });
149
134
  export default prisma;
150
135
  ```
151
136
 
152
- ### Criando um repositório
137
+ ### Criando um repository
153
138
 
154
- ```ts
155
- // src/repositories/userRepository.ts
139
+ ```typescript
140
+ // src/repositories/user.repository.ts
141
+ import { VSRepository, DynamicMethod } from "vsrepo";
142
+ import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
156
143
  import prisma from "../configs/db";
157
- import { setupVSRepo } from "../../generated/vsrepo";
158
- import type { User } from "../../generated/prisma/client";
159
-
160
- const userRepository = setupVSRepo<User, "User">()(({
161
- tableName: "user",
162
- pkName: "id",
163
- selectModels: {
164
- public: { id: true, name: true, email: true },
165
- },
166
- defaultSelectModel: "public",
167
- }).build(prisma);
168
-
169
- export default userRepository;
170
- ```
171
-
172
- ### Usando o repositório
173
-
174
- ```ts
175
- import userRepository from "./repositories/userRepository";
176
-
177
- const user = await userRepository.save({
178
- name: "John",
179
- email: "john@email.com",
180
- password: "password",
181
- });
182
-
183
- const found = await userRepository.get(user.id);
184
- const all = await userRepository.getAll();
185
-
186
- user.name = "John Smith";
187
-
188
- await userRepository.save(user);
189
- await userRepository.remove(user.id);
190
- ```
191
-
192
- ---
193
-
194
- ## Abordagem baseada em classes (DynamicRepository)
195
-
196
- Se você prefere um estilo OOP com decorators em vez da abordagem funcional `setupVSRepo`, o VSRepository também oferece `DynamicRepository` — uma classe que você pode estender com decorators `@DynamicMethod()` para definir seus métodos dinâmicos.
197
-
198
- Veja **[README-DynamicRepo.pt-BR.md](./README-DynamicRepo.pt-BR.md)** para a documentação completa da abordagem baseada em classes, incluindo exemplos de integração com NestJS, configuração de decorators e uma comparação com o `setupVSRepo`.
199
-
200
- ---
201
-
202
- ## Integração com NestJS
203
-
204
- O VSRepository pode ser facilmente integrado a projetos NestJS através de providers. Abaixo está um exemplo completo usando o padrão de injeção de dependência do NestJS.
205
-
206
- ### Configurando o repositório como provider
144
+ import type { UserGetPayload } from "../../generated/prisma/models";
207
145
 
208
- ```ts
209
- // src/modules/user/user.repository.ts
210
- import { Provider } from "@nestjs/common";
211
- import { PrismaService } from "../../database/prisma.service";
212
- import { UserGetPayload } from "../../../generated/prisma/models";
213
- import { setupVSRepo } from "../../../generated/vsrepo";
146
+ type User = UserGetPayload<{ include: { address: true } }>;
214
147
 
215
- const userVSRepo = setupVSRepo<
216
- UserGetPayload<{ include: { profile: true } }>,
217
- "User"
218
- >()(({
219
- tableName: "user",
220
- pkName: "id",
221
- selectModels: {
222
- public: {
223
- id: true,
224
- email: true,
225
- createdAt: true,
226
- updatedAt: true,
227
- },
228
- auth: {
229
- id: true,
230
- email: true,
231
- password: true,
232
- },
233
- },
234
- defaultSelectModel: "public",
235
- requiredWhere: {
236
- deletedAt: null,
237
- },
238
- relations: {
239
- profile: {
240
- mode: "oto",
241
- pk: "id",
242
- restriction: "add",
243
- },
244
- },
245
- methods: {
246
- findAuthByEmail: {
247
- map: true,
248
- proxyTo: "findUniqueByEmail",
249
- selectModel: "auth",
250
- },
251
- findByEmailEndsWith: {
252
- map: true,
253
- }
254
- },
255
- });
256
-
257
- const setupUserRepository = (prisma: PrismaService) => {
258
- return userVSRepo.build(prisma);
259
- };
260
-
261
- export type UserRepository = ReturnType<typeof setupUserRepository>;
262
- /*
263
- O tipo também pode ser inferido usando o `RepositoryOf` do VSRepository, passando o tipo de `userVSRepo`:
264
-
265
- export type UserRepository = RepositoryOf<typeof userVSRepo>;
266
-
267
- OBS: Se você usar `.extend` para estender o repositório ou configurar os métodos base,
268
- usar `ReturnType` é recomendado por ser mais simples de inferir o tipo
269
- */
270
-
271
- export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
272
-
273
- export const UserRepositoryProvider: Provider = {
274
- provide: USER_REPOSITORY,
275
- inject: [PrismaService],
276
- useFactory: setupUserRepository,
277
- };
278
- ```
279
-
280
- ### Registrando o provider no módulo
281
-
282
- ```ts
283
- // src/modules/user/user.module.ts
284
- import { Module } from "@nestjs/common";
285
- import { UserRepositoryProvider } from "./user.repository";
286
- import { UserService } from "./user.service";
287
- import { UserController } from "./user.controller";
288
-
289
- @Module({
290
- imports: [DatabaseModule],
291
- providers: [UserRepositoryProvider, UserService],
292
- controllers: [UserController],
293
- exports: [UserService],
294
- })
295
- export class UserModule {}
296
- ```
297
-
298
- ### Usando o repositório em um service
299
-
300
- ```ts
301
- // src/modules/user/user.service.ts
302
- import { Injectable, Inject } from "@nestjs/common";
303
- import { USER_REPOSITORY, type UserRepository } from "./user.repository";
304
-
305
- @Injectable()
306
- export class UserService {
307
- constructor(
308
- @Inject(USER_REPOSITORY)
309
- private readonly userRepository: UserRepository,
310
- ) {}
311
-
312
- async getUserById(id: string) {
313
- return this.userRepository.get(id);
314
- }
315
-
316
- async getUserAuthByEmail(email: string) {
317
- return this.userRepository.findAuthByEmail(email);
318
- }
319
-
320
- async createUser(data: { email: string; password: string; name: string }) {
321
- return this.userRepository.save({
322
- email: data.email,
323
- password: data.password,
324
- name: data.name,
148
+ class UserRepository extends VSRepository<User, string> {
149
+ constructor() {
150
+ super({
151
+ pkName: "id",
152
+ adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
153
+ softRemoveKey: "deletedAt",
154
+ defaultOrdering: { createdAt: "desc" },
325
155
  });
326
156
  }
327
- }
328
- ```
329
-
330
- **Benefícios dessa abordagem:**
331
157
 
332
- - ✅ Repositórios type-safe com injeção de dependência
333
- - ✅ Fácil de testar (mock do `USER_REPOSITORY`)
334
- - ✅ Isolamento da lógica de persistência
335
- - ✅ Reuso do repositório em múltiplos services
336
- - ✅ Suporte a transações via `PrismaService`
158
+ @DynamicMethod()
159
+ declare findByEmail: (email: string) => Promise<User[]>;
337
160
 
338
- ---
339
-
340
- ## Métodos base
341
-
342
- Ao chamar `.build(prisma)`, os métodos base abaixo ficam automaticamente disponíveis:
343
-
344
- | Método | Descrição |
345
- | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
346
- | `get(pk)` | Busca um registro pela sua chave primária |
347
- | `getOrThrow(pk)` | Busca um registro pela sua chave primária; lança `VSRepoRuntimeError` (código `"20727"`) se não for encontrado |
348
- | `getList(pks)` | Busca múltiplos registros a partir de uma lista de chaves primárias |
349
- | `save(obj)` | Cria ou atualiza — se o objeto tiver uma `pk`, realiza um `upsert`; caso contrário, um `create` |
350
- | `saveList(objs)` | Salva um array de objetos em uma única transação automática |
351
- | `patch(pk, obj)` | Atualiza parcialmente um registro pela sua chave primária |
352
- | `patchList(tuples)` | Atualiza parcialmente múltiplos registros via um array de tuplas `[pk, obj]` em uma transação automática |
353
- | `merge(pk, obj)` | Busca um registro e faz um deep merge em memória — **não persiste**, retorna o objeto mesclado |
354
- | `remove(pk)` | Remove um registro pela sua chave primária |
355
- | `removeList(pks)` | Remove vários registros a partir de uma lista de chaves primárias — retorna `{ count }` |
356
- | `getAll()` | Retorna todos os registros (aceita `pagination` e `order` em `options`) |
357
- | `total()` | Retorna o número total de registros |
358
- | `has(pk)` | Verifica se um registro existe pela sua chave primária — retorna `boolean` |
359
-
360
- Todos eles aceitam `options` como último argumento.
361
-
362
- ### Soft-delete
363
-
364
- Quando `softRemovekName` está configurado no repositório, os métodos adicionais abaixo ficam disponíveis:
365
-
366
- | Método | Descrição |
367
- | --------------------------- | ---------------------------------------------------------------------------------- |
368
- | `softRemove(pk)` | Marca um registro como removido, preenchendo `softRemovekName` com a data atual |
369
- | `softRemoveList(pks)` | Marca múltiplos registros como removidos em lote — retorna `{ count }` |
370
- | `restore(pk)` | Restaura um registro com soft-delete, limpando o campo `softRemovekName` |
371
- | `restoreList(pks)` | Restaura múltiplos registros com soft-delete em lote — retorna `{ count }` |
372
-
373
- ```ts
374
- const userRepository = setupVSRepo<User, "user">()(({
375
- tableName: "user",
376
- pkName: "id",
377
- softRemovekName: "deletedAt", // deve ser um campo DateTime no schema do Prisma
378
- }).build(prisma);
379
-
380
- await userRepository.softRemove(1);
381
- await userRepository.restore(1);
382
- ```
383
-
384
- > O campo informado em `softRemovekName` **deve** ser do tipo `DateTime` no schema do Prisma. O VSRepository valida isso no momento do `build` e lança `VSRepoBuildError` se o tipo estiver incorreto.
385
-
386
- ### Operações em lote
387
-
388
- `saveList` e `patchList` executam automaticamente todas as operações dentro de uma única transação do Prisma. Se alguma operação falhar, todas as anteriores são desfeitas (rollback).
389
-
390
- ```ts
391
- // saveList — cria ou atualiza múltiplos objetos em uma transação automática
392
- const users = await userRepository.saveList([
393
- { name: "Mary", email: "mary@email.com" },
394
- { id: 2, name: "John Updated", email: "john@email.com" },
395
- ]);
161
+ @DynamicMethod()
162
+ declare findOneByEmail: (email: string) => Promise<User | null>;
163
+ }
396
164
 
397
- // patchList — atualiza parcialmente múltiplos registros via tuplas [pk, obj]
398
- const updated = await userRepository.patchList([
399
- [1, { active: false }],
400
- [2, { name: "New Name" }],
401
- ]);
165
+ export default new UserRepository();
402
166
  ```
403
167
 
404
- Quando você já está dentro de uma transação existente, passe-a em `options.db`. Nesse caso, `db` deve ser um `DbTransaction` (não o client principal):
405
-
406
- ```ts
407
- await prisma.$transaction(async (tx) => {
408
- await userRepository.saveList([{ name: "Mary" }, { name: "Gus" }], { db: tx });
409
- await userRepository.patchList([[1, { active: false }], [2, { active: true }]], { db: tx });
410
- });
411
- ```
168
+ > A API do core (`VSRepository`, `VSRepoAdapter`, `DynamicMethod`, `QueryMethod`, `VSRepoError`, enums e tipos) é importada do entry point único `vsrepo`. O adapter concreto vem de um pacote **separado** (`@vsrepo/*-adapter`). No Prisma 7, instale o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já publicado (o construtor dele recebe um objeto de config — `tableName`, `pkName`, `relations`/`logLevel` opcionais — como no exemplo acima). Adapters oficiais para outros ORMs estão planejados, mas ainda não publicados; até lá, você pode implementar o contrato `VSRepoAdapter` você mesmo (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) — e publicá-lo para ajudar o projeto é muito bem-vindo.
412
169
 
413
- ### Merge
170
+ > **O terceiro generic (`OrmTypes`):** `VSRepository<Entity, PKType, OrmTypes>` aceita um terceiro type parameter opcional descrevendo os tipos de client/transaction do seu ORM, via `VSRepoOrmTypes` (`{ dbClient; dbTransaction }`). Ao fornecê-lo, `getDbClient()`, o callback de `transaction()` e a option `db` de todo método passam a ser tipados corretamente, em vez de `any`:
171
+ > ```typescript
172
+ > type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
173
+ >
174
+ > class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
175
+ > // getDbClient() agora retorna PrismaClient, e transaction(fn) tipa `tx` como Prisma.TransactionClient
176
+ > }
177
+ > ```
178
+ > Se omitido, o padrão é `VSRepoOrmTypes` (`dbClient`/`dbTransaction` como `any`).
414
179
 
415
- O método `merge` busca um registro pela PK e faz um deep merge (`deepmerge`) do objeto informado com os dados existentes **em memória**. Ele **não persiste** as alterações — retorna o resultado mesclado para que você decida o que fazer com ele.
180
+ ### Usando o repository
416
181
 
417
- ```ts
418
- const existing = await userRepository.get(1);
419
- // existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
182
+ ```typescript
183
+ import userRepository from "./repositories/user.repository";
420
184
 
421
- const merged = await userRepository.merge(1, {
422
- profile: { bio: "Updated bio" },
185
+ const usuario = await userRepository.save({
186
+ name: "Joao",
187
+ email: "joao@email.com",
188
+ password: "password",
423
189
  });
424
- // merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
425
190
 
426
- // Para persistir, passe para save ou patch:
427
- await userRepository.save(merged);
428
- ```
191
+ const encontrado = await userRepository.get(usuario.id);
192
+ const todos = await userRepository.getAll();
193
+ const porEmail = await userRepository.findByEmail("joao@email.com");
429
194
 
430
- Retorna `null` se o registro não for encontrado.
431
-
432
- **O merge de relações to-many (`otm`/`mtm`) é feito por PK, não por simples concatenação.** Para relações to-one (`oto`/`mto`), o `merge` faz um deep merge comum do objeto. Para relações to-many, cada item do array enviado é comparado com o item existente que tem a mesma PK (definida em `relations[key].pk`): se a PK bater, os dois objetos são mesclados; se não bater (um item novo, sem correspondente), ele é simplesmente adicionado à lista. Itens existentes que não aparecem no array enviado são mantidos.
433
-
434
- ```ts
435
- const existing = await userRepository.get(1);
436
- // existing: {
437
- // id: 1,
438
- // posts: [
439
- // { id: 10, title: "Post A", published: false },
440
- // { id: 11, title: "Post B", published: true },
441
- // ],
442
- // }
443
-
444
- const merged = await userRepository.merge(1, {
445
- posts: [
446
- { id: 10, published: true }, // mesma PK (id: 10) → mescla com o item existente
447
- { title: "Post C" }, // sem PK → adicionado como novo item
448
- ],
449
- });
450
- // merged: {
451
- // id: 1,
452
- // posts: [
453
- // { id: 10, title: "Post A", published: true }, // mesclado
454
- // { id: 11, title: "Post B", published: true }, // mantido, não estava no array enviado
455
- // { title: "Post C" }, // adicionado
456
- // ],
457
- // }
195
+ await userRepository.patch(usuario.id, { name: "Joao Pedro" });
196
+ await userRepository.remove(usuario.id);
458
197
  ```
459
198
 
460
- > Observe que o `merge` nunca remove itens de uma relação to-many — ele apenas mescla os que batem por PK e adiciona os que não batem. Para remover itens de uma relação, use `save`/`patch` com `restriction: "set"` na configuração da relação.
461
-
462
- ### Configurando os métodos base
463
-
464
- O segundo argumento de `.build(prisma, config)` permite ajustar o comportamento global do repositório e customizar cada método base individualmente através de `baseMethods`.
465
-
466
- ```ts
467
- userVSRepo.build(prisma, {
468
- // Exibe os logs internos do VSRepository no console (queries montadas, prefixo
469
- // detectado, filtros aplicados, etc). Ótimo para debugar métodos dinâmicos. Padrão = false.
470
- showWorking: true,
471
-
472
- baseMethods: {
473
- get: {
474
- // Habilita/desabilita o método no repositório final. Se `false`, o método
475
- // nem aparece no tipo do repositório (não é apenas um erro em runtime). Padrão = true.
476
- active: true,
477
-
478
- // Select model aplicado por padrão quando o método é chamado sem `options.selectModel`.
479
- // Sobrescreve o `defaultSelectModel` de setupVSRepo apenas para este método.
480
- defaultSelect: "public",
481
- },
482
- remove: {
483
- active: true,
484
- defaultSelect: "minimal",
485
-
486
- // Quando `true`, ignora o `requiredWhere` configurado em setupVSRepo para
487
- // este método específico — útil quando um método precisa "furar" um
488
- // filtro global (ex: multi-tenancy) em um caso específico. Padrão = false.
489
- ignoreRequiredWhere: false,
490
- },
491
- save: {
492
- // Aqui apenas `ignoreRequiredWhere` é definido — `active` e `defaultSelect`
493
- // mantêm seus padrões (true e o `defaultSelectModel` global).
494
- ignoreRequiredWhere: true,
495
- },
496
- patch: {
497
- // Apenas o select é sobrescrito; o método continua ativo normalmente.
498
- defaultSelect: "minimal",
499
- },
500
- has: {
501
- active: false, // Desabilita 'has' (padrão = true) — o método desaparece do repositório
502
- },
503
- softRemove: {
504
- // Métodos de soft-delete seguem as mesmas opções (`active`, `defaultSelect`,
505
- // `ignoreRequiredWhere`). Só estão disponíveis se `softRemovekName` estiver configurado.
506
- active: true,
507
- defaultSelect: "minimal",
508
- },
509
- },
510
- });
511
- ```
512
-
513
- > Métodos de lote/agregação como `removeList`, `softRemoveList`, `restoreList`, `total` e `has` **não** aceitam `defaultSelect` (eles não retornam um registro selecionável — retornam `{ count }` ou `boolean`). Nesses casos, `BaseMethodConfig` fica restrito a `active` e `ignoreRequiredWhere`.
514
-
515
199
  ---
516
200
 
517
- ## Select Models
518
-
519
- `selectModels` define projeções de dados nomeadas e reutilizáveis.
520
-
521
- ```ts
522
- selectModels: {
523
- public: { id: true, name: true, email: true },
524
- internal: { id: true, name: true, email: true, password: true },
525
- minimal: { id: true },
526
- },
527
- defaultSelectModel: "public",
528
- ```
529
-
530
- `defaultSelectModel` define qual select é usado automaticamente quando nenhum é especificado na chamada. É recomendado sempre defini-lo junto com `selectModels`.
531
-
532
- **Usando um select específico na chamada:**
533
-
534
- ```ts
535
- const user = await userRepository.get(id, { selectModel: "minimal" });
536
- ```
537
-
538
- **Retornando o payload padrão do Prisma (sem select):**
201
+ ## Options do construtor
539
202
 
540
- ```ts
541
- const fullUser = await userRepository.get(id, { selectModel: false });
542
- ```
543
-
544
- ### Select bruto (`options.select`)
545
-
546
- Além do `selectModel` (nomeado, pré-configurado em `selectModels`), você pode passar um `select` bruto do Prisma diretamente na chamada, sem precisar registrá-lo antecipadamente no repositório.
547
-
548
- ```ts
549
- const usuario = await usuarioRepository.get(id, {
550
- select: { id: true, nome: true },
551
- });
552
- ```
203
+ `VSRepoOptions<T, K>`, passado para o `super(...)` dentro do construtor do seu repository:
553
204
 
554
- `options.select` aceita qualquer `select` válido para o modelo Prisma do repositório — é totalmente tipado e oferece o mesmo autocomplete/validação de chamar `prisma.usuario.findMany({ select: ... })` diretamente, e o tipo de retorno do método é restrito exatamente aos campos selecionados.
555
-
556
- **Regras e comportamento:**
557
-
558
- - **Mutuamente exclusivo com `selectModel`, `includeModel` e `include`.** Apenas um dos quatro pode ser fornecido por chamada; os tipos garantem isso — passar mais de um é um erro em tempo de compilação.
559
- - **Ad hoc, não reutilizável.** Diferente do `selectModel`, não precisa ser declarado em `selectModels`. Use-o para projeções pontuais que não justificam um select model nomeado.
560
- - **Nenhum `defaultSelectModel` é aplicado.** Quando `select` é fornecido, o select padrão (`defaultSelectModel`) é ignorado e apenas o `select` bruto é enviado ao Prisma.
561
-
562
- ```ts
563
- // CORRETO ✅ — apenas select bruto
564
- await usuarioRepository.get(id, { select: { id: true, nome: true } });
565
-
566
- // ERRADO ❌ — combinar select com selectModel/includeModel/include não é permitido
567
- await usuarioRepository.get(id, { selectModel: "public", select: { id: true } });
568
- await usuarioRepository.get(id, { include: { posts: true }, select: { id: true } });
569
- ```
570
-
571
- > **Quando usar `selectModel` vs. `select`:** prefira `selectModel` para projeções reutilizadas em várias chamadas (definidas uma vez em `selectModels`); use `select` para projeções específicas e ocasionais que não precisam de um nome.
205
+ | Option | Tipo | Descrição |
206
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
207
+ | `adapter` | `VSRepoAdapter<T>` | **Obrigatório.** A instância do adapter que traduz as chamadas do repository em chamadas contra o ORM/banco por trás dele. |
208
+ | `pkName` | `keyof T` | **Obrigatório.** Nome do campo que representa a primary key da entidade. |
209
+ | `softRemoveKey` | `keyof T` | Opcional. Quando definido, habilita `softRemove`, `softRemoveList`, `restore` e `restoreList`. |
210
+ | `defaultOrdering` | `Ordering<T>` | Opcional. Ordenação padrão aplicada automaticamente em queries que aceitam `order`, a menos que seja sobrescrita em uma chamada específica. |
211
+ | `logLevel` | `VSLogLevel` | Opcional. Severidade mínima impressa pelo logger interno. Padrão: `VSLogLevel.WARN`. |
212
+ | `logSlowThresholdMs` | `number` | Opcional. Duração (ms) acima da qual uma operação concluída é logada como `WARN` em vez de `DEBUG`. Padrão: 300ms. |
572
213
 
573
214
  ---
574
215
 
575
- ## Include Models
576
-
577
- `includeModels` funciona de forma parecida com `selectModels`, mas em vez de receber um `select`, recebe um `include` válido do Prisma.
578
-
579
- ```ts
580
- const userRepository = setupVSRepo<User, "user">()(({
581
- tableName: "user",
582
- pkName: "id",
583
- selectModels: {
584
- public: { id: true, name: true, email: true },
585
- },
586
- defaultSelectModel: "public",
587
- includeModels: {
588
- withPosts: { posts: true },
589
- withPostsAndProfile: { posts: true, profile: true },
590
- },
591
- }).build(prisma);
592
- ```
593
-
594
- **Usando um `includeModel` na chamada:**
595
-
596
- ```ts
597
- const user = await userRepository.get(id, { includeModel: "withPosts" });
598
- ```
599
-
600
- Nesse caso, o `select` padrão (`selectModels`/`defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
601
-
602
- ### Diferenças em relação a `selectModels`
216
+ ## Métodos base
603
217
 
604
- - **Só pode ser passado na chamada do método**, via `options.includeModel`. Não existe `defaultIncludeModel` ou `defaultInclude` — não há como configurar um `includeModel` padrão no repositório, ao contrário do que acontece com `defaultSelectModel`.
605
- - **`includeModel` e `selectModel` não podem ser passados juntos** na mesma chamada. Se um `includeModel` for informado, qualquer `selectModel` (incluindo o padrão) é ignorado.
218
+ Disponíveis automaticamente em toda subclasse de `VSRepository`:
606
219
 
607
- ```ts
608
- // CORRETO ✅ — apenas includeModel
609
- await userRepository.get(id, { includeModel: "withPosts" });
220
+ | Método | Descrição |
221
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `get(pk, options?)` | Busca um registro pela primary key. |
223
+ | `getOrThrow(pk, options?)` | Busca um registro pela primary key, lançando erro se não encontrar. |
224
+ | `getList(pks, options?)` | Busca vários registros por uma lista de primary keys. |
225
+ | `getAll(options?)` | Busca todos os registros; aceita `pagination` e `order` em `options`. |
226
+ | `save(obj, options?)` | Cria ou atualiza (upsert) um único registro. |
227
+ | `saveList(objs, options?)` | Cria ou atualiza (upsert) vários registros em uma única chamada. |
228
+ | `patch(pk, obj, options?)` | Atualiza parcialmente um registro pela primary key. |
229
+ | `merge(pk, obj, options?)` | Busca um registro e o retorna mesclado (deep-merge), em memória, com o objeto informado — **não** persiste nada. |
230
+ | `remove(pk, options?)` | Remove um registro pela primary key. |
231
+ | `removeList(pks, options?)` | Remove vários registros pela primary key, retornando `{ count }`. |
232
+ | `total(options?)` | Retorna o total de registros. |
233
+ | `has(pk, options?)` | Verifica se um registro existe, retornando `boolean`. |
234
+ | `transaction(fn, options?)` | Executa `fn` dentro de uma transação nativa do ORM. |
235
+ | `getDbClient()` | Retorna a instância do client do ORM usada fora de transações. |
236
+ | `query<T>(query, options?)` | Executa uma instrução SQL raw diretamente contra o banco. Veja [Queries raw pontuais com `query()`](#queries-raw-pontuais-com-query). |
610
237
 
611
- // CORRETO ✅ — apenas selectModel
612
- await userRepository.get(id, { selectModel: "public" });
238
+ Todos os métodos acima (exceto `transaction`, `query` e `getDbClient`, que recebem options proprias ou nenhuma) aceitam um objeto `MethodOptions<Entity, OrmTypes>` como último argumento (`select`, `relations`, `see`, `db`).
613
239
 
614
- // ERRADO ❌ — combinar os dois não é permitido
615
- await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
616
- ```
240
+ ---
617
241
 
618
- ### Include bruto (`options.include`)
242
+ ## Soft-delete
619
243
 
620
- Além do `includeModel` (nomeado, pré-configurado em `includeModels`), você pode passar um `include` bruto do Prisma diretamente na chamada, sem precisar registrá-lo antes no repositório.
244
+ O soft-delete agora é um **conceito nativo de primeira classe**. Configure `softRemoveKey` uma vez no repository:
621
245
 
622
- ```ts
623
- const user = await userRepository.get(id, {
624
- include: { posts: true, profile: true },
246
+ ```typescript
247
+ super({
248
+ pkName: "id",
249
+ adapter,
250
+ softRemoveKey: "deletedAt",
625
251
  });
626
252
  ```
627
253
 
628
- `options.include` aceita qualquer `include` válido para o modelo Prisma do repositório — é totalmente tipado e oferece o mesmo autocomplete/validação de chamar `prisma.user.findMany({ include: ... })` diretamente.
254
+ Isso libera quatro métodos extras:
629
255
 
630
- **Regras e comportamento:**
256
+ | Método | Efeito |
257
+ | ------------------------------- | --------------------------------------- |
258
+ | `softRemove(pk, options?)` | Define `deletedAt` para a data atual. |
259
+ | `softRemoveList(pks, options?)` | O mesmo, em lote — retorna `{ count }`. |
260
+ | `restore(pk, options?)` | Volta `deletedAt` para `null`. |
261
+ | `restoreList(pks, options?)` | O mesmo, em lote — retorna `{ count }`. |
631
262
 
632
- - **Mutuamente exclusivo com `selectModel` e `includeModel`.** Apenas um dos três pode ser informado por chamada; os tipos garantem isso — passar mais de um é um erro em tempo de compilação.
633
- - **Ad hoc, não reutilizável.** Diferente do `includeModel`, não precisa ser declarado em `includeModels`. Use para includes pontuais que não justificam um model nomeado.
634
- - **Nenhum `selectModel` padrão é aplicado.** Assim como com `includeModel`, quando `include` é informado, o select (incluindo `defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
263
+ Todo o restante dos métodos aceita uma option `see` que controla a visibilidade de registros com soft-delete:
635
264
 
636
- ```ts
637
- // CORRETO ✅ — apenas include bruto
638
- await userRepository.get(id, { include: { posts: true } });
639
-
640
- // ERRADO ❌ — combinar include com selectModel/includeModel não é permitido
641
- await userRepository.get(id, { selectModel: "public", include: { posts: true } });
642
- await userRepository.get(id, { includeModel: "withPosts", include: { posts: true } });
265
+ ```typescript
266
+ await userRepository.getAll({ see: "active" }); // padrão — apenas registros não removidos
267
+ await userRepository.getAll({ see: "removed" }); // apenas registros com soft-delete
268
+ await userRepository.getAll({ see: "all" }); // todos, ignorando o soft-delete
643
269
  ```
644
270
 
645
- > **Quando usar `includeModel` vs. `include`:** prefira `includeModel` para includes reutilizados em várias chamadas (definidos uma vez em `includeModels`); use `include` para includes específicos e pontuais que não precisam de um nome.
646
-
647
271
  ---
648
272
 
649
- ## Required Where
650
-
651
- `requiredWhere` define filtros que são aplicados automaticamente em toda query do repositório.
652
-
653
- ```ts
654
- requiredWhere: { active: true },
655
- ```
273
+ ## `select` e `relations`
656
274
 
657
- Agora toda query vai incluir automaticamente `active: true`:
658
-
659
- ```ts
660
- // Internamente: WHERE active = true
661
- const users = await userRepository.findMany();
662
-
663
- // Internamente: WHERE email = 'john@email.com' AND active = true
664
- const user = await userRepository.findByEmail("john@email.com");
665
- ```
666
-
667
- Útil para soft-deletes manuais, multi-tenancy e filtros globais de qualquer tipo.
668
-
669
- ---
670
-
671
- ## Ordenação padrão (Default Ordering)
672
-
673
- `defaultOrdering` define uma ordenação padrão que é aplicada automaticamente em toda query que aceita `orderBy`, sem precisar repetir o argumento `order` em cada chamada.
674
-
675
- ```ts
676
- const userRepository = setupVSRepo<User, "user">()(({
677
- tableName: "user",
678
- pkName: "id",
679
- defaultOrdering: { createdAt: "desc" },
680
- }).build(prisma);
681
- ```
275
+ Os `selectModels`/`defaultSelectModel` nomeados e reutilizáveis da v1 não existem mais. Na v2 você passa `select` e `relations` diretamente em cada chamada — não há nada para pré-registrar:
682
276
 
683
- Com isso, toda query de listagem já virá ordenada por `createdAt` decrescente:
684
-
685
- ```ts
686
- // Internamente: ORDER BY createdAt DESC
687
- const users = await userRepository.getAll();
688
-
689
- // Também se aplica ao getAll com paginação
690
- const paginated = await userRepository.getAll({ pagination: { take: 10 } });
691
- ```
692
-
693
- **`defaultOrdering` é ignorado quando:**
694
-
695
- - O método usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered` — nesses casos, o argumento `order` passado na chamada tem prioridade.
696
- - O método dinâmico tem `injectOrdering` configurado — a ordenação fixa do método tem precedência.
277
+ ```typescript
278
+ const usuario = await userRepository.get(id, {
279
+ select: { id: true, name: true, address: { city: true } },
280
+ });
697
281
 
698
- ```ts
699
- methods: {
700
- findManyPaginatedAndOrdered: { map: true }, // order vem do argumento → defaultOrdering ignorado
701
- findManyByActive: { map: true }, // sem Ordered → defaultOrdering aplicado
702
- findManyByStatus: {
703
- map: true,
704
- injectOrdering: { name: "asc" }, // injectOrdering → defaultOrdering ignorado
705
- },
706
- }
282
+ const usuarioComEndereco = await userRepository.get(id, {
283
+ relations: { address: true },
284
+ });
707
285
  ```
708
286
 
709
- > `defaultOrdering` aceita o mesmo tipo do `orderBy` nativo do Prisma para o modelo — incluindo arrays de ordenações encadeadas.
710
-
711
- ---
712
-
713
- ## Opção `see`
714
-
715
- Quando `softRemovekName` está configurado, todo método aceita a opção `see` para controlar a visibilidade de registros com soft-delete:
716
-
717
- | Valor | Comportamento |
718
- | ----------- | -------------------------------------------------------------- |
719
- | `"active"` | Retorna apenas registros que **não** foram removidos (padrão) |
720
- | `"removed"` | Retorna apenas registros removidos |
721
- | `"all"` | Retorna todos os registros, independente do status |
722
-
723
- ```ts
724
- // Retorna apenas usuários ativos (padrão)
725
- const active = await userRepository.getAll();
726
-
727
- // Retorna apenas usuários removidos
728
- const removed = await userRepository.getAll({ see: "removed" });
287
+ - `select` espelha o formato da entidade: campos escalares recebem um `boolean`; campos de relação recebem um `boolean` ou um `select` aninhado.
288
+ - `relations` carrega registros relacionados; cada campo de relação recebe um `boolean` ou um objeto `relations` aninhado.
289
+ - Se `select` e `relations` podem ser combinados depende do adapter (veja abaixo).
729
290
 
730
- // Retorna todos
731
- const all = await userRepository.getAll({ see: "all" });
732
- ```
733
-
734
- > A opção `see` funciona independentemente do `requiredWhere` — ela é aplicada em cima do filtro de soft-delete, não como substituta dele.
291
+ > ⚠️ **Comportamento de `relations` depende do adapter:**
292
+ >
293
+ > O core apenas repassa `MethodOptions.select` e `MethodOptions.relations` ao adapter — cada adapter decide como traduzi-los para o ORM subjacente:
294
+ >
295
+ > - **TypeORM (`@vsrepo/typeorm-adapter`)** — `relations` é **obrigatório** para carregar qualquer relação, mesmo quando você quer apenas uma projeção aninhada via `select`. O TypeORM não fará JOIN/carregar a relação a menos que ela esteja listada em `relations`:
296
+ > ```typescript
297
+ > // TypeORM: apenas select NÃO é suficiente
298
+ > await userRepository.get(id, {
299
+ > select: { id: true, address: { city: true } },
300
+ > relations: { address: true }, // ← obrigatório no TypeORM
301
+ > });
302
+ > ```
303
+ > - **Prisma 7 (`@vsrepo/prisma7-adapter` / `VSRepoPrisma7Adapter`)** — `relations` é convertido para `include` do Prisma (`parsePrismaInclude`). **Se `select` estiver presente, `relations` é ignorado** porque o Prisma não permite `select` + `include` na mesma query:
304
+ > ```typescript
305
+ > // Prisma7: relations é ignorado quando select existe
306
+ > await userRepository.get(id, {
307
+ > select: { id: true, name: true },
308
+ > relations: { address: true }, // ← ignorado, include = undefined
309
+ > });
310
+ > ```
311
+ >
312
+ > Adapters customizados podem mapear `relations` de forma diferente — consulte a documentação do adapter para a semântica exata.
735
313
 
736
314
  ---
737
315
 
738
316
  ## Métodos dinâmicos
739
317
 
740
- Métodos dinâmicos são definidos na propriedade `methods` e têm seu comportamento inferido pelo nome.
741
-
742
- ```ts
743
- methods: {
744
- findOneByEmail: { map: true },
745
- findManyPaginated: { map: true },
746
- updateById: { map: true },
747
- deleteManyByIdIn: { map: true },
318
+ Métodos dinâmicos são declarados como um campo `declare` anotado com `@DynamicMethod()`. O comportamento deles — qual método do adapter chamar, quais filtros aplicar e como os argumentos se mapeiam para eles — é inferido inteiramente a partir do **nome** do campo, seguindo a mesma filosofia de convenção sobre configuração da v1.
319
+
320
+ ```typescript
321
+ class UserRepository extends VSRepository<User, string> {
322
+ @DynamicMethod()
323
+ declare findByEmail: (email: string) => Promise<User[]>;
324
+
325
+ @DynamicMethod()
326
+ declare findOneByEmail: (email: string) => Promise<User | null>;
327
+
328
+ @DynamicMethod()
329
+ declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
330
+
331
+ // Baseado em where: VSRepoWhere<T> como primeiro parâmetro, pagination penúltimo, MethodOptions por último
332
+ @DynamicMethod()
333
+ declare findWherePaginated: (
334
+ where: VSRepoWhere<User>,
335
+ pagination: Pagination,
336
+ options?: MethodOptions<User>,
337
+ ) => Promise<User[]>;
338
+
339
+ // OrderedAndPaginated: filtros de campo, depois order, depois pagination, depois MethodOptions
340
+ @DynamicMethod()
341
+ declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
342
+ name: string,
343
+ age: [number, number],
344
+ order: Ordering<User>,
345
+ pagination: Pagination,
346
+ options?: MethodOptions<User>,
347
+ ) => Promise<User[]>;
748
348
  }
749
349
  ```
750
350
 
751
- ---
752
-
753
351
  ### Prefixos disponíveis
754
352
 
755
- O prefixo do nome do método determina qual operação do Prisma será chamada e quais argumentos são esperados.
756
-
757
- | Prefixo | Operação do Prisma | Retorno | Observações |
758
- | ---------------------------- | --------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
759
- | `findOneBy` | `findFirst` | `T \| null` | Retorno único. |
760
- | `findBy` | `findMany` / `findFirst` | `T[]` ou `T \| null` | Padrão é lista; use `fbMode: "one"` para retorno único (**deprecated**, use `findOneBy`) |
761
- | `findUniqueBy` | `findUnique` | `T \| null` | |
762
- | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Lança um erro se não encontrado |
763
- | `findFirstBy` | `findFirst` | `T \| null` | Aceita campos como filtro |
764
- | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Aceita campos como filtro; lança um erro se não encontrado |
765
- | `findFirst` | `findFirst` | `T \| null` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
766
- | `findFirstOrThrow` | `findFirstOrThrow` | `T` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere`; lança um erro se não encontrado |
767
- | `findManyBy` | `findMany` | `T[]` | Aceita campos como filtro |
768
- | `findMany` | `findMany` | `T[]` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
769
- | `findOneWhere` | `findFirst` | `T \| null` | Recebe um objeto `where` explícito como argumento |
770
- | `findListWhere` | `findMany` | `T[]` | Recebe um objeto `where` explícito como argumento |
771
- | `existsBy` | `findFirst` | `boolean` | Retorna `true` se encontrado, `false` caso contrário |
772
- | `existsWhere` | `findFirst` | `boolean` | Recebe um objeto `where` explícito e retorna se existe |
773
- | `countBy` | `count` | `number` | Aceita campos como filtro |
774
- | `countWhere` | `count` | `number` | Recebe um objeto `where` explícito como argumento |
775
- | `count` | `count` | `number` | Sem filtros de campo; aplica apenas `requiredWhere` e `pushWhere` |
776
- | `create` | `create` | `T` | Recebe `data` como argumento |
777
- | `createMany` | `createMany` | `{ count: number }` | Recebe `data` como argumento; suporta `SkipDuplicates` |
778
- | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Recebe `data` como argumento; suporta `SkipDuplicates` |
779
- | `updateBy` | `update` | `T` | Recebe `data` como argumento |
780
- | `updateManyBy` | `updateMany` | `{ count: number }` | Recebe `data` como argumento |
781
- | `updateManyWhere` | `updateMany` | `{ count: number }` | Recebe um objeto `where` e um objeto `data` como argumentos |
782
- | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Recebe `data` como argumento |
783
- | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Recebe um objeto `where` e um objeto `data` como argumentos |
784
- | `upsertBy` | `upsert` | `T` | Recebe `update` e `create` como argumentos |
785
- | `deleteBy` | `delete` | `T` | |
786
- | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
787
- | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Recebe um objeto `where` explícito como argumento |
788
- | `aggregate` | `aggregate` | `Dynamic` | O nome deve ser exato; recebe argumentos nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
789
- | `groupBy` | `groupBy` | `Dynamic[]` | O nome deve ser exato; recebe argumentos nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
790
-
791
- ---
353
+ | Prefixo | Método do adapter | Observações |
354
+ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
355
+ | `findBy` | `findMany` | Filtros de campo seguem o prefixo. |
356
+ | `findOneBy` | `findOne` | Filtros de campo seguem o prefixo; resultado único. |
357
+ | `findOneOrThrowBy` | `findOneOrThrow` | Lança erro se não encontrar. |
358
+ | `findOneOrThrow` | `findOneOrThrow` | Sem filtros de campo; aplica só soft-delete/`see`. |
359
+ | `findOneOrThrowWhere` | `findOneOrThrow` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
360
+ | `findWhere` | `findMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
361
+ | `findOneWhere` | `findOne` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
362
+ | `findOne` | `findOne` | Sem filtros de campo; aplica só soft-delete/`see`. |
363
+ | `countBy` | `count` | Filtros de campo seguem o prefixo. |
364
+ | `countWhere` | `count` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
365
+ | `count` | `count` | Sem filtros de campo. |
366
+ | `existsBy` | `exists` | Retorna `boolean`. |
367
+ | `existsWhere` | `exists` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
368
+ | `create` | `create` | Recebe `data` como argumento. |
369
+ | `createMany` | `createMany` | Recebe `data[]` como argumento; suporta `IgnoreConflicts`. |
370
+ | `createManyReturning` | `createManyReturning` | Recebe `data[]` como argumento; suporta `IgnoreConflicts`; retorna os registros criados (`T[]`), em vez de `CountResult`. |
371
+ | `updateBy` | `update` | Filtros de campo + `data` como argumento. |
372
+ | `updateWhere` | `update` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`. |
373
+ | `updateManyBy` | `updateMany` | Filtros de campo + `data`. |
374
+ | `updateManyWhere` | `updateMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`. |
375
+ | `updateManyReturningBy` | `updateManyReturning` | Filtros de campo + `data`; retorna os registros atualizados. |
376
+ | `updateManyReturningWhere` | `updateManyReturning` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois `data`; retorna os registros atualizados. |
377
+ | `upsertBy` | `upsert` | Filtros de campo + payloads `create`/`update`. |
378
+ | `upsertWhere` | `upsert` | Recebe um `VSRepoWhere<T>` como primeiro argumento, depois os payloads `create`/`update`. |
379
+ | `deleteBy` | `delete` | Filtros de campo seguem o prefixo. |
380
+ | `deleteWhere` | `delete` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
381
+ | `deleteManyBy` | `deleteMany` | Filtros de campo seguem o prefixo. |
382
+ | `deleteManyWhere` | `deleteMany` | Recebe um `VSRepoWhere<T>` como primeiro argumento. |
383
+ | `deleteManyReturningBy` | `deleteManyReturning` | Filtros de campo seguem o prefixo; retorna os registros removidos. |
384
+ | `deleteManyReturningWhere` | `deleteManyReturning` | Recebe um `VSRepoWhere<T>` como primeiro argumento; retorna os registros removidos. |
385
+
386
+ > `aggregate` e `groupBy` **ainda não estão implementados** na v2 (existiam na v1). Está planejado, mas não disponível no momento.
792
387
 
793
388
  ### Filtros de campo
794
389
 
795
- Filtros são sufixos aplicados ao nome do campo dentro do método. O próprio campo vem capitalizado logo após o prefixo (ou após `By`).
796
-
797
- | Sufixo | Operador Prisma | Argumento obrigatório |
798
- | --------------------- | ---------------------- | --------------------------- |
799
- | *(sem sufixo)* | igualdade (`=`) | sim |
800
- | `Not` | `not` | sim |
801
- | `In` | `in` | sim (array) |
802
- | `NotIn` | `notIn` | sim (array) |
803
- | `Contains` | `contains` | sim |
804
- | `NotContains` | `not.contains` | sim |
805
- | `StartsWith` | `startsWith` | sim |
806
- | `NotStartsWith` | `not.startsWith` | sim |
807
- | `EndsWith` | `endsWith` | sim |
808
- | `NotEndsWith` | `not.endsWith` | sim |
809
- | `GreaterThan` | `gt` | sim |
810
- | `GreaterThanEqual` | `gte` | sim |
811
- | `LessThan` | `lt` | sim |
812
- | `LessThanEqual` | `lte` | sim |
813
- | `Between` | `gte` + `lte` | sim (tupla `[min, max]`) |
814
- | `NotBetween` | `not.gte` + `not.lte` | sim (tupla `[min, max]`) |
815
- | `IsNull` | `null` | não |
816
- | `IsNotNull` | `not: null` | não |
817
- | `IsTrue` | `true` | não |
818
- | `IsFalse` | `false` | não |
819
- | `Insensitive` | `mode: 'insensitive'` | combinador |
820
-
821
- `Insensitive` é um combinador e pode ser usado junto com outro filtro de texto:
822
-
823
- ```ts
824
- findByNameContainsInsensitive // { name: { contains: value, mode: 'insensitive' } }
825
- findByEmailStartsWithInsensitive // { email: { startsWith: value, mode: 'insensitive' } }
826
- findByNameInsensitive // { name: { equals: value, mode: 'insensitive' } }
390
+ Aplicados como sufixos ao nome do campo dentro do método (mesma ideia da v1, com um sufixo renomeado):
391
+
392
+ | Sufixo | Significado | Argumento |
393
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
394
+ | _(sem sufixo)_ | igualdade (`=`) | sim |
395
+ | `Not` | negação | sim |
396
+ | `In` | está em | sim (array) |
397
+ | `NotIn` | não está em | sim (array) |
398
+ | `Contains` | contém substring | sim |
399
+ | `NotContains` | não contém substring | sim |
400
+ | `StartsWith` | começa com | sim |
401
+ | `NotStartsWith` | não começa com | sim |
402
+ | `EndsWith` | termina com | sim |
403
+ | `NotEndsWith` | não termina com | sim |
404
+ | `GreaterThan` | `>` | sim |
405
+ | `GreaterThanEqual` | `>=` | sim |
406
+ | `LessThan` | `<` | sim |
407
+ | `LessThanEqual` | `<=` | sim |
408
+ | `Between` | intervalo inclusivo | sim (tupla `[min, max]`) |
409
+ | `NotBetween` | fora de um intervalo inclusivo | sim (tupla `[min, max]`) |
410
+ | `IsNull` | campo é `null` | não |
411
+ | `IsNotNull` | campo não é `null` | não |
412
+ | `IsTrue` | campo é `true` | não |
413
+ | `IsFalse` | campo é `false` | não |
414
+ | `IgnoreCase` | combinador case-insensitive para filtros de texto | não _(renomeado do `Insensitive` da v1)_ |
415
+ | `Optional` | **explícita** o argumento do campo como opcional — ele já é opcional por padrão, então esse sufixo é facultativo e serve apenas para deixar isso explícito | — |
416
+
417
+ ```typescript
418
+ @DynamicMethod()
419
+ declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
420
+
421
+ @DynamicMethod()
422
+ declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
827
423
  ```
828
424
 
829
- `Between` e `NotBetween` recebem uma **tupla `[valorMinimo, valorMaximo]`**:
830
-
831
- ```ts
832
- methods: {
833
- findManyByAgeBetween: { map: true },
834
- findManyBySalaryNotBetween: { map: true },
835
- findManyByCreatedAtBetween: { map: true },
836
- }
837
-
838
- await userRepository.findManyByAgeBetween([18, 65]);
839
- await userRepository.findManyBySalaryNotBetween([1000, 5000]);
840
- await userRepository.findManyByCreatedAtBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
841
- ```
842
-
843
- O sufixo `Optional` pode ser adicionado a qualquer campo para tornar o argumento opcional:
844
-
845
- ```ts
846
- findByNameOptionalAndEmail // name é opcional, email é obrigatório
847
- ```
848
-
849
- ---
850
-
851
425
  ### Operadores lógicos
852
426
 
853
- | Operador | Uso no nome | Exemplo |
854
- | --------- | -------------------------------- | ---------------------------------- |
855
- | `And` | entre dois campos | `findOneByIdAndEmail` |
856
- | `Or` | entre dois campos | `findByNameOrEmail` |
857
- | `AND` | separa um bloco final de `AND` | `findByEmailOrNameANDActiveStatus` |
858
-
859
- `AND` (em maiúsculas) tem uma regra específica:
427
+ | Operador | Uso no nome | Exemplo |
428
+ | -------- | ------------------------------ | --------------------------------------------------- |
429
+ | `And` | entre dois campos | `findOneByIdAndEmail` |
430
+ | `Or` | entre dois campos | `findByNameOrEmail` |
431
+ | `AND` | separa um bloco final em `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
860
432
 
861
- - Só pode existir **um** `AND` por método.
862
- - Todos os campos depois do `AND` são injetados dentro de `AND: []`.
863
- - Depois de um `AND`, não pode haver um `Or`.
433
+ Regras do `AND` (em capslock), iguais às da v1: só é permitido **um** `AND` por nome de método; todo campo conectado por `And` depois dele é aninhado dentro de `AND: []`; `Or` não pode aparecer depois de um `AND`.
864
434
 
865
- Exemplo:
435
+ ### Filtros de relação
866
436
 
867
- ```ts
868
- methods: {
869
- findOneByIdAndEmail: { map: true },
870
- findByNameOrEmail: { map: true },
871
- findFirstByIdOrEmailAndName: { map: true },
872
- findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
873
- }
437
+ Filtram por campos de entidades relacionadas. Internamente, mapeiam para os operadores `_some`/`_every`/`_none`/`_with`/`_without` de `VSRepoWhere` (veja [`select` e `relations`](#select-e-relations) para o equivalente de eager loading).
874
438
 
875
- await userRepository.findOneByIdAndEmail(1, "john@email.com");
876
- await userRepository.findByNameOrEmail("John", "john@email.com");
877
- await userRepository.findFirstByIdOrEmailAndName(1, "john@email.com", "John");
878
- await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
879
- ```
439
+ | Sufixo | Significado | Restrição |
440
+ | -------------- | ------------------------------------------------- | ----------------------------------------------------------------------- |
441
+ | `Some` | pelo menos um registro relacionado corresponde | apenas relações to-many |
442
+ | `SomeField` | filtra dentro dos registros relacionados | apenas relações to-many |
443
+ | `Every` | todo registro relacionado corresponde | apenas relações to-many (precisa de `Field` para ser um filtro efetivo) |
444
+ | `EveryField` | filtra dentro dos registros relacionados | apenas relações to-many |
445
+ | `None` | nenhum registro relacionado corresponde | apenas relações to-many |
446
+ | `NoneField` | filtra dentro dos registros relacionados | apenas relações to-many |
447
+ | `With` | o registro relacionado existe | apenas relações to-one |
448
+ | `WithField` | filtra um campo dentro do registro relacionado | apenas relações to-one |
449
+ | `Without` | o registro relacionado não existe | apenas relações to-one |
450
+ | `WithoutField` | filtro negado em um campo do registro relacionado | apenas relações to-one |
880
451
 
881
- Gera (`findOneByIdAndEmail`):
452
+ ```typescript
453
+ @DynamicMethod()
454
+ declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
882
455
 
883
- ```ts
884
- {
885
- id: 1,
886
- email: "john@email.com"
887
- }
456
+ @DynamicMethod()
457
+ declare findByProductsSome: () => Promise<User[]>;
888
458
  ```
889
459
 
890
- Gera (`findByNameOrEmail`):
460
+ ### Ordenação, paginação e distinct
891
461
 
892
- ```ts
893
- {
894
- OR: [
895
- { name: "John" },
896
- { email: "john@email.com" }
897
- ]
898
- }
899
- ```
462
+ | Sufixo | Efeito |
463
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
464
+ | `Paginated` | Injeta um argumento `pagination` (`{ limit?, offset? }`) como **penúltimo** parâmetro (antes do `MethodOptions` opcional). |
465
+ | `Ordered` | Injeta um argumento `order: Ordering<T>` como **penúltimo** parâmetro (antes do `MethodOptions` opcional). |
466
+ | `OrderedAndPaginated` | Injeta `order` como antepenúltimo, depois `pagination` como penúltimo — ambos antes do `MethodOptions`. |
467
+ | `PaginatedAndOrdered` | Injeta `pagination` como antepenúltimo, depois `order` como penúltimo — ambos antes do `MethodOptions`. |
468
+ | `OrderBy<Campo>Asc` / `OrderBy<Campo>Desc` | **Novo na v2.** Embute uma ordenação fixa diretamente no nome do método — encadeie campos com `And` (ex.: `OrderByCreatedAtAscAndNameDesc`). Não precisa de argumento `order`. |
469
+ | `Distinct<Campo>And<Campo>...` | Embute campos `distinct` fixos diretamente no nome do método (só válido em métodos da família `findBy`/`findWhere`). |
470
+ | `IgnoreConflicts` | No `createMany`/`createManyReturning`, ignora registros que violariam uma constraint única, em vez de lançar erro. _(Renomeado do `SkipDuplicates` da v1.)_ |
900
471
 
901
- Gera (`findFirstByIdOrEmailAndName`):
472
+ > ⚠️ **Ordem dos parâmetros:** `pagination` e `order` sempre vêm **antes** do último argumento opcional `MethodOptions<T>`. Quando `order` e `pagination` estão presentes juntos, a ordem relativa entre eles segue o nome do sufixo (`OrderedAndPaginated` → order, pagination; `PaginatedAndOrdered` → pagination, order).
902
473
 
903
- ```ts
904
- {
905
- OR: [
906
- { id: 1 },
907
- {
908
- email: "john@email.com",
909
- name: "John"
910
- }
911
- ]
912
- }
913
- ```
474
+ ```typescript
475
+ // Paginated: pagination é o penúltimo parâmetro (antes do MethodOptions)
476
+ @DynamicMethod()
477
+ declare findByActiveOrderByCreatedAtDescPaginated:
478
+ (active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
914
479
 
915
- Gera (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
480
+ // OrderedAndPaginated: order, depois pagination, depois MethodOptions
481
+ @DynamicMethod()
482
+ declare findByNameContainsIgnoreCaseOrderedAndPaginated:
483
+ (name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
916
484
 
917
- ```ts
918
- {
919
- OR: [
920
- { email: "john@email.com" },
921
- { name: "John" }
922
- ],
923
- AND: [
924
- { activeStatus: true },
925
- { age: { gt: 17 } }
926
- ]
927
- }
928
- ```
485
+ @DynamicMethod()
486
+ declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
929
487
 
930
- ---
488
+ // createManyReturning: mesmo comportamento do createMany, mas retorna os registros criados
489
+ @DynamicMethod()
490
+ declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
931
491
 
932
- ### Filtros de relação
933
-
934
- Permitem filtrar por campos de modelos relacionados.
492
+ // findOne sem filtro (equivalente ao findOneOrThrow sem filtro, mas retorna null em vez de lançar)
493
+ @DynamicMethod()
494
+ declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
495
+ ```
935
496
 
936
- > [!IMPORTANT]
497
+ > ⚠️ **Precedência entre `Distinct` e `OrderBy`:** quando os dois são usados no mesmo nome de método, **`Distinct` deve vir antes de `OrderBy`**:
937
498
  >
938
- > - **Tipagem de relação**: Para que o TypeScript reconheça os tipos dos campos de relação nos métodos dinâmicos, o tipo genérico da entidade passado para `setupVSRepo` deve incluir as relações estruturadas (ex.: usando o `UserGetPayload<{ include: { profile: true, posts: true } }>` do Prisma).
939
- > - **Compatibilidade de sufixos**:
940
- > - Os sufixos `Some`, `Every` e `None` só funcionam em relações **to-many** (`many-to-many` e `one-to-many`).
941
- > - Os sufixos `With` e `Without` só funcionam em relações **to-one** (`one-to-one` e `many-to-one`).
942
-
943
- | Sufixo de relação | Operador Prisma | Observação |
944
- | ---------------------- | ---------------- | ------------------------------------------------- |
945
- | `Some` | `some: {}` | A relação tem *algum* registro |
946
- | `SomeField` | `some.field` | Filtra dentro dos registros da relação |
947
- | `EveryField` | `every.field` | Filtra dentro dos registros da relação |
948
- | `None` | `none: {}` | A relação não tem *nenhum* registro |
949
- | `NoneField` | `none.field` | Filtra dentro dos registros da relação |
950
- | `With` | `is: {}` | A relação existe (não é nula) |
951
- | `WithField` | `is.field` | Filtra um campo dentro da relação |
952
- | `Without` | `isNot: {}` | A relação não existe (é nula) |
953
- | `WithoutField` | `isNot.field` | Filtra um campo dentro da relação com negação |
954
-
955
- Considerando `user` com uma relação to-one `profile` e uma relação to-many `posts`:
956
-
957
- ```ts
958
- methods: {
959
- // to-many (posts)
960
- findByPostsSome: { map: true }, // tem pelo menos um post
961
- findByPostsSomeTitle: { map: true }, // tem pelo menos um post com aquele título
962
- findByPostsEveryPublishedIsTrue:{ map: true }, // todos os posts estão publicados
963
- findByPostsNone: { map: true }, // não tem nenhum post
964
- findByPostsNoneTitle: { map: true }, // nenhum post tem aquele título
965
-
966
- // to-one (profile)
967
- findByProfileWith: { map: true }, // tem um profile (não é nulo)
968
- findByProfileWithBio: { map: true }, // tem um profile com aquela bio
969
- findByProfileWithout: { map: true }, // não tem profile (é nulo)
970
- findByProfileWithoutBio: { map: true }, // tem um profile, mas com uma bio diferente da informada
971
- }
499
+ > ```typescript
500
+ > @DynamicMethod()
501
+ > declare findByActiveDistinctNameOrderByCreatedAtDesc:
502
+ > (active: boolean) => Promise<User[]>;
503
+ > ```
504
+ >
505
+ > Colocar `OrderBy` antes de `Distinct` (ex.: `findByActiveOrderByCreatedAtDescDistinctName`) não é um padrão válido e não será interpretado como esperado.
972
506
 
973
- await userRepository.findByPostsSome();
974
- await userRepository.findByPostsSomeTitle("My first post");
975
- await userRepository.findByPostsEveryPublishedIsTrue();
976
- await userRepository.findByPostsNone();
977
- await userRepository.findByPostsNoneTitle("Draft");
507
+ ### Options do decorador
978
508
 
979
- await userRepository.findByProfileWith();
980
- await userRepository.findByProfileWithBio("Hello, world!");
981
- await userRepository.findByProfileWithout();
982
- await userRepository.findByProfileWithoutBio("Old bio");
983
- ```
509
+ `@DynamicMethod<T>(options?)` aceita:
984
510
 
985
- Gera (`findByPostsSomeTitle`):
511
+ | Option | Tipo | Descrição |
512
+ | ---------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
513
+ | `proxyTo` | `string` | Redireciona a lógica do método para outro padrão de método dinâmico válido — útil para nomes que não seguem a convenção de nomenclatura. |
514
+ | `injectOrdering` | `Ordering<T>` | Ordenação fixa injetada automaticamente, sobrescrevendo o `defaultOrdering` do repository. |
986
515
 
987
- ```ts
988
- {
989
- posts: {
990
- some: { title: "My first post" }
991
- }
992
- }
516
+ ```typescript
517
+ @DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
518
+ declare findByStatus: (status: string) => Promise<User[]>;
993
519
  ```
994
520
 
995
- Gera (`findByPostsEveryPublishedIsTrue`):
996
-
997
- ```ts
998
- {
999
- posts: {
1000
- every: { published: true }
1001
- }
1002
- }
1003
- ```
521
+ ---
1004
522
 
1005
- Gera (`findByProfileWithBio`):
523
+ ## Query methods (SQL raw)
1006
524
 
1007
- ```ts
1008
- {
1009
- profile: {
1010
- is: { bio: "Hello, world!" }
1011
- }
1012
- }
1013
- ```
525
+ `@QueryMethod` ignora totalmente o engine de parsing por nome e executa uma instrução SQL raw através do método `query()` do adapter. Use placeholders `$1`, `$2`, ... — nunca interpole valores diretamente na string SQL.
1014
526
 
1015
- Gera (`findByProfileWithout`):
527
+ ```typescript
528
+ class UserRepository extends VSRepository<User, string> {
529
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1')
530
+ declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
1016
531
 
1017
- ```ts
1018
- {
1019
- profile: {
1020
- isNot: {}
1021
- }
532
+ @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
533
+ declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
1022
534
  }
1023
535
  ```
1024
536
 
1025
- > `Some`, `None`, `With` e `Without` (sem campo) não recebem argumento — toda a relação é testada quanto à existência de registros (`some`/`none`) ou quanto a ser `null`/não `null` (`is`/`isNot`). As variantes `SomeField`, `EveryField`, `NoneField`, `WithField` e `WithoutField` recebem como argumento o valor do campo filtrado.
1026
-
1027
- ---
1028
-
1029
- ### Sufixos de paginação e ordenação
1030
-
1031
- Aplicados no **final** do nome do método, injetam automaticamente os argumentos de paginação e ordenação.
537
+ | Option | Tipo | Padrão | Descrição |
538
+ | ----------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
+ | `modifying` | `boolean` | `false` | Quando `true`, executa como `INSERT`/`UPDATE`/`DELETE` e o método resolve para o número de linhas afetadas. Quando `false`, executa como query de leitura e resolve para o tipo de retorno declarado. |
1032
540
 
1033
- | Sufixo | Argumentos adicionais |
1034
- | ------------------------ | ---------------------------- |
1035
- | `Paginated` | `(pagination)` |
1036
- | `Ordered` | `(order)` |
1037
- | `OrderedAndPaginated` | `(order, pagination)` |
1038
- | `PaginatedAndOrdered` | `(pagination, order)` |
541
+ Query methods aceitam `{ args, db? }` na chamada — `db` permite que participem de um bloco `transaction()`, assim como os métodos base e dinâmicos.
1039
542
 
1040
- Para `createMany` e `createManyAndReturn`, o sufixo `SkipDuplicates` está disponível:
543
+ ### Queries raw pontuais com `query()`
1041
544
 
1042
- | Sufixo | Efeito |
1043
- | ---------------------| ---------------------------------------------- |
1044
- | `SkipDuplicates` | Ignora registros duplicados durante a inserção |
545
+ Para SQL raw pontual que não justifica declarar um `@QueryMethod` na classe do repository, chame `query()` diretamente — ele está disponível em toda instância de `VSRepository` e passa pela mesma implementação de `query()` do adapter por baixo dos panos:
1045
546
 
1046
- ---
1047
-
1048
- ### Distinct
1049
-
1050
- O sufixo `Distinct` permite obter apenas registros únicos com base em um ou mais campos, equivalente à opção `distinct` do Prisma.
1051
-
1052
- Para usá-lo, coloque `Distinct` no nome do método (depois dos filtros de campo, se houver), seguido dos campos desejados separados por `And`. A primeira letra de cada campo deve ser maiúscula, assim como nos filtros de campo comuns.
1053
-
1054
- ```ts
1055
- methods: {
1056
- // Retorna usuários únicos combinando "age" e "role" (sem filtro de campo)
1057
- findManyDistinctAgeAndRole: { map: true },
1058
-
1059
- // Distinct combinado com o sufixo Paginated
1060
- findManyDistinctNamePaginated: { map: true },
1061
-
1062
- // Distinct combinado com um filtro de campo (name) — filtra por name e depois aplica distinct em role
1063
- findManyByNameDistinctRole: { map: true },
1064
- },
547
+ ```typescript
548
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
1065
549
  ```
1066
550
 
1067
- ```ts
1068
- // Sem argumentos: os campos distinct já estão fixados no nome do método
1069
- await userRepository.findManyDistinctAgeAndRole();
1070
-
1071
- // O argumento de paginação continua funcionando normalmente
1072
- await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
551
+ ```typescript
552
+ const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
553
+ args: ["maria@email.com"],
554
+ });
1073
555
 
1074
- // O filtro do campo "name" continua sendo passado normalmente como argumento
1075
- await userRepository.findManyByNameDistinctRole("John");
556
+ const linhasAfetadas = await userRepository.query<number>(
557
+ 'UPDATE "user" SET active = true WHERE id = $1',
558
+ { args: ["123"], modifying: true },
559
+ );
1076
560
  ```
1077
561
 
1078
- > Os campos especificados depois de `Distinct` são resolvidos a partir do nome do método em tempo de build — eles **não** se tornam argumentos em tempo de execução, diferente dos filtros de campo comuns.
1079
-
1080
- `Distinct` está disponível nos prefixos que leem múltiplos ou um único registro: `findMany`, `findManyBy`, `findFirst`, `findFirstBy`, `findFirstOrThrow`, `findFirstOrThrowBy`, `findBy`, `findOneBy`, `findWhere`, `findOneWhere`, `findListWhere`, `existsBy` e `existsWhere`.
562
+ | Option | Tipo | Padrão | Descrição |
563
+ | ----------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
564
+ | `args` | `any[]` | `undefined` | Parâmetros posicionais injetados nos placeholders `$1`, `$2`, ... Nunca interpole valores diretamente na string SQL. |
565
+ | `db` | `any` | Client padrão do repository | Client ou transação do banco em que essa query deve rodar. |
566
+ | `modifying` | `boolean` | `false` | Quando `true`, trata a instrução como `INSERT`/`UPDATE`/`DELETE`. |
1081
567
 
1082
- ---
1083
-
1084
- ### Configuração de método
1085
-
1086
- Cada entrada em `methods` aceita as seguintes opções:
1087
-
1088
- | Opção | Tipo | Padrão | Descrição |
1089
- | --------------------- | ---------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
1090
- | `map` | `boolean` | — | **Obrigatório.** Define se o método será exposto no repositório. |
1091
- | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combina com `requiredWhere`. `overwrite` ignora `requiredWhere`. |
1092
- | `selectModel` | `keyof SelectModels \| false` | — | Sobrescreve o `defaultSelectModel` para este método. |
1093
- | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Apenas para `findBy`. `'one'` retorna `T \| null`; `'list'` retorna `T[]`. |
1094
- | `proxyTo` | `Padrão de método válido` | — | Delega a lógica para outro padrão de método válido. |
1095
- | `pushWhere` | `WhereModel<M>` | — | `where` extra adicionado à query além do `requiredWhere`. |
1096
- | `injectOrdering` | `OrderingModel<M>` | — | Ordenação fixa injetada automaticamente na query. |
1097
- | `injectPagination` | `PaginationModel<M>` | — | Paginação fixa injetada automaticamente na query. |
1098
- | `query` | `{ value: string; modifying?: boolean }` | — | Transforma o método em um **Query Method** (SQL bruto). Ignora todas as outras opções acima — veja [Query Methods](#query-methods). |
568
+ Assim como os métodos base, dinâmicos e query, `query()` aceita `db` em `options` para participar de um bloco `transaction()`.
1099
569
 
1100
570
  ---
1101
571
 
1102
- ### Aggregate e GroupBy
1103
-
1104
- ```ts
1105
- const userRepository = setupVSRepo<User, "user">()(({
1106
- tableName: "user",
1107
- pkName: "id",
1108
- methods: {
1109
- aggregate: { map: true },
1110
- groupBy: { map: true },
1111
- },
1112
- }).build(prisma);
1113
- ```
1114
-
1115
- > [!NOTE]
1116
- > Esses métodos devem ter exatamente esses nomes (`aggregate` e `groupBy`).
1117
- > Diferente dos demais métodos dinâmicos, eles recebem argumentos nativos do Prisma e **ignoram** as configurações `selectModels`, `pushWhere` e `requiredWhere`.
1118
-
1119
- ---
1120
-
1121
- ### Query Methods
1122
-
1123
- Query Methods permitem que um método execute **SQL bruto** diretamente, contornando totalmente o parser de nomes dos métodos dinâmicos. São úteis para consultas complexas (joins pesados, CTEs, funções específicas do banco) que não são práticas de expressar com os prefixos/sufixos padrão.
1124
-
1125
- Internamente, o VSRepository executa a SQL através do Prisma usando `$queryRawUnsafe` (para leitura) ou `$executeRawUnsafe` (para escrita), e os valores do array `args` são passados como **parâmetros posicionais** (`$1`, `$2`, ...) — a mesma técnica de *prepared statements* usada pelo próprio Prisma. Isso significa que os valores nunca são concatenados na string SQL, o que é o que efetivamente previne SQL Injection.
1126
-
1127
- > [!WARNING]
1128
- > `$1`, `$2`, ... na sua SQL devem representar sempre **valores** (parâmetros de dados), nunca nomes de colunas, tabelas ou trechos de SQL dinâmicos. Nomes de identificadores (colunas/tabelas) não podem ser passados como parâmetro posicional — se seu método precisar variar isso, monte o SQL a partir de um conjunto fixo e conhecido de opções no seu próprio código, nunca a partir de entrada não confiável.
1129
-
1130
- ```ts
1131
- const userRepository = setupVSRepo<User, "user">()({
1132
- tableName: "user",
1133
- pkName: "id",
1134
- methods: {
1135
- // Query method de leitura (não-modifying)
1136
- findActiveUsersRaw: {
1137
- map: true,
1138
- query: {
1139
- value: 'SELECT * FROM "user" WHERE active = $1',
1140
- },
1141
- },
1142
-
1143
- // Query method de escrita (modifying: true)
1144
- deactivateUsersOlderThanRaw: {
1145
- map: true,
1146
- query: {
1147
- value: 'UPDATE "user" SET active = false WHERE "createdAt" < $1',
1148
- modifying: true,
1149
- },
1150
- },
1151
- },
1152
- }).build(prisma);
1153
- ```
572
+ ## Transações
1154
573
 
1155
- **Chamando um Query Method:**
574
+ Todos os métodos (base, dinâmicos e de query) aceitam `options.db` para participar de uma transação compartilhada:
1156
575
 
1157
- Todo Query Method recebe um único argumento no formato `{ args: [...], db? }`:
576
+ ```typescript
577
+ await userRepository.transaction(async tx => {
578
+ const usuario = await userRepository.save(
579
+ { name: "Maria", email: "maria@email.com" },
580
+ { db: tx },
581
+ );
1158
582
 
1159
- ```ts
1160
- // Não-modifying: retorna 'any' por padrão, mas aceita um generic para
1161
- // inferir/afirmar o tipo de retorno na própria chamada
1162
- const usuariosAtivos = await userRepository.findActiveUsersRaw<User[]>({
1163
- args: [true],
1164
- });
1165
-
1166
- // Modifying: sempre retorna 'number' (quantidade de linhas afetadas)
1167
- const afetados = await userRepository.deactivateUsersOlderThanRaw({
1168
- args: [new Date("2024-01-01")],
1169
- });
1170
-
1171
- // Participando de uma transação, via 'db'
1172
- await userRepository.prisma.$transaction(async (tx) => {
1173
- await userRepository.deactivateUsersOlderThanRaw({
1174
- args: [new Date("2024-01-01")],
1175
- db: tx,
1176
- });
583
+ await userLogsRepository.save(
584
+ { action: "Usuário criado", data: { userId: usuario.id } },
585
+ { db: tx },
586
+ );
1177
587
  });
1178
588
  ```
1179
589
 
1180
- | Opção | Tipo | Padrão | Descrição |
1181
- | ------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1182
- | `value` | `string` | — | **Obrigatório.** SQL bruto a ser executado. Use `$1`, `$2`, ... para os placeholders dos valores em `args`. |
1183
- | `modifying` | `boolean` | `false` | Quando `true`, executa via `$executeRawUnsafe` e o método sempre resolve para `number`. Quando `false`, executa via `$queryRawUnsafe` e o método resolve para `TReturn` (`any` por padrão, inferível via generic na chamada). |
1184
-
1185
- > [!NOTE]
1186
- > Diferente dos demais métodos dinâmicos, Query Methods **ignoram completamente** `selectModels`, `requiredWhere`, `pushWhere`, `whereType`, `injectOrdering`, `injectPagination` e `proxyTo` — nada disso se aplica, já que não há parsing de nome nem montagem de `where`/`select` pelo VSRepository. Nomes de métodos livres (fora dos padrões de `findBy`, `updateBy`, etc.) também **não** exigem `proxyTo`.
1187
-
1188
- A mesma funcionalidade está disponível na abordagem baseada em classes através do decorator `@QueryMethod` — veja o [README-DynamicRepo.pt-BR.md](./README-DynamicRepo.pt-BR.md#o-decorator-querymethod).
1189
-
1190
- ---
1191
-
1192
- ## Relações no save
590
+ Repositories diferentes podem compartilhar a mesma transação, desde que seus adapters apontem para a mesma conexão do ORM por trás deles.
1193
591
 
1194
- Configure as relações para que `save` e `patch` as gerenciem automaticamente (`saveList` e `patchList` também gerenciam relações automaticamente).
592
+ `transaction()` aceita um `VSRepoTransactionOptions` opcional como segundo argumento:
1195
593
 
1196
- ```ts
1197
- import type { Prisma } from "../../generated/prisma/client";
594
+ ```typescript
595
+ import { TransactionIsolationLevel } from "vsrepo";
1198
596
 
1199
- type User = Prisma.userGetPayload<{
1200
- include: { profile: true; posts: true };
1201
- }>;
1202
-
1203
- const userRepository = setupVSRepo<User, "user">()(({
1204
- tableName: "user",
1205
- pkName: "id",
1206
-
1207
- relations: {
1208
- profile: {
1209
- pk: "id",
1210
- mode: "oto",
1211
- restriction: "set",
1212
- },
1213
- posts: {
1214
- pk: "id",
1215
- mode: "otm",
1216
- restriction: "add",
597
+ await userRepository.transaction(
598
+ async tx => {
599
+ await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1217
600
  },
1218
- },
1219
- }).build(prisma);
601
+ { isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
602
+ );
1220
603
  ```
1221
604
 
1222
- **Modos de relação:**
605
+ | Option | Type | Descrição |
606
+ | ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
607
+ | `isolationLevel` | `TransactionIsolationLevel` | Nível de isolamento usado na transação. O padrão é o default do ORM por trás dela. |
608
+ | `timeoutMs` | `number` | Tempo máximo (em ms) que a transação pode rodar antes de ser abortada. |
1223
609
 
1224
- | Modo | Relação |
1225
- | ----- | ------------------ |
1226
- | `oto` | um-para-um |
1227
- | `otm` | um-para-muitos |
1228
- | `mto` | muitos-para-um |
1229
- | `mtm` | muitos-para-muitos |
610
+ `TransactionIsolationLevel` espelha os níveis de isolamento SQL padrão: `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`. O suporte a um determinado nível depende do adapter/ORM e do banco de dados por trás dele.
1230
611
 
1231
- **Restrições:**
1232
-
1233
- | Restrição | Comportamento na atualização |
1234
- | ----------- | ----------------------------------------------------------- |
1235
- | `set` | Substitui totalmente (remove os que não foram enviados) |
1236
- | `add` | Adiciona/atualiza sem remover os existentes |
612
+ ---
1237
613
 
1238
- > [!WARNING]
1239
- > **`set` significa coisas diferentes dependendo do `mode` da relação — e isso pode causar perda de dados se você não tomar cuidado.**
1240
- >
1241
- > Em relações onde o registro relacionado **pertence** ao registro pai (`oto` e `otm`), "remover os que não foram enviados" significa **apagar o registro do banco de dados** (`delete`/`deleteMany`). Em relações onde o registro relacionado é **independente** (`mto` e `mtm`), "remover" significa apenas **desvincular** (`disconnect`/`set: []`) — o registro relacionado continua existindo no banco, apenas deixa de apontar para o pai (ou de estar na tabela de junção).
1242
- >
1243
- > | Modo | `restriction: "set"` quando um item é omitido | O item continua existindo no banco de dados? |
1244
- > | ----- | ------------------------------------------------ | ----------------------------------------------------- |
1245
- > | `oto` | Passar `null` no campo → **apaga** o registro relacionado (`delete: true`) | Não |
1246
- > | `otm` | Itens fora da lista enviada → **apagados** (`deleteMany` com `notIn`) | Não |
1247
- > | `mto` | Passar `null` no campo (com `nullable: true`) → **desvincula** (`disconnect: true`) | Sim |
1248
- > | `mtm` | Itens fora da lista enviada → **desvinculados** da tabela de junção (`set: []`) | Sim |
1249
- >
1250
- > Exemplo prático: se `posts` for `otm` com `restriction: "set"`, um `save`/`patch` que envia o usuário com apenas 2 dos 5 posts existentes vai **apagar os outros 3 posts do banco de dados**, não apenas desvinculá-los do usuário. Se o comportamento esperado for apenas desvincular sem apagar, use `restriction: "add"` (que nunca remove nada) e trate a remoção manualmente.
614
+ ## Tipos utilitários
1251
615
 
1252
- **Relação `mto` com nullable:**
616
+ Além dos tipos que descrevem o formato da entidade já vistos acima (`VSRepoSelect`, `VSRepoRelations`, `VSRepoWhere`), o VSRepository exporta um conjunto de tipos utilitários. Eles aparecem ao longo de várias seções anteriores, mas aqui está uma referência consolidada. Todos fazem parte da API pública e podem ser importados diretamente:
1253
617
 
1254
- Use `nullable` (minúsculo) para permitir desvincular uma relação many-to-one:
618
+ ```typescript
619
+ import type {
620
+ MethodOptions,
621
+ Pagination,
622
+ Ordering,
623
+ OrderByField,
624
+ SortDirection,
625
+ SeeMode,
626
+ DeepPartial,
627
+ CountResult,
628
+ QueryMethodArg,
629
+ KeysOfType,
630
+ Primitive,
631
+ VSRepoWhere,
632
+ VSRepoOrmTypes,
633
+ VSRepoTransactionOptions,
634
+ TransactionIsolationLevel,
635
+ } from "vsrepo";
636
+ ```
637
+
638
+ | Tipo | Descrição | Usado por |
639
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
640
+ | `MethodOptions<T, K>` | Options aceitas como último argumento por todo método base e dinâmico: `select`, `relations`, `see`, `db`. | [Métodos base](#métodos-base), [Métodos Dinâmicos](#métodos-dinâmicos). |
641
+ | `Pagination` | `{ limit?, offset? }` aceito por `getAll` e pelos métodos dinâmicos com `Paginated`. | [Métodos base](#métodos-base), [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct). |
642
+ | `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Formato de ordenação aceito por `getAll`, `defaultOrdering` e `injectOrdering`, e pelos métodos dinâmicos com `Ordered`. Pode ser um único objeto ou um array encadeado; objetos aninhados ordenam relações to-one. | [Options do construtor](#options-do-construtor), [Options do decorador](#options-do-decorador), [Ordenação, paginação e distinct](#ordenação-paginação-e-distinct). |
643
+ | `SeeMode` | `"active" \| "removed" \| "all"` — controla a visibilidade de registros com soft-delete. | [Soft-delete](#soft-delete). |
644
+ | `DeepPartial<T>` | Torna todas as propriedades de `T` opcionais recursivamente, incluindo objetos aninhados e elementos de array. | `save`, `saveList`, `patch`, `merge`, e todo método de escrita do `VSRepoAdapter`. |
645
+ | `CountResult` | `{ count: number }` — o formato retornado por operações em lote. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
646
+ | `QueryMethodArg<T>` | `{ args?: T, db? }` — parâmetros posicionais do SQL (`$1`, `$2`, ...) e cliente de transação para o `@QueryMethod`. | [Query methods (SQL raw)](#query-methods-sql-raw). |
647
+ | `KeysOfType<T, K>` | Extrai as chaves de `T` cujo tipo de valor é atribuível a `K`. | Restringe `pkName`, em [Options do construtor](#options-do-construtor), aos campos da entidade compatíveis com o tipo de chave primária configurado. |
648
+ | `Primitive` | União de tipos escalares (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date`) tratados como valores-folha — e não relações — ao percorrer o formato de uma entidade. | Usado por `Ordering<T>` para distinguir campos escalares de campos de relação. |
649
+ | `VSRepoWhere<T>` | Tipo de filtro agnóstico de ORM aceito pelos métodos dinâmicos `*Where` (ex.: `findWhere`, `findOneWhere`, `updateWhere`). Suporta filtros de campo, operadores lógicos (`AND`/`OR`/`NOT`) e filtros de relação. | [Prefixos `findWhere`, `findOneWhere` e demais `*Where`](#prefixos-disponíveis). |
650
+ | `VSRepoOrmTypes` | `{ dbClient; dbTransaction }` — descreve os tipos de client/transaction do seu ORM. Passado como terceiro generic de `VSRepository<Entity, PKType, OrmTypes>` para tipar `getDbClient()`, `transaction()` e a option `db` em vez de `any`. | [Criando um repository](#criando-um-repository). |
651
+ | `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options aceitas como segundo argumento de `transaction()`. | [Transações](#transações). |
652
+ | `TransactionIsolationLevel` | Enum dos níveis de isolamento SQL padrão (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) aceitos por `VSRepoTransactionOptions.isolationLevel`. | [Transações](#transações). |
653
+
654
+ ### `DeepPartial<T>`
655
+
656
+ Torna todas as propriedades opcionais recursivamente, percorrendo objetos aninhados e elementos de array — diferente do `Partial<T>` nativo do TypeScript, que só torna o nível superior opcional:
657
+
658
+ ```typescript
659
+ type User = { id: string; name: string; address: { city: string; zip: string } };
660
+
661
+ const patch: DeepPartial<User> = {
662
+ address: { city: "São Paulo" }, // zip pode ser omitido; city mantém seu tipo
663
+ };
1255
664
 
1256
- ```ts
1257
- relations: {
1258
- category: {
1259
- pk: "id",
1260
- mode: "mto",
1261
- restriction: "set",
1262
- nullable: true, // permite passar null para desvincular
1263
- },
665
+ await userRepository.patch(id, patch);
666
+ ```
667
+
668
+ ### `KeysOfType<T, K>`
669
+
670
+ Filtra um tipo de objeto para as chaves cujo valor corresponde a um tipo dado — é isso que permite que `pkName` aceite apenas campos da entidade que sejam de fato atribuíveis ao tipo de chave primária do repository:
671
+
672
+ ```typescript
673
+ type User = { id: string; age: number; name: string };
674
+ type StringKeys = KeysOfType<User, string>; // "id" | "name"
675
+ ```
676
+
677
+ ### `Ordering<T>`
678
+
679
+ Aceita um único objeto de ordenação ou um array deles, aplicados na ordem declarada:
680
+
681
+ ```typescript
682
+ const order: Ordering<User> = { createdAt: "desc" };
683
+ const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
684
+
685
+ await userRepository.getAll({ order: chained });
686
+ ```
687
+
688
+ ---
689
+
690
+ ## Escrevendo seu próprio adapter
691
+
692
+ Como o núcleo é agnóstico de ORM e é distribuído sem um adapter embutido, adicionar suporte a um ORM/banco — seja como solução provisória para o seu próprio projeto, seja como candidato a um futuro pacote `@vsrepo/*-adapter` — significa implementar a classe abstrata `VSRepoAdapter<T>`:
693
+
694
+ ```typescript
695
+ export abstract class VSRepoAdapter<T> {
696
+ abstract runInTransaction<R>(
697
+ fn: (tx: any) => Promise<R>,
698
+ options?: VSRepoTransactionOptions,
699
+ ): Promise<R>;
700
+ abstract getDbClient(): any;
701
+ abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
702
+ abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
703
+ abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
704
+ abstract findMany(
705
+ where: VSRepoWhere<T>,
706
+ options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
707
+ ): Promise<T[]>;
708
+ abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
709
+ abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
710
+ abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
711
+ abstract createMany(
712
+ objs: DeepPartial<T>[],
713
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
714
+ ): Promise<CountResult>;
715
+ abstract createManyReturning(
716
+ objs: DeepPartial<T>[],
717
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
718
+ ): Promise<T[]>;
719
+ abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
720
+ abstract deleteMany(
721
+ where: VSRepoWhere<T>,
722
+ options?: AdapterMethodOptions<T>,
723
+ ): Promise<CountResult>;
724
+ abstract deleteManyReturning(
725
+ where: VSRepoWhere<T>,
726
+ options?: AdapterMethodOptions<T>,
727
+ ): Promise<T[]>;
728
+ abstract update(
729
+ where: VSRepoWhere<T>,
730
+ obj: DeepPartial<T>,
731
+ options?: AdapterMethodOptions<T>,
732
+ ): Promise<T>;
733
+ abstract updateMany(
734
+ where: VSRepoWhere<T>,
735
+ obj: DeepPartial<T>,
736
+ options?: AdapterMethodOptions<T>,
737
+ ): Promise<CountResult>;
738
+ abstract updateManyReturning(
739
+ where: VSRepoWhere<T>,
740
+ obj: DeepPartial<T>,
741
+ options?: AdapterMethodOptions<T>,
742
+ ): Promise<T[]>;
743
+ abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
744
+ abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
745
+ abstract merge<K>(
746
+ where: VSRepoWhere<T>,
747
+ obj: DeepPartial<T>,
748
+ options?: AdapterMethodOptions<T>,
749
+ ): Promise<K & T>;
750
+ abstract upsert(
751
+ where: VSRepoWhere<T>,
752
+ create: DeepPartial<T>,
753
+ update: DeepPartial<T>,
754
+ options?: AdapterMethodOptions<T>,
755
+ ): Promise<T>;
1264
756
  }
1265
757
  ```
1266
758
 
1267
- ---
1268
-
1269
- ## Transações
1270
-
1271
- Todos os métodos aceitam `options.db` para participar de uma transação:
759
+ O `VSRepository` nunca fala diretamente com o ORM — ele só chama esses métodos com um `VSRepoWhere<T>` e um `AdapterMethodOptions<T>` já resolvidos. Uma vez que um adapter implemente esse contrato, todo método base, método dinâmico e query method passa a funcionar com ele automaticamente. Pra uma implementação completa e funcional, veja o repositório externo [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter).
1272
760
 
1273
- ```ts
1274
- await userRepository.prisma.$transaction(async (tx) => {
1275
- const user = await userRepository.save(
1276
- { name: "Mary", email: "mary@email.com", password: "password" },
1277
- { db: tx }
1278
- );
761
+ ### Logging a partir do seu adapter
1279
762
 
1280
- await userLogsRepository.save(
1281
- { action: "User registration", data: { registeredUser: user.id } },
1282
- { db: tx }
1283
- );
1284
- });
1285
- ```
763
+ O `vsrepo` exporta a mesma classe `VSLogger` usada internamente pelo core, então seu adapter pode logar no mesmo formato/estilo (timestamps, labels de nível coloridos, avisos de operação lenta) em vez de implementar o seu próprio:
1286
764
 
1287
- Para `saveList` e `patchList`, o campo `db` deve ser um `DbTransaction`:
765
+ ```typescript
766
+ import { VSLogger, VSLogLevel } from "vsrepo";
1288
767
 
1289
- ```ts
1290
- await prisma.$transaction(async (tx) => {
1291
- // CORRETO: tx é um DbTransaction
1292
- const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
768
+ export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
769
+ private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
1293
770
 
1294
- await userLogsRepository.save(
1295
- { action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
1296
- { db: tx }
1297
- );
1298
- });
771
+ async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
772
+ const start = this.logger.startPerformLog("adapter findOne");
773
+ try {
774
+ // ... fala com o ORM ...
775
+ this.logger.endPerformLog(start);
776
+ return result;
777
+ } catch (err) {
778
+ this.logger.endPerformLog(start);
779
+ this.logger.logError("adapter findOne falhou", err);
780
+ throw err;
781
+ }
782
+ }
783
+ }
1299
784
  ```
1300
785
 
1301
- ---
1302
-
1303
- ## Estendendo um repositório
1304
-
1305
- ```ts
1306
- const userRepository = setupVSRepo<User, "user">()(({
1307
- tableName: "user",
1308
- pkName: "id",
1309
- methods: {
1310
- findOneByEmailEndsWith: { map: true },
1311
- },
1312
- })
1313
- .build(prisma)
1314
- .extend((repo) => ({
1315
- findActiveByDomain: async (domain: string) => {
1316
- return repo.findOneByEmailEndsWith(`@${domain}`);
1317
- },
786
+ | Método | Descrição |
787
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
788
+ | `new VSLogger(logLevel, name, slowThresholdMs?)` | Cria um logger; `name` prefixa cada linha, `slowThresholdMs` tem default 300. |
789
+ | `logDebug/logInfo/logWarn(text, obj?)` | Loga no nível dado se `logLevel` permitir; `obj` é anexado como JSON formatado. |
790
+ | `logError(text, err?)` | Loga em `ERROR`; se `err` for uma `Error`, só `name`/`message`/`stack`/`cause` são logados. |
791
+ | `startPerformLog(operation)` / `endPerformLog(data)` | Envolve um trecho de código para logar sua duração, escalando pra `WARN` se ultrapassar `slowThresholdMs`. |
792
+ | `getLogLevel()` | Retorna o `VSLogLevel` configurado do logger. |
1318
793
 
1319
- activateMultiple: async (ids: string[]) => {
1320
- return repo.patchList(ids.map(id => [id, { active: true }]));
1321
- },
1322
- }));
1323
- ```
794
+ Isso é puramente uma conveniência para autores de adapters — nada no core exige que seu adapter o utilize.
1324
795
 
1325
796
  ---
1326
797
 
1327
798
  ## Tratamento de erros
1328
799
 
1329
- O VSRepository lança `VSRepoError` e suas subclasses em situações específicas (erros do Prisma não são sobrescritos):
800
+ A v2 simplifica a hierarquia de erros da v1: em vez de várias subclasses, existe uma classe base `VSRepoError` carregando um campo `type: VSRepoErrorType`, além de uma subclasse dedicada `VSRepoAdapterError` (veja abaixo) para falhas vindas do ORM/banco subjacente.
1330
801
 
1331
- ```ts
1332
- import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
802
+ ```typescript
803
+ import { VSRepoError } from "vsrepo";
1333
804
 
1334
805
  try {
1335
- const user = await userRepository.getOrThrow("id-that-does-not-exist");
806
+ await userRepository.get(id);
1336
807
  } catch (error) {
1337
- if (error instanceof VSRepoRuntimeError && error.code === "20727") {
1338
- console.error("Record not found");
1339
- } else if (error instanceof VSRepoError) {
1340
- console.error("Repository error:", error.message);
1341
- } else {
1342
- console.error("Error:", error.message)
1343
- }
808
+ if (error instanceof VSRepoError) {
809
+ console.error(`[${error.type}] ${error.message}`);
810
+ }
1344
811
  }
1345
812
  ```
1346
813
 
1347
- **Subclasses disponíveis:**
1348
-
1349
- | Classe | Quando é lançada |
1350
- | ------------------------- | -------------------------------------------------------------------- |
1351
- | `VSRepoConfigError` | Configuração inválida em `setupVSRepo` |
1352
- | `VSRepoBuildError` | Nome de método, tipo de campo ou configuração inválida em `build` |
1353
- | `VSRepoExtendError` | Argumento inválido em `extend` |
1354
- | `VSRepoDecoratorError` | Argumento inválido passado para `@DynamicMethod` ou `@QueryMethod` |
1355
- | `VSRepoRuntimeError` | Erro em tempo de execução durante uma operação |
1356
-
1357
- `VSRepoRuntimeError` tem uma propriedade `code: VSRepoRuntimeErrorCode` para identificação programática, sem precisar fazer parsing da mensagem (legível por humanos, e possivelmente localizada):
1358
-
1359
- | Código | Significado |
1360
- | ------ | ----------- |
1361
- | `"65706"` | Um argumento obrigatório está ausente ou tem formato inválido — ex.: `pk` ausente, `pks`/`objs`/`tuples` que não são um array, ou `options`/`obj` que não são um objeto válido. |
1362
- | `"20727"` | Nenhum registro foi encontrado para a primary key informada (`getOrThrow` ao buscar o registro base). |
1363
- | `"67542"` | A validação (zod) de `options` de um método, ou de um argumento de `@QueryMethod`, falhou. |
1364
- | `"91868"` | Uma relação passada em `save`/`patch`/`merge` tem formato inválido para o `mode`/`restriction` configurados (ex.: `null` numa relação `mtm`/`otm`, ou um array numa relação `oto`/`mto`). |
1365
- | `"48670"` | Um método dinâmico (`config.methods`) foi chamado com menos argumentos posicionais do que os campos do `where` exigem. |
1366
-
1367
- ---
1368
-
1369
- ## Tipos utilitários
814
+ | `VSRepoErrorType` | Quando é lançado |
815
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
816
+ | `DECORATOR` | Argumentos inválidos foram passados para `@DynamicMethod` ou `@QueryMethod`. |
817
+ | `RESOLVER` | A biblioteca falhou ao resolver a configuração de um método dinâmico/de query em um método chamável (ex.: um nome de método desconhecido). |
818
+ | `DYNAMIC` | Um método dinâmico já resolvido falhou em tempo de execução (ex.: argumentos faltando). |
819
+ | `VALIDATOR` | Options ou argumentos de método inválidos foram detectados durante a validação. |
820
+ | `BASE` | Uso inválido de um método base (`get`, `save`, `remove`, etc). |
821
+ | `ADAPTER` | Um `VSRepoAdapter` falhou ao falar com o ORM/banco subjacente — sempre é lançado como `VSRepoAdapterError`. |
1370
822
 
1371
- ### Tipos de client
823
+ ### `VSRepoAdapterError` e `AdapterErrorCode`
1372
824
 
1373
- ```ts
1374
- import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
1375
-
1376
- type DbClient = PrismaClient;
1377
- type DbTransaction = Prisma.TransactionClient;
1378
- type ClientOrTransaction = DbClient | DbTransaction;
1379
- ```
825
+ Quando um adapter fala com o ORM/banco subjacente e essa operação falha, o adapter encapsula a falha em um `VSRepoAdapterError` — uma subclasse de `VSRepoError` com `type: VSRepoErrorType.ADAPTER`. Ele carrega um `code: AdapterErrorCode` **estável e agnóstico de adapter** além do erro bruto lançado pelo ORM/driver, para que quem chama possa reagir às falhas sem depender do formato de erro de nenhum ORM específico:
1380
826
 
1381
- ### Tipo de visibilidade do soft-delete
827
+ ```typescript
828
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1382
829
 
1383
- ```ts
1384
- import type { SeeMode } from "../../generated/vsrepo";
1385
-
1386
- type SeeMode = "active" | "removed" | "all";
1387
- ```
1388
-
1389
- ### Tipos derivados do modelo Prisma
830
+ try {
831
+ await userRepository.save({ name: "Maria" });
832
+ } catch (error) {
833
+ if (error instanceof VSRepoAdapterError) {
834
+ console.error(`[${error.code}] ${error.message}`, error.originalError);
1390
835
 
1391
- ```ts
1392
- import type {
1393
- SelectModel,
1394
- SelectModels,
1395
- IncludeModel,
1396
- IncludeModels,
1397
- WhereModel,
1398
- OrderingModel,
1399
- PaginationModel,
1400
- ModelUpsertInput,
1401
- PrismaModelInputs,
1402
- } from "../../generated/vsrepo";
836
+ if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
837
+ // tratar chave duplicada, ex.: retornar uma mensagem amigável
838
+ }
839
+ }
840
+ }
1403
841
  ```
1404
842
 
1405
- ### Tipos de opções de método
843
+ | Propriedade | Tipo | Descrição |
844
+ | --------------- | ------------------ | --------------------------------------------------------------------------------- |
845
+ | `code` | `AdapterErrorCode` | Código estável e agnóstico que classifica a falha. |
846
+ | `originalError` | `unknown` | O erro bruto (ou `null`/`undefined`) lançado pelo driver do ORM/banco subjacente. |
847
+ | `message` | `string` | Descrição legível da falha do adapter. |
848
+ | `type` | `VSRepoErrorType` | Sempre `VSRepoErrorType.ADAPTER`. |
849
+ | `cause` | `unknown` | Causa raiz opcional da qual o erro foi encadeado. |
1406
850
 
1407
- ```ts
1408
- import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
851
+ As implementações de adapter o constroem diretamente ao mapear uma falha do ORM:
1409
852
 
1410
- // MethodOptions<S, IM> — opções passadas para os métodos do repositório
1411
- type Opts = MethodOptions<"public" | "minimal", "withPosts">;
853
+ ```typescript
854
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1412
855
 
1413
- // MethodOptionsModel<TRepo> — derivado de uma instância configurada do VSRepository
1414
- const userVSRepo = setupVSRepo<User, "user">()(config);
1415
- type OptsModel = MethodOptionsModel<typeof userVSRepo>;
856
+ throw new VSRepoAdapterError(
857
+ "falha ao criar o usuário",
858
+ AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
859
+ originalError, // erro bruto do banco/driver
860
+ );
1416
861
  ```
1417
862
 
1418
- > O segundo parâmetro de `MethodOptions` (`IM`) representa as chaves válidas de `includeModels`. Quando informado, `selectModel` e `includeModel` se tornam mutuamente exclusivos no tipo — não é possível passar os dois na mesma chamada.
1419
- >
1420
- > `MethodOptions` também aceita mais dois parâmetros genéricos, `RI` e `RS`, para tipar `include` e `select` brutos respectivamente (ambos com padrão `never`, ou seja, não são aceitos a menos que tipados explicitamente): `MethodOptions<"public", "withPosts", "usuario", IncludeModel<"usuario">, SelectModel<"usuario">>`. O `MethodOptionsModel`, derivado diretamente de um repositório configurado, não expõe `RI`/`RS` — use `MethodOptions` diretamente se precisar tipar as opções de `include`/`select` brutos.
863
+ #### `AdapterErrorCode`
1421
864
 
1422
- ### Tipos de configuração
865
+ `AdapterErrorCode` é um enum de códigos granulares e agnósticos que um adapter pode lançar através de `VSRepoAdapterError`. Eles espelham as falhas mais comuns lançadas por ORMs e drivers de banco, para que erros de qualquer ORM possam ser mapeados para o mesmo código estável:
1423
866
 
1424
- ```ts
1425
- import type {
1426
- MethodConfig,
1427
- RepoConfig,
1428
- BuildConfig,
1429
- RepositoryRelations,
1430
- ExtractRelationConfig,
1431
- } from "../../generated/vsrepo";
1432
- ```
1433
-
1434
- ### Tipo do repositório construído
1435
-
1436
- ```ts
1437
- import type { RepositoryOf } from "../../generated/vsrepo";
867
+ ```typescript
868
+ import { AdapterErrorCode } from "vsrepo";
1438
869
 
1439
- const userVSRepo = setupVSRepo<User, "user">()({ ... });
1440
- type UserRepository = RepositoryOf<typeof userVSRepo>;
870
+ console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
1441
871
  ```
1442
872
 
1443
- `RepositoryOf` aceita três parâmetros:
873
+ | Código | Significado |
874
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
875
+ | `UNKNOWN` | Erro não classificado/desconhecido; o fallback quando nenhum código mais específico corresponde. |
876
+ | `MISSING_DB_CLIENT` | Cliente de banco (ou pool de conexões) não fornecido ou que não pôde ser resolvido. |
877
+ | `CONNECTION_FAILED` | Não foi possível alcançar/conectar ao banco, ou uma conexão estabelecida foi perdida/terminada. |
878
+ | `CONNECTION_POOL_EXHAUSTED` | Pool de conexões esgotado/depletado — nenhuma conexão disponível, todas ocupadas ou o limite foi atingido. |
879
+ | `TIMEOUT` | O banco não respondeu a tempo; uma query excedeu o timeout permitido. |
880
+ | `UNIQUE_CONSTRAINT_VIOLATION` | Violação de constraint unique (chave duplicada). Ex.: Postgres/SQLite `23505`, MySQL `1062`. |
881
+ | `FOREIGN_KEY_VIOLATION` | Violação de constraint de foreign key (linha referenciada não existe). |
882
+ | `NOT_NULL_VIOLATION` | Violação de constraint NOT NULL. |
883
+ | `CHECK_VIOLATION` | Violação de constraint CHECK. |
884
+ | `CONSTRAINT_VIOLATION` | Violação geral de integridade/constraint não coberta por um código mais específico. |
885
+ | `NOT_FOUND` | Registro solicitado não encontrado (ex.: uma operação tipo `findOneOrThrow`). |
886
+ | `INVALID_DATA` | Valor de campo inválido para o tipo/tamanho, ou um valor obrigatório ausente. |
887
+ | `VALUE_TOO_LONG` | Valor fornecido excede o limite de tamanho da coluna/campo. |
888
+ | `CONVERSION_ERROR` | Um valor não pôde ser convertido/convertido para o tipo alvo. Ex.: Postgres `22P02`, MySQL `1366`. |
889
+ | `INVALID_QUERY` | A query/stored procedure SQL está malformada ou é inválida. |
890
+ | `TABLE_OR_COLUMN_NOT_FOUND` | A tabela/coluna/relação referenciada não existe. |
891
+ | `DEADLOCK` | Operação abortada por timeout de lock ou deadlock entre transações concorrentes. |
892
+ | `LOCK_TIMEOUT` | Não foi possível adquirir um lock de banco obrigatório a tempo. |
893
+ | `LOCKED` | O registro está travado e não pode ser modificado. |
894
+ | `ACCESS_DENIED` | O usuário/role atual não tem permissão para a operação. |
895
+ | `INVALID_CREDENTIALS` | Credenciais de conexão inválidas (host/usuário/senha). |
896
+ | `ROW_NOT_ALLOWED` | O usuário autenticado não é dono do registro / a segurança em nível de linha rejeitou. |
897
+ | `MODEL_NOT_FOUND` | Entidade/modelo ou tabela não definida/mapeada no ORM, ou o adapter não tem os metadados do modelo. |
898
+ | `FIELD_NOT_FOUND` | Nome de campo/coluna nos dados ou no `where` não existe na entidade/modelo. |
899
+ | `TRANSACTION_CLOSED` | Transação usada depois de commit/rollback. |
900
+ | `TRANSACTION_ALREADY_STARTED` | Uma transação aninhada não pôde ser aberta (ex.: chamadas `transaction()` aninhadas). |
901
+ | `TRANSACTION_CONFLICT` | Uma transação falhou ao commitar e foi desfeita. |
902
+ | `TRANSACTION_NOT_STARTED` | Nenhuma transação ativa quando uma era obrigatória. |
903
+ | `CONNECTION_CLOSED` | Conexão fechada/terminada enquanto uma transação ou query estava em andamento. |
904
+ | `INVALID_PARTIAL` | `merge`/`upsert`/`update` recebeu um objeto parcial inválido ou faltando chaves obrigatórias. |
905
+ | `NOT_SUPPORTED` | Feature/operação não suportada solicitada ao adapter (ex.: `query()` bruto não suportado). |
906
+ | `INVALID_ADAPTER_CONFIG` | Configuração do adapter inválida ou incompleta (options obrigatórias ausentes, ou com tipo/valor inválido). |
907
+ | `INTERNAL` | Bug interno do adapter ou estado irrecuperável; deve raramente ser usado — prefira um código mais específico. |
1444
908
 
1445
- ```ts
1446
- type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
1447
- ```
909
+ #### `VSRepoError` vs. erros brutos do ORM
1448
910
 
1449
- ### Tipos de payload para `save` e `patch`
911
+ Erros de uso/configuração fora do adapter lançam o `VSRepoError` base. Falhas lançadas _pelo ORM subjacente_ enquanto um método do adapter roda são **encapsuladas** em `VSRepoAdapterError` (classificadas por um `AdapterErrorCode`, com o erro original preservado em `originalError`) em vez de se propagarem cruas — é isso que torna quem chama independente do formato de erro de qualquer ORM específico.
1450
912
 
1451
- ```ts
1452
- import type { SaveObject, PatchObject } from "../../generated/vsrepo";
913
+ ---
1453
914
 
1454
- const userVSRepo = setupVSRepo<User, "user">()(({
1455
- tableName: "user",
1456
- pkName: "id",
1457
- relations: {
1458
- profile: { pk: "id", mode: "oto", restriction: "set" },
1459
- },
1460
- });
915
+ ## Logging
1461
916
 
1462
- type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
1463
- type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
1464
- ```
917
+ Todo repository tem um logger interno, configurado via `logLevel` e `logSlowThresholdMs` nas options do construtor:
1465
918
 
1466
- ---
919
+ ```typescript
920
+ import { VSLogLevel } from "vsrepo";
1467
921
 
1468
- ## Referência da API
1469
-
1470
- ### `setupVSRepo<T, M>()(config)`
1471
-
1472
- ```ts
1473
- setupVSRepo<TPayload, TTableName>()({
1474
- tableName: Uncapitalize<M>; // Nome da tabela no Prisma
1475
- pkName: keyof T; // Nome da chave primária
1476
- softRemovekName?: keyof T & string; // Campo DateTime para soft-delete
1477
- selectModels?: SelectModels<M>; // Projeções de dados nomeadas (select)
1478
- defaultSelectModel?: keyof SM; // Select aplicado por padrão
1479
- includeModels?: IncludeModels<M>; // Projeções de dados nomeadas (include) — sem padrão, apenas na chamada
1480
- requiredWhere?: WhereModel<M>; // Filtros sempre aplicados
1481
- defaultOrdering?: OrderingModel<M>; // Ordenação padrão para queries sem Ordered/injectOrdering
1482
- relations?: RepositoryRelations<T>; // Configuração de relações
1483
- methods?: Record<string, MethodConfig<M, SM>>; // Métodos dinâmicos
922
+ super({
923
+ pkName: "id",
924
+ adapter,
925
+ logLevel: VSLogLevel.DEBUG,
926
+ logSlowThresholdMs: 200,
1484
927
  });
1485
928
  ```
1486
929
 
1487
- ### `.build(prisma, config?)`
1488
-
1489
- ```ts
1490
- vsRepo.build(prisma, {
1491
- showWorking?: boolean; // Exibe os logs internos no console (padrão = false)
1492
-
1493
- baseMethods?: {
1494
- // Métodos que podem usar um defaultSelect
1495
- get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1496
- getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1497
- getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1498
- remove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1499
- save?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1500
- saveList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1501
- patch?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1502
- patchList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1503
- merge?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1504
- getAll?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1505
- softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1506
- restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
1507
-
1508
- // Métodos que NÃO aceitam defaultSelect
1509
- removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1510
- softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1511
- restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
1512
- total?: { active?: boolean; ignoreRequiredWhere?: boolean };
1513
- has?: { active?: boolean; ignoreRequiredWhere?: boolean };
1514
- };
1515
- });
1516
- ```
930
+ | Nível | Significado |
931
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
932
+ | `DEBUG` | Detalhes internos verbosos, incluindo toda query resolvida — muito útil para debugar métodos dinâmicos. |
933
+ | `INFO` | Eventos de alto nível do ciclo de vida, como a inicialização do repository. |
934
+ | `WARN` (padrão) | Problemas recuperáveis e operações lentas (veja `logSlowThresholdMs`, padrão de 300ms). |
935
+ | `ERROR` | Falhas lançadas durante a execução de uma operação. |
1517
936
 
1518
- ### `.extend(fn)`
937
+ ---
1519
938
 
1520
- ```ts
1521
- repo.extend((repo) => ({
1522
- myMethod: () => { ... }
1523
- }));
1524
- ```
939
+ ## Desenvolvimento
1525
940
 
1526
- ---
941
+ O core da v2 é compilado e empacotado a partir desta branch como um pacote npm padrão:
1527
942
 
1528
- ## Exemplos práticos
1529
-
1530
- Além deste README, o repositório tem uma pasta **[`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples)** com exemplos práticos, comentados e prontos para executar — é o melhor lugar para ver o VSRepository sendo usado em cenários reais.
1531
-
1532
- ```text
1533
- examples/
1534
- ├── prisma.ts # Instância do PrismaClient usada pelos exemplos
1535
- ├── repositories.ts # Configuração dos repositórios (User, Address, Product) com setupVSRepo
1536
- └── tests/
1537
- ├── base-methods.test.ts # Métodos base: get, save, patch, remove, getAll, total, has...
1538
- ├── relations.test.ts # Como configurar e usar relações em save/patch e em filtros
1539
- ├── required-where.test.ts # Como requiredWhere é aplicado automaticamente às queries
1540
- ├── dynamic-methods.test.ts # Prefixos, filtros de campo, operadores lógicos e paginação/ordenação
1541
- ├── transactions.test.ts # Transações com options.db e acesso à instância via repository.prisma
1542
- ├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList e SeeMode
1543
- └── batch-methods.test.ts # Operações em lote: getList, saveList, patchList e merge
1544
- ```
943
+ ```bash
944
+ # 1. Instalar as dependências
945
+ pnpm install
1545
946
 
1546
- Cada arquivo em `tests/` é um script independente e executável (via `tsx`) que demonstra um conjunto específico de funcionalidades, com `console.log` em cada etapa para você acompanhar o resultado no terminal. A própria pasta tem um [README](https://github.com/jaobrabo123/VSRepository/blob/main/examples/README.md) explicando a ordem de leitura sugerida, como configurar o ambiente e como rodar os testes.
947
+ # 2. Compilar os fontes TypeScript em dist/ (remove um dist/ anterior primeiro)
948
+ pnpm build
1547
949
 
1548
- ---
950
+ # 3. (Opcional) Inspecionar o que seria publicado sem gerar um tarball
951
+ npm pack --dry-run
1549
952
 
1550
- ## Contribuindo
953
+ # 4. Gerar o tarball instalável (roda `prepack` -> `pnpm build` automaticamente)
954
+ npm pack
1551
955
 
1552
- Contribuições são bem-vindas! Se você encontrou um bug, tem uma ideia de melhoria ou quer ajudar com a documentação, sinta-se à vontade para participar (**[Repositório no GitHub](https://github.com/jaobrabo123/VSRepository)**):
956
+ # 5. Consumir localmente em outro projeto
957
+ npm install ../caminho/vsrepo-1.4.0.tgz
958
+ ```
1553
959
 
1554
- 1. Faça um **fork** do projeto.
1555
- 2. Crie uma nova branch com sua alteração: `git checkout -b fixing-bug`.
1556
- 3. Envie para sua branch: `git push origin fixing-bug`.
1557
- 4. Abra um **Pull Request**.
960
+ Observações:
1558
961
 
1559
- Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
962
+ - `pnpm build` executa `tsc -p tsconfig.build.json`, que gera o JS compilado e as declarações de tipo em `dist/` com `rootDir: src`.
963
+ - O pacote publicado contém **apenas** a pasta `dist/` além dos READMEs e da `LICENSE` (veja `files` no `package.json`). Fontes, testes, a pasta `v1/` e `generated/` **não** são enviados — os adapters viverão em seus próprios pacotes `@vsrepo/*-adapter`.
964
+ - O core é ORM-agnóstico e não tem dependência peer de `@prisma/client`.
1560
965
 
1561
966
  ---
1562
967
 
1563
968
  ## Requisitos
1564
969
 
1565
- - Node.js 18+ (ESM)
1566
- - Prisma
1567
- - TypeScript (opcional, mas fortemente recomendado)
1568
- - `"moduleResolution": "bundler"` ou `"nodenext"` no tsconfig
1569
-
1570
- `tsconfig.json` recomendado:
970
+ - Node.js 18+
971
+ - TypeScript, com **decorators legacy/experimentais** habilitados (necessário para `@DynamicMethod`/`@QueryMethod`):
1571
972
 
1572
973
  ```json
1573
974
  {
1574
- "compilerOptions": {
1575
- "target": "ES2020",
1576
- "module": "NodeNext",
1577
- "moduleResolution": "NodeNext",
1578
- "strict": true,
1579
- "skipLibCheck": true,
1580
- "lib": ["ES2020"]
1581
- }
975
+ "compilerOptions": {
976
+ "experimentalDecorators": true
977
+ }
1582
978
  }
1583
979
  ```
1584
980
 
1585
- ---
1586
-
1587
- ## Solução de problemas
1588
-
1589
- **Tipos genéricos não são inferidos** — Verifique se `strict: true` e `moduleResolution: "bundler"` ou `"nodenext"` estão configurados no `tsconfig.json`.
1590
-
1591
- **Método dinâmico não existe em tempo de execução** — O campo referenciado no nome do método deve existir no modelo do Prisma. Ex.: `findByEmail` requer que o modelo tenha um campo `email`.
981
+ - `reflect-metadata` (já incluso como dependência, importado internamente — você não precisa importá-lo você mesmo)
982
+ - Pelo menos um `VSRepoAdapter` funcional para o seu banco — no Prisma 7, instale o [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) já publicado (veja [Status dos adapters](#status-dos-adapters)); adapters oficiais para outros ORMs estão planejados, mas ainda não publicados, então por enquanto isso significa escrever o seu próprio (veja [Escrevendo seu próprio adapter](#escrevendo-seu-próprio-adapter)) — e, se publicá-lo, contribuir de volta com o projeto é bem-vindo
1592
983
 
1593
- **`proxyTo` obrigatório** — Nomes fora dos padrões conhecidos (ex.: `searchByEmail`) não são interpretados diretamente. Use `proxyTo: "findByEmail"` nesses casos.
1594
-
1595
- **Select model retorna campos inesperados** — Verifique se o select model define exatamente os campos que o seu tipo TypeScript espera.
1596
-
1597
- **`selectModel`, `includeModel` e `include` juntos na mesma chamada** — Não é permitido. Apenas um dos três pode ser informado por chamada: se `includeModel` ou `include` for informado, o `select` (incluindo `defaultSelectModel`) é ignorado e apenas o `include` é enviado ao Prisma.
1598
-
1599
- **`includeModel` não aparece como opção padrão do repositório** — Isso é esperado. Diferente do `defaultSelectModel`, não existe `defaultIncludeModel`/`defaultInclude`. Um `includeModel` só pode ser definido na chamada do método, via `options.includeModel`. Um include bruto e ad hoc pode ser definido via `options.include`, sem precisar ser registrado em `includeModels`.
984
+ ---
1600
985
 
1601
- **`softRemovekName` lança um erro no build** — O campo informado deve ser do tipo `DateTime` no schema do Prisma. Tipos como `Boolean` ou `String` não são aceitos.
986
+ ## Contribuindo
1602
987
 
1603
- **`defaultOrdering` não está sendo aplicado** — Verifique se o método usa o sufixo `Ordered`, `OrderedAndPaginated` ou `PaginatedAndOrdered`, e se tem `injectOrdering` configurado. Ambos têm prioridade sobre a ordenação padrão.
988
+ Contribuições são bem-vindas, especialmente para finalizar os adapters do Prisma e do TypeORM! (**[Repositório do GitHub](https://github.com/jaobrabo123/VSRepository)**):
1604
989
 
1605
- **Sufixo `Distinct` não é reconhecido** — `Distinct` só é resolvido em prefixos de leitura (`findMany`, `findFirst`, `findBy`, `existsBy`, etc). Em métodos como `count`, `createMany`, `updateMany` ou `deleteMany` o sufixo é ignorado.
990
+ 1. Faça um **Fork** do projeto.
991
+ 2. Crie uma branch a partir de `v2` para sua alteração: `git checkout -b v2-minha-alteracao`.
992
+ 3. Faça o push da sua branch: `git push origin v2-minha-alteracao`.
993
+ 4. Abra um **Pull Request** contra a `v2`.
1606
994
 
1607
- **`saveList`/`patchList` com `db` inválido** — O campo `db` nesses métodos só aceita uma `DbTransaction` (o retorno de `prisma.$transaction`), não o client principal. Passar o `PrismaClient` diretamente causará comportamento inesperado.
995
+ Para reportar problemas ou sugerir funcionalidades, abra uma **Issue**.