vsrepo 1.4.2 → 2.1.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 (151) hide show
  1. package/README.md +827 -1322
  2. package/README.pt-BR.md +833 -1325
  3. package/dist/VSRepoAdapter.d.ts +109 -0
  4. package/dist/VSRepoAdapter.js +18 -0
  5. package/dist/VSRepository.d.ts +166 -1201
  6. package/dist/VSRepository.js +327 -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 +38 -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 +40 -0
  46. package/dist/internal/validators/vsrepo.validator.js +182 -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/decimal-like.type.d.ts +23 -0
  56. package/dist/types/utils/deep-partial.type.d.ts +14 -0
  57. package/dist/types/utils/keys-of-type.type.d.ts +20 -0
  58. package/dist/types/utils/methods-options.type.d.ts +23 -0
  59. package/dist/types/utils/numeric-keys.type.d.ts +24 -0
  60. package/dist/types/utils/numeric-like.type.d.ts +10 -0
  61. package/dist/types/utils/ordering.type.d.ts +39 -0
  62. package/dist/types/utils/pagination.type.d.ts +11 -0
  63. package/dist/types/utils/perform-data.type.d.ts +4 -0
  64. package/dist/types/utils/primitive.type.d.ts +7 -0
  65. package/dist/types/utils/query-method-arg.type.d.ts +27 -0
  66. package/dist/types/utils/restrict-method-options.type.d.ts +14 -0
  67. package/dist/types/utils/see-mode.type.d.ts +12 -0
  68. package/dist/types/vsrepo/vsrepo-args.type.d.ts +9 -0
  69. package/dist/types/vsrepo/vsrepo-method.type.d.ts +4 -0
  70. package/dist/types/vsrepo/vsrepo-method.type.js +2 -0
  71. package/dist/types/vsrepo/vsrepo-options.type.d.ts +34 -0
  72. package/dist/types/vsrepo/vsrepo-options.type.js +2 -0
  73. package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
  74. package/dist/types/vsrepo/vsrepo-orm-types.type.js +2 -0
  75. package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -0
  76. package/dist/types/vsrepo/vsrepo-pretty-where.type.js +2 -0
  77. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +17 -0
  78. package/dist/types/vsrepo/vsrepo-query-options.type.js +2 -0
  79. package/dist/types/vsrepo/vsrepo-query.type.d.ts +5 -0
  80. package/dist/types/vsrepo/vsrepo-query.type.js +2 -0
  81. package/dist/types/vsrepo/vsrepo-relations.type.d.ts +26 -0
  82. package/dist/types/vsrepo/vsrepo-relations.type.js +2 -0
  83. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.d.ts +19 -0
  84. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.js +2 -0
  85. package/dist/types/vsrepo/vsrepo-select.type.d.ts +15 -0
  86. package/dist/types/vsrepo/vsrepo-select.type.js +2 -0
  87. package/dist/types/vsrepo/vsrepo-transaction-options.type.d.ts +12 -0
  88. package/dist/types/vsrepo/vsrepo-transaction-options.type.js +2 -0
  89. package/dist/types/vsrepo/vsrepo-ugly-where.type.d.ts +9 -0
  90. package/dist/types/vsrepo/vsrepo-ugly-where.type.js +2 -0
  91. package/dist/types/vsrepo/vsrepo-where.type.d.ts +99 -0
  92. package/dist/types/vsrepo/vsrepo-where.type.js +2 -0
  93. package/package.json +16 -37
  94. package/README-DynamicRepo.md +0 -625
  95. package/README-DynamicRepo.pt-BR.md +0 -625
  96. package/dist/DynamicRepository.d.ts +0 -497
  97. package/dist/DynamicRepository.js +0 -26
  98. package/dist/VSRepoError.d.ts +0 -83
  99. package/dist/VSRepoError.js +0 -17
  100. package/dist/internal/decorators/dynamic-method.decorator.js +0 -14
  101. package/dist/internal/decorators/query-method.decorator.js +0 -20
  102. package/dist/internal/entities/dynamic-method-metadata.entity.js +0 -26
  103. package/dist/internal/errors/vs-repo.error.js +0 -31
  104. package/dist/internal/resolvers/base-methods.resolve.js +0 -541
  105. package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +0 -143
  106. package/dist/internal/resolvers/data-payload-with-relations.resolve.js +0 -60
  107. package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +0 -63
  108. package/dist/internal/resolvers/dynamic-method-customization.resolve.js +0 -57
  109. package/dist/internal/resolvers/dynamic-method-info.resolve.js +0 -279
  110. package/dist/internal/resolvers/dynamic-methods-metadata.resolve.js +0 -15
  111. package/dist/internal/resolvers/merge-wheres.resolve.js +0 -22
  112. package/dist/internal/resolvers/pretty-wheres.resolve.js +0 -87
  113. package/dist/internal/resolvers/select.resolve.js +0 -7
  114. package/dist/internal/resolvers/specific-where.resolve.js +0 -84
  115. package/dist/internal/resolvers/ugly-where.resolve.js +0 -178
  116. package/dist/internal/utils/logger.util.js +0 -21
  117. package/dist/internal/utils/schemas.util.js +0 -31
  118. package/dist/internal/validation/build-config.validate.js +0 -84
  119. package/dist/internal/validation/constructor-config.validate.js +0 -64
  120. package/dist/internal/validation/dynamic-method-config.validate.js +0 -19
  121. package/dist/internal/validation/extension.validate.js +0 -15
  122. package/dist/internal/validation/is-object.validate.js +0 -6
  123. package/dist/internal/validation/method-options.validate.js +0 -42
  124. package/dist/internal/validation/obj-with-relations.validate.js +0 -37
  125. package/dist/internal/validation/prisma-client.validate.js +0 -10
  126. package/dist/internal/validation/query-method-arg.validate.js +0 -22
  127. package/dist/internal/validation/query-method-options.validate.js +0 -24
  128. package/scripts/configure-prisma-import.mjs +0 -283
  129. package/scripts/copy-types.mjs +0 -24
  130. /package/dist/{internal/decorators/types/dynamic-method-config.type.js → types/adapter/adapter-method-options.type.js} +0 -0
  131. /package/dist/{internal/errors/types/vs-repo-error-type.type.js → types/adapter/adapter-query-options.type.js} +0 -0
  132. /package/dist/{internal/errors/types/vs-repo-runtime-error-code.type.js → types/decorators/dynamic-method-options.type.js} +0 -0
  133. /package/dist/{internal/validation/types → types/decorators}/query-method-options.type.js +0 -0
  134. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-customization.type.js +0 -0
  135. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-info.type.js +0 -0
  136. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-where-ops.type.js +0 -0
  137. /package/dist/{internal/resolvers/types/base-method-function.type.js → types/utils/count-result.type.js} +0 -0
  138. /package/dist/{internal/resolvers/types/pretty-where.type.js → types/utils/decimal-like.type.js} +0 -0
  139. /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/deep-partial.type.js} +0 -0
  140. /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/keys-of-type.type.js} +0 -0
  141. /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/methods-options.type.js} +0 -0
  142. /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/numeric-keys.type.js} +0 -0
  143. /package/dist/{internal/validation/types/base-methods.type.js → types/utils/numeric-like.type.js} +0 -0
  144. /package/dist/{internal/validation/types/build-config.type.js → types/utils/ordering.type.js} +0 -0
  145. /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
  146. /package/dist/{internal/validation/types/constructor-config.type.js → types/utils/perform-data.type.js} +0 -0
  147. /package/dist/{internal/validation/types/method-options.type.js → types/utils/primitive.type.js} +0 -0
  148. /package/dist/{internal/validation/types → types/utils}/query-method-arg.type.js +0 -0
  149. /package/dist/{internal/validation/types/method.type.js → types/utils/restrict-method-options.type.js} +0 -0
  150. /package/dist/{internal/validation/types → types/utils}/see-mode.type.js +0 -0
  151. /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
package/README.md CHANGED
@@ -9,139 +9,127 @@
9
9
  </p>
10
10
  </div>
11
11
 
12
- # VSRepository
12
+ # VSRepository v2
13
13
 
14
14
  🇺🇸 You're reading the English version. [🇧🇷 Ler em português](./README.pt-BR.md)
15
15
 
16
- Repository pattern library for projects using **Prisma**, with full **TypeScript** support and automatic **type inference**.
16
+ > ✅ **Released.** VSRepository v2.0.0 (the ORM-agnostic core) and the [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) are both published and ready to use. Prisma 7 is the first fully supported adapter; other ORMs (TypeORM, Drizzle, etc.) are still in progress — see [Adapter status](#adapter-status). If you need the previous Prisma-only release, use the [`v1`](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs instead.
17
+
18
+ **ORM-agnostic** repository pattern library, with full **TypeScript** support and automatic **type inference**. VSRepository v2 is a rewrite of the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) library: instead of talking to Prisma directly, the core now delegates every operation to a pluggable **adapter**, so the same repository API can work against Prisma, TypeORM, or any other ORM/database that implements the adapter contract.
17
19
 
18
20
  VSRepository lets you create strongly-typed repositories with:
19
21
 
20
- - Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
22
+ - Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `merge`, `getAll`, `total`, `has`
21
23
  - **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
22
- - **Dynamic methods** inferred from their name: `findOneByEmail`, `findManyPaginated`, `updateById`, `deleteManyByNameStartsWith`
23
- - Reusable **select models** for different data projections
24
+ - **Dynamic methods** inferred from a `declare` field name via the `@DynamicMethod` decorator: `findByEmail`, `findManyByStatusPaginated`, `updateById`
25
+ - **Raw SQL query methods** via the new `@QueryMethod` decorator, bypassing the name-parsing engine entirely
26
+ - Ad-hoc **`select`/`relations`** per call — no more pre-declared named projections
24
27
  - **Type safety** across 100% of operations
25
- - Native Prisma **transactions** (automatic in `saveList` and `patchList`)
26
- - **Extensibility** with custom methods
27
-
28
- > 💡 Want to see all of this in practice? The repository's [`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples) folder has commented, runnable examples for every feature — see the [Practical examples](#practical-examples) section below.
28
+ - Native ORM **transactions**, shared across repositories
29
+ - An **ORM-agnostic core** — the same repository class works with any `VSRepoAdapter` implementation
29
30
 
30
31
  ---
31
32
 
32
33
  ## Table of contents
33
34
 
35
+ - [What changed from v1](#what-changed-from-v1)
36
+ - [Adapter status](#adapter-status)
34
37
  - [Installation](#installation)
35
- - [Generating the types](#generating-the-types)
36
38
  - [Basic usage](#basic-usage)
37
- - [Class-based approach (DynamicRepository)](#class-based-approach-dynamicrepository)
38
- - [NestJS integration](#nestjs-integration)
39
+ - [Constructor options](#constructor-options)
39
40
  - [Base methods](#base-methods)
40
- - [Soft-delete](#soft-delete)
41
- - [Batch operations](#batch-operations)
42
- - [Merge](#merge)
43
- - [Configuring the base methods](#configuring-the-base-methods)
44
- - [Select Models](#select-models)
45
- - [Raw select (options.select)](#raw-select-optionsselect)
46
- - [Include Models](#include-models)
47
- - [Raw include (options.include)](#raw-include-optionsinclude)
48
- - [Required Where](#required-where)
49
- - [Default Ordering](#default-ordering)
50
- - [`see` option](#see-option)
41
+ - [Soft-delete](#soft-delete)
42
+ - [Atomic and aggregate methods](#atomic-and-aggregate-methods)
43
+ - [Which fields are eligible](#which-fields-are-eligible)
44
+ - [Writing an adapter](#writing-an-adapter)
45
+ - [`select` and `relations`](#select-and-relations)
51
46
  - [Dynamic methods](#dynamic-methods)
52
- - [Available prefixes](#available-prefixes)
53
- - [Field filters](#field-filters)
54
- - [Logical operators](#logical-operators)
55
- - [Relation filters](#relation-filters)
56
- - [Pagination and ordering suffixes](#pagination-and-ordering-suffixes)
57
- - [Distinct](#distinct)
58
- - [Method configuration](#method-configuration)
59
- - [Aggregate and GroupBy](#aggregate-and-groupby)
60
- - [Query Methods](#query-methods)
61
- - [Relations in save](#relations-in-save)
47
+ - [Available prefixes](#available-prefixes)
48
+ - [Field filters](#field-filters)
49
+ - [Logical operators](#logical-operators)
50
+ - [Relation filters](#relation-filters)
51
+ - [Ordering, pagination and distinct](#ordering-pagination-and-distinct)
52
+ - [Decorator options](#decorator-options)
53
+ - [Query methods (raw SQL)](#query-methods-raw-sql)
54
+ - [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query)
62
55
  - [Transactions](#transactions)
63
- - [Extending a repository](#extending-a-repository)
64
- - [Error handling](#error-handling)
65
56
  - [Utility types](#utility-types)
66
- - [API Reference](#api-reference)
67
- - [Practical examples](#practical-examples)
68
- - [Contributing](#contributing)
57
+ - [Writing your own adapter](#writing-your-own-adapter)
58
+ - [Error handling](#error-handling)
59
+ - [`VSRepoAdapterError` and `AdapterErrorCode`](#vsrepoadaptererror-and-adaptererrorcode)
60
+ - [Logging](#logging)
61
+ - [Development](#development)
69
62
  - [Requirements](#requirements)
70
- - [Troubleshooting](#troubleshooting)
63
+ - [Contributing](#contributing)
71
64
 
72
65
  ---
73
66
 
74
- ## Installation
75
-
76
- ```bash
77
- npm i vsrepo @prisma/client
78
- ```
79
-
80
- Generate the Prisma Client:
81
-
82
- ```bash
83
- npx prisma generate
84
- ```
67
+ ## What changed from v1
68
+
69
+ If you're coming from the [v1](https://github.com/jaobrabo123/VSRepository/tree/v1) code/docs, here's the short version. See each linked section for details.
70
+
71
+ | Area | v1 | v2 |
72
+ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
73
+ | Database access | Talks to **Prisma** directly, bundled in the core package | Talks to a **`VSRepoAdapter`**; ORM support ships as separate packages (`@vsrepo/prisma7-adapter`, `@vsrepo/typeorm-adapter`, ...) instead of being bundled in the core `vsrepo` package |
74
+ | Defining a repository | Functional `setupVSRepo<T, M>()({...}).build(prisma)`, **or** a `DynamicRepository` class | A single **class-based** API: `extends VSRepository<Entity, PKType, OrmTypes>` |
75
+ | Dynamic methods | `methods: { findByEmail: { map: true } }` config object | `@DynamicMethod()` decorator on a `declare` field |
76
+ | Data projections | Named, reusable `selectModels` + `defaultSelectModel` | Ad-hoc `select`/`relations` passed per call (no named models) |
77
+ | Eager loading | `include`/`includeModels` (Prisma-specific) | ORM-agnostic `relations` option |
78
+ | Global filters | `requiredWhere` (any arbitrary filter, always applied) | **Removed**; Now it only accepts `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
79
+ | Case-insensitive filter suffix | `Insensitive` | `IgnoreCase` |
80
+ | Inline ordering in method name | Not supported (`order` had to be passed as an argument via `Ordered`/`Paginated`) | `OrderBy<Field>Asc`/`OrderBy<Field>Desc` chains baked directly into the method name |
81
+ | Duplicate handling on `createMany` | `SkipDuplicates` suffix | `IgnoreConflicts` suffix |
82
+ | `aggregate` / `groupBy` | Supported (Prisma-native passthrough) | **Not implemented yet** |
83
+ | Error types | `VSRepoError` + subclasses (`VSRepoConfigError`, `VSRepoBuildError`, `VSRepoExtendError`, `VSRepoRuntimeError`) | A base `VSRepoError` class with a `type: VSRepoErrorType` field (`DECORATOR`, `RESOLVER`, `DYNAMIC`, `VALIDATOR`, `BASE`, `ADAPTER`), plus a `VSRepoAdapterError` subclass carrying an `AdapterErrorCode` and the original ORM error |
84
+ | Debug logging | `showWorking: true` boolean | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` for slow-query warnings |
85
+ | `vsrepo generate` CLI (type generation step) | Required before use | Not part of the v2 core — types come directly from your entity/ORM types |
86
+ | CRUD extras | `patchList`, raw `options.select`/`options.include` | `select`/`relations` are the default (always "raw"); `patch`/`merge` keep the same semantics. **`patchList` was removed** — for a batch partial update, use a `updateManyBy`/`updateManyWhere` dynamic method instead |
85
87
 
86
88
  ---
87
89
 
88
- ## Generating the types
89
-
90
- VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
91
-
92
- ```bash
93
- npx vsrepo generate
94
- ```
90
+ ## Adapter status
95
91
 
96
- Equivalent to:
92
+ VSRepository v2 is **ORM-agnostic by design**. The core package (`vsrepo`) only ships the repository class, the decorators, the name-parsing engine, error handling and logging — it does **not** ship a production adapter. Actual ORM/database support is meant to live in **separate, independently versioned packages**, one per ORM (and, where it makes sense, one per major ORM version), for example:
97
93
 
98
- ```bash
99
- npx vsrepo generate \
100
- --output generated/vsrepo \
101
- --prisma generated/prisma
102
- ```
94
+ - `@vsrepo/prisma7-adapter`
95
+ - `@vsrepo/prisma8-adapter`
96
+ - `@vsrepo/typeorm-adapter`
97
+ - `@vsrepo/drizzle-adapter`
103
98
 
104
- **Available flags:**
99
+ The Prisma 7 adapter has now been published to npm as `@vsrepo/prisma7-adapter` — it's currently the **only** published adapter. Adapters for the other ORMs listed above (Prisma 8, TypeORM, Drizzle) are **planned**; they just haven't been published yet. Until an official `@vsrepo/*-adapter` package exists for your ORM, you're welcome to write your own for your project, and if you'd like, publish it and open a PR to help grow the ecosystem — contributions here are very welcome.
105
100
 
106
- | Flag | Alias | Default |
107
- | ---------- | ----- | -------------------- |
108
- | `--output` | `-o` | `generated/vsrepo` |
109
- | `--prisma` | `-p` | `generated/prisma` |
101
+ | Adapter | Status |
102
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. **Note:** the atomic/aggregate methods (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Atomic and aggregate methods](#atomic-and-aggregate-methods)) were added to the `VSRepoAdapter` contract after this adapter's last release; confirm its changelog/version implements them before relying on `increment`/`sum`/etc. against Prisma 7. |
104
+ | TypeORM (`@vsrepo/typeorm-adapter`) | 🟡 **Planned, not published yet.** Only a reference `where`-clause parser (`parseVSRepoWhere`) was written to validate the design; it's the planned starting point for the future `@vsrepo/typeorm-adapter` package. Community contributions toward this are welcome. |
105
+ | Other ORMs (Prisma 8, Drizzle, etc.) | 🟡 **Planned, not published yet.** No official package exists yet — write your own adapter for now (see [Writing your own adapter](#writing-your-own-adapter)), and consider publishing/contributing it back. |
106
+ | Custom adapters | 🟢 Fully supported today — implement the [`VSRepoAdapter`](#writing-your-own-adapter) abstract class yourself for any ORM/database you need, in your own project or package, following the same shape as `@vsrepo/*-adapter` is expected to have. |
110
107
 
111
- **Generated files:**
108
+ In short: the repository class, the `@DynamicMethod`/`@QueryMethod` decorators, the name-parsing engine, error handling and logging are all working end-to-end, and Prisma 7 support is now a released, published adapter. Official adapters for the remaining ORMs are on the roadmap and will ship as separate `@vsrepo/*-adapter` packages rather than as part of the core `vsrepo` package — but you don't have to wait for that: writing (and optionally publishing) your own adapter in the meantime is a fully supported way to use v2 today and to contribute back to the project.
112
109
 
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
- ```
110
+ ---
123
111
 
124
- After generating, always import from the generated folder:
112
+ ## Installation
125
113
 
126
- ```ts
127
- // CORRECT ✅
128
- import { setupVSRepo } from "../../generated/vsrepo";
114
+ v2 is installed as the core package plus one adapter package for your ORM, for example:
129
115
 
130
- // WRONG ❌
131
- import { setupVSRepo } from "vsrepo";
116
+ ```bash
117
+ npm i vsrepo @vsrepo/prisma7-adapter
132
118
  ```
133
119
 
120
+ > `vsrepo` v2.0.0 and `@vsrepo/prisma7-adapter` are both published to npm and ready to use. For any ORM other than Prisma 7, no adapter package exists yet — install the core and write your own adapter (see [Writing your own adapter](#writing-your-own-adapter)).
121
+
134
122
  ---
135
123
 
136
124
  ## Basic usage
137
125
 
138
- ### Configuring the Prisma Client
126
+ ### Implementing/choosing an adapter
139
127
 
140
- ```ts
128
+ ```typescript
141
129
  // src/configs/db.ts
142
- import { PrismaClient } from '../../generated/prisma/client';
143
- import { PrismaPg } from '@prisma/adapter-pg';
144
- import 'dotenv/config';
130
+ import { PrismaClient } from "../../generated/prisma/client";
131
+ import { PrismaPg } from "@prisma/adapter-pg";
132
+ import "dotenv/config";
145
133
 
146
134
  const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
147
135
  const prisma = new PrismaClient({ adapter });
@@ -151,1457 +139,974 @@ export default prisma;
151
139
 
152
140
  ### Creating a repository
153
141
 
154
- ```ts
155
- // src/repositories/userRepository.ts
142
+ ```typescript
143
+ // src/repositories/user.repository.ts
144
+ import { VSRepository, DynamicMethod } from "vsrepo";
145
+ import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
156
146
  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
- ```
147
+ import type { UserGetPayload } from "../../generated/prisma/models";
171
148
 
172
- ### Using the repository
149
+ type User = UserGetPayload<{ include: { address: true } }>;
173
150
 
174
- ```ts
175
- import userRepository from "./repositories/userRepository";
151
+ class UserRepository extends VSRepository<User, string> {
152
+ constructor() {
153
+ super({
154
+ pkName: "id",
155
+ adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
156
+ softRemoveKey: "deletedAt",
157
+ defaultOrdering: { createdAt: "desc" },
158
+ });
159
+ }
176
160
 
177
- const user = await userRepository.save({
178
- name: "John",
179
- email: "john@email.com",
180
- password: "password",
181
- });
161
+ @DynamicMethod()
162
+ declare findByEmail: (email: string) => Promise<User[]>;
182
163
 
183
- const found = await userRepository.get(user.id);
184
- const all = await userRepository.getAll();
185
-
186
- user.name = "John Smith";
164
+ @DynamicMethod()
165
+ declare findOneByEmail: (email: string) => Promise<User | null>;
166
+ }
187
167
 
188
- await userRepository.save(user);
189
- await userRepository.remove(user.id);
168
+ export default new UserRepository();
190
169
  ```
191
170
 
192
- ---
193
-
194
- ## Class-based approach (DynamicRepository)
195
-
196
- If you prefer an OOP style with decorators instead of the functional `setupVSRepo` approach, VSRepository also provides `DynamicRepository` — a class you can extend with `@DynamicMethod()` decorators to define your dynamic methods.
197
-
198
- See **[README-DynamicRepo.md](./README-DynamicRepo.md)** (or the [pt-BR version](./README-DynamicRepo.pt-BR.md)) for full documentation on the class-based approach, including NestJS integration examples, decorator config, and a comparison with `setupVSRepo`.
199
-
200
- ---
201
-
202
- ## NestJS integration
171
+ > The core API (`VSRepository`, `VSRepoAdapter`, `DynamicMethod`, `QueryMethod`, `VSRepoError`, enums and types) is imported from the single `vsrepo` entry point. The concrete adapter comes from a **separate** package (`@vsrepo/*-adapter`). On Prisma 7, install the published [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) (its constructor takes a config object — `tableName`, `pkName`, optional `relations`/`logLevel` — as shown above). Official adapters for other ORMs are planned but not published yet; until they are, you can implement the `VSRepoAdapter` contract yourself (see [Writing your own adapter](#writing-your-own-adapter)) — and publishing it to help the project is very welcome.
203
172
 
204
- VSRepository can be easily integrated into NestJS projects through providers. Below is a complete example using NestJS's dependency injection pattern.
173
+ > **The third generic parameter (`OrmTypes`):** `VSRepository<Entity, PKType, OrmTypes>` accepts an optional third type parameter describing your ORM's client/transaction types, via `VSRepoOrmTypes` (`{ dbClient; dbTransaction }`). Supplying it gives you a correctly-typed `getDbClient()`, `transaction()` callback, and `db` option on every method, instead of `any`:
174
+ > ```typescript
175
+ > type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
176
+ >
177
+ > class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
178
+ > // getDbClient() now returns PrismaClient, and transaction(fn) types `tx` as Prisma.TransactionClient
179
+ > }
180
+ > ```
181
+ > If omitted, it defaults to `VSRepoOrmTypes` (`dbClient`/`dbTransaction` both `any`).
205
182
 
206
- ### Configuring the repository as a provider
183
+ ### Using the repository
207
184
 
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";
185
+ ```typescript
186
+ import userRepository from "./repositories/user.repository";
214
187
 
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
- },
188
+ const user = await userRepository.save({
189
+ name: "Joao",
190
+ email: "joao@email.com",
191
+ password: "password",
255
192
  });
256
193
 
257
- const setupUserRepository = (prisma: PrismaService) => {
258
- return userVSRepo.build(prisma);
259
- };
260
-
261
- export type UserRepository = ReturnType<typeof setupUserRepository>;
262
- /*
263
- The type can also be inferred using VSRepository's `RepositoryOf`, passing the `userVSRepo` type:
264
-
265
- export type UserRepository = RepositoryOf<typeof userVSRepo>;
266
-
267
- NOTE: If you use `.extend` to extend the repository or configure the base methods,
268
- using `ReturnType` is recommended since it's simpler to infer the type
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
- ```
194
+ const found = await userRepository.get(user.id);
195
+ const all = await userRepository.getAll();
196
+ const byEmail = await userRepository.findByEmail("joao@email.com");
279
197
 
280
- ### Registering the provider in the module
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 {}
198
+ await userRepository.patch(user.id, { name: "Joao Pedro" });
199
+ await userRepository.remove(user.id);
296
200
  ```
297
201
 
298
- ### Using the repository in a 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
- }
202
+ ---
319
203
 
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,
325
- });
326
- }
327
- }
328
- ```
204
+ ## Constructor options
329
205
 
330
- **Benefits of this approach:**
206
+ `VSRepoOptions<T, K>`, passed to `super(...)` inside your repository's constructor:
331
207
 
332
- - ✅ Type-safe repositories with dependency injection
333
- - ✅ Easy to test (mock the `USER_REPOSITORY`)
334
- - ✅ Isolation of persistence logic
335
- - ✅ Repository reuse across multiple services
336
- - ✅ Transaction support via `PrismaService`
208
+ | Option | Type | Description |
209
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
210
+ | `adapter` | `VSRepoAdapter<T>` | **Required.** The adapter instance that translates repository calls into calls against the underlying ORM/database. |
211
+ | `pkName` | `keyof T` | **Required.** Name of the field that represents the entity's primary key. |
212
+ | `softRemoveKey` | `keyof T` | Optional. When set, enables `softRemove`, `softRemoveList`, `restore` and `restoreList`. |
213
+ | `defaultOrdering` | `Ordering<T>` | Optional. Default ordering applied automatically to queries that accept `order`, unless overridden per call. |
214
+ | `logLevel` | `VSLogLevel` | Optional. Minimum severity printed by the internal logger. Defaults to `VSLogLevel.WARN`. |
215
+ | `logSlowThresholdMs` | `number` | Optional. Duration (ms) above which a finished operation is logged as `WARN` instead of `DEBUG`. Defaults to 300ms. |
337
216
 
338
217
  ---
339
218
 
340
219
  ## Base methods
341
220
 
342
- When calling `.build(prisma)`, the base methods below are automatically made available:
343
-
344
- | Method | Description |
345
- | ------------------------ | -------------------------------------------------------------------------------------------------------------|
346
- | `get(pk)` | Fetches a record by its primary key |
347
- | `getOrThrow(pk)` | Fetches a record by its primary key; throws `VSRepoRuntimeError` (code `"20727"`) if not found |
348
- | `getList(pks)` | Fetches multiple records from a list of primary keys |
349
- | `save(obj)` | Creates or updates — if the object has a `pk` it performs an `upsert`, otherwise a `create` |
350
- | `saveList(objs)` | Saves an array of objects in a single automatic transaction |
351
- | `patch(pk, obj)` | Partially updates a record by its primary key |
352
- | `patchList(tuples)` | Partially updates multiple records via an array of `[pk, obj]` tuples in an automatic transaction |
353
- | `merge(pk, obj)` | Fetches a record and deep merges it in memory — **does not persist**, returns the merged object |
354
- | `remove(pk)` | Removes a record by its primary key |
355
- | `removeList(pks)` | Removes several records by a list of primary keys — returns `{ count }` |
356
- | `getAll()` | Returns all records (accepts `pagination` and `order` in `options`) |
357
- | `total()` | Returns the total number of records |
358
- | `has(pk)` | Checks whether a record exists by its primary key — returns `boolean` |
359
-
360
- All of them accept `options` as the last argument.
361
-
362
- ### Soft-delete
363
-
364
- When `softRemovekName` is configured on the repository, the following additional methods become available:
365
-
366
- | Method | Description |
367
- | -------------------------- | ---------------------------------------------------------------------------------- |
368
- | `softRemove(pk)` | Marks a record as removed by filling `softRemovekName` with the current date |
369
- | `softRemoveList(pks)` | Marks multiple records as removed in batch — returns `{ count }` |
370
- | `restore(pk)` | Restores a soft-deleted record, clearing the `softRemovekName` field |
371
- | `restoreList(pks)` | Restores multiple soft-deleted records in batch — returns `{ count }` |
372
-
373
- ```ts
374
- const userRepository = setupVSRepo<User, "user">()(({
375
- tableName: "user",
376
- pkName: "id",
377
- softRemovekName: "deletedAt", // must be a DateTime field in the Prisma schema
378
- }).build(prisma);
379
-
380
- await userRepository.softRemove(1);
381
- await userRepository.restore(1);
382
- ```
383
-
384
- > The field provided in `softRemovekName` **must** be of type `DateTime` in the Prisma schema. VSRepository validates this at `build` time and throws `VSRepoBuildError` if the type is incorrect.
385
-
386
- ### Batch operations
387
-
388
- `saveList` and `patchList` automatically run all operations inside a single Prisma transaction. If any operation fails, all previous ones are rolled back.
389
-
390
- ```ts
391
- // saveList — creates or updates multiple objects in an automatic transaction
392
- const users = await userRepository.saveList([
393
- { name: "Mary", email: "mary@email.com" },
394
- { id: 2, name: "John Updated", email: "john@email.com" },
395
- ]);
396
-
397
- // patchList — partially updates multiple records via [pk, obj] tuples
398
- const updated = await userRepository.patchList([
399
- [1, { active: false }],
400
- [2, { name: "New Name" }],
401
- ]);
402
- ```
403
-
404
- When you're already inside an existing transaction, pass it in `options.db`. In this case, `db` must be a `DbTransaction` (not the main client):
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
- ```
412
-
413
- ### Merge
414
-
415
- The `merge` method fetches a record by its PK and deeply merges (`deepmerge`) the provided object with the existing data **in memory**. It **does not persist** the changes — it returns the merged result so you can decide what to do with it.
221
+ Available automatically on every `VSRepository` subclass:
222
+
223
+ | Method | Description |
224
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
225
+ | `get(pk, options?)` | Fetches a record by primary key. |
226
+ | `getOrThrow(pk, options?)` | Fetches a record by primary key, throwing if not found. |
227
+ | `getList(pks, options?)` | Fetches multiple records by a list of primary keys. |
228
+ | `getAll(options?)` | Fetches all records; accepts `pagination` and `order` in `options`. |
229
+ | `save(obj, options?)` | Creates or updates (upsert) a single record. |
230
+ | `saveList(objs, options?)` | Creates or updates (upsert) multiple records in one call. |
231
+ | `patch(pk, obj, options?)` | Partially updates a record by primary key. |
232
+ | `merge(pk, obj, options?)` | Fetches a record and returns it deep-merged, in memory, with the given object — does **not** persist anything. |
233
+ | `remove(pk, options?)` | Deletes a record by primary key. |
234
+ | `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
235
+ | `total(options?)` | Returns the total number of records. |
236
+ | `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
237
+ | `increment(pk, field, value, options?)` | Atomically adds `value` to a numeric field. See [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
238
+ | `decrement(pk, field, value, options?)` | Atomically subtracts `value` from a numeric field. |
239
+ | `multiply(pk, field, value, options?)` | Atomically multiplies a numeric field by `value`. |
240
+ | `divide(pk, field, value, options?)` | Atomically divides a numeric field by `value`. |
241
+ | `sum(field, where?, options?)` | Sums a numeric field across every matching record; `null` if none match. |
242
+ | `average(field, where?, options?)` | Arithmetic mean of a numeric field across every matching record; `null` if none match. |
243
+ | `min(field, where?, options?)` | Minimum value of a numeric field across every matching record; `null` if none match. |
244
+ | `max(field, where?, options?)` | Maximum value of a numeric field across every matching record; `null` if none match. |
245
+ | `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
246
+ | `getDbClient()` | Returns the underlying ORM client instance used outside of transactions. |
247
+ | `query<T>(query, options?)` | Executes a raw SQL statement directly against the database. See [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query). |
248
+
249
+ Most of the above accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`). A few — `total`, `has`, `removeList`, `sum`, `average`, `min`, `max`, and the soft-delete batch methods (`softRemoveList`/`restoreList`) — don't return/shape an `Entity`, so they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` instead (`see`, `db` only; no `select`/`relations`). `transaction`, `query`, and `getDbClient` accept their own options or none at all.
416
250
 
417
- ```ts
418
- const existing = await userRepository.get(1);
419
- // existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
251
+ ---
420
252
 
421
- const merged = await userRepository.merge(1, {
422
- profile: { bio: "Updated bio" },
423
- });
424
- // merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
253
+ ## Soft-delete
425
254
 
426
- // To persist, pass it to save or patch:
427
- await userRepository.save(merged);
428
- ```
255
+ Soft-delete is now a **first-class, built-in concept**. Configure `softRemoveKey` once on the repository:
429
256
 
430
- Returns `null` if the record is not found.
431
-
432
- **Merging to-many relations (`otm`/`mtm`) is done by PK, not by simple concatenation.** For to-one relations (`oto`/`mto`), `merge` performs a regular deep merge of the object. For to-many relations, each item in the sent array is matched against the existing item that has the same PK (defined in `relations[key].pk`): if the PK matches, the two objects are merged together; if it doesn't match (a new item with no counterpart), it's simply added to the list. Existing items that don't appear in the sent array are kept.
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 }, // same PK (id: 10) → merges with the existing item
447
- { title: "Post C" }, // no PK → added as a new item
448
- ],
257
+ ```typescript
258
+ super({
259
+ pkName: "id",
260
+ adapter,
261
+ softRemoveKey: "deletedAt",
449
262
  });
450
- // merged: {
451
- // id: 1,
452
- // posts: [
453
- // { id: 10, title: "Post A", published: true }, // merged
454
- // { id: 11, title: "Post B", published: true }, // kept, wasn't in the sent array
455
- // { title: "Post C" }, // added
456
- // ],
457
- // }
458
263
  ```
459
264
 
460
- > Note that `merge` never removes items from a to-many relation — it only merges the ones that match by PK and adds the ones that don't. To remove items from a relation, use `save`/`patch` with `restriction: "set"` in the relation configuration.
461
-
462
- ### Configuring the base methods
463
-
464
- The second argument of `.build(prisma, config)` lets you adjust the repository's global behavior and customize each base method individually through `baseMethods`.
265
+ This unlocks four extra methods:
465
266
 
466
- ```ts
467
- userVSRepo.build(prisma, {
468
- // Shows VSRepository's internal logs on the console (built queries, detected prefix,
469
- // applied filters, etc). Great for debugging dynamic methods. Default = false.
470
- showWorking: true,
267
+ | Method | Effect |
268
+ | ------------------------------- | ------------------------------------- |
269
+ | `softRemove(pk, options?)` | Sets `deletedAt` to the current date. |
270
+ | `softRemoveList(pks, options?)` | Same, in batch — returns `{ count }`. |
271
+ | `restore(pk, options?)` | Sets `deletedAt` back to `null`. |
272
+ | `restoreList(pks, options?)` | Same, in batch — returns `{ count }`. |
471
273
 
472
- baseMethods: {
473
- get: {
474
- // Enables/disables the method on the final repository. If `false`, the method
475
- // doesn't even appear in the repository's type (it's not just a runtime error). Default = true.
476
- active: true,
274
+ Every other method accepts a `see` option controlling visibility of soft-deleted rows:
477
275
 
478
- // Select model applied by default when the method is called without `options.selectModel`.
479
- // Overrides the `defaultSelectModel` from setupVSRepo for this method only.
480
- defaultSelect: "public",
481
- },
482
- remove: {
483
- active: true,
484
- defaultSelect: "minimal",
485
-
486
- // When `true`, ignores the `requiredWhere` configured in setupVSRepo for
487
- // this specific method — useful when a method needs to "punch through" a
488
- // global filter (e.g. multi-tenancy) in a specific case. Default = false.
489
- ignoreRequiredWhere: false,
490
- },
491
- save: {
492
- // Here only `ignoreRequiredWhere` is set — `active` and `defaultSelect`
493
- // keep their defaults (true and the global `defaultSelectModel`).
494
- ignoreRequiredWhere: true,
495
- },
496
- patch: {
497
- // Only the select is overridden; the method stays active normally.
498
- defaultSelect: "minimal",
499
- },
500
- has: {
501
- active: false, // Disables 'has' (default = true) — the method disappears from the repository
502
- },
503
- softRemove: {
504
- // Soft-delete methods follow the same options (`active`, `defaultSelect`,
505
- // `ignoreRequiredWhere`). They're only available if `softRemovekName` is configured.
506
- active: true,
507
- defaultSelect: "minimal",
508
- },
509
- },
510
- });
276
+ ```typescript
277
+ await userRepository.getAll({ see: "active" }); // default — only non-deleted records
278
+ await userRepository.getAll({ see: "removed" }); // only soft-deleted records
279
+ await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
511
280
  ```
512
281
 
513
- > Batch/aggregate methods like `removeList`, `softRemoveList`, `restoreList`, `total`, and `has` **do not** accept `defaultSelect` (they don't return a selectable record — they return `{ count }` or `boolean`). In these cases `BaseMethodConfig` is restricted to `active` and `ignoreRequiredWhere`.
514
-
515
282
  ---
516
283
 
517
- ## Select Models
518
-
519
- `selectModels` defines named, reusable data projections.
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` defines which select is used automatically when none is specified in the call. It's recommended to always define it together with `selectModels`.
284
+ ## Atomic and aggregate methods
531
285
 
532
- **Using a specific select in the call:**
286
+ Every `VSRepository` subclass gets 8 extra methods for working with numeric fields, split into two groups:
533
287
 
534
- ```ts
535
- const user = await userRepository.get(id, { selectModel: "minimal" });
536
- ```
537
-
538
- **Returning Prisma's default payload (without select):**
288
+ **Atomic updates** — evaluated server-side against the row's *current* value (`UPDATE ... SET field = field + value`), not a client-side read-modify-write:
539
289
 
540
- ```ts
541
- const fullUser = await userRepository.get(id, { selectModel: false });
290
+ ```typescript
291
+ await userRepository.increment("user-1", "balance", 50); // balance = balance + 50
292
+ await userRepository.decrement("user-1", "balance", 50); // balance = balance - 50
293
+ await userRepository.multiply("user-1", "balance", 2); // balance = balance * 2
294
+ await userRepository.divide("user-1", "balance", 4); // balance = balance / 4
542
295
  ```
543
296
 
544
- ### Raw `select` (`options.select`)
297
+ All four return the updated `Entity` and accept the full `MethodOptions<Entity, OrmTypes>` (`select`, `relations`, `see`, `db`) as their last argument, same as `get`/`save`/`patch`.
545
298
 
546
- Besides `selectModel` (named, pre-configured in `selectModels`), you can pass a raw Prisma `select` directly in the call, without registering it beforehand on the repository.
299
+ **Aggregates** — computed across every record matching an (optional) `where`:
547
300
 
548
- ```ts
549
- const user = await userRepository.get(id, {
550
- select: { id: true, name: true },
551
- });
301
+ ```typescript
302
+ await userRepository.sum("balance"); // total balance across every active record
303
+ await userRepository.sum("balance", { active: true }); // ...restricted by a where
304
+ await userRepository.average("balance");
305
+ await userRepository.min("balance");
306
+ await userRepository.max("balance");
552
307
  ```
553
308
 
554
- `options.select` accepts any valid Prisma `select` for the repository's model — it's fully typed and offers the same autocomplete/validation as calling `prisma.user.findMany({ select: ... })` directly, and the method's return type is narrowed to exactly the fields selected.
555
-
556
- **Rules and behavior:**
309
+ All four return `number | null` — `null` when no record matches, mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL` (not `0`) over an empty set. Unlike the atomic methods, they accept the narrower `RestrictMethodOptions<Entity, OrmTypes>` (`see`, `db` only — no `select`/`relations`, since the result is a plain number, not a shaped `Entity`).
557
310
 
558
- - **Mutually exclusive with `selectModel`, `includeModel` and `include`.** Only one of the four can be provided per call; the types enforce this — passing more than one is a compile-time error.
559
- - **Ad hoc, not reusable.** Unlike `selectModel`, it doesn't need to be declared in `selectModels`. Use it for one-off projections that don't justify a named select model.
560
- - **No `defaultSelectModel` is applied.** When `select` is provided, the default select (`defaultSelectModel`) is ignored and only the raw `select` is sent to Prisma.
311
+ Both groups respect `softRemoveKey`/`see` the same way every other base method does — `sum("balance")` only totals non-deleted records by default, pass `{ see: "all" }` or `{ see: "removed" }` to change that.
561
312
 
562
- ```ts
563
- // CORRECT ✅ — raw select only
564
- await userRepository.get(id, { select: { id: true, name: true } });
313
+ ### Which fields are eligible
565
314
 
566
- // WRONG ❌ — combining select with selectModel/includeModel/include is not allowed
567
- await userRepository.get(id, { selectModel: "public", select: { id: true } });
568
- await userRepository.get(id, { include: { posts: true }, select: { id: true } });
569
- ```
315
+ `field` is constrained to `NumericKeys<Entity>` — keys whose (non-nullable) value type is a `number`, a `bigint`, or a `DecimalLike` object (anything exposing `toNumber()` and `decimalPlaces()`, matching e.g. Prisma's `Prisma.Decimal`):
570
316
 
571
- > **When to use `selectModel` vs. `select`:** prefer `selectModel` for projections reused across multiple calls (defined once in `selectModels`); use `select` for specific, occasional projections that don't need a name.
317
+ ```typescript
318
+ type Product = { id: string; name: string; price: Decimal; stock: number | null };
572
319
 
573
- ---
574
-
575
- ## Include Models
576
-
577
- `includeModels` works similarly to `selectModels`, but instead of receiving a `select`, it receives a valid Prisma `include`.
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);
320
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok — Decimal-like
321
+ await productRepository.increment(id, "stock", 5); // ok — nullable numeric fields are included
322
+ await productRepository.increment(id, "name", 1); // compile error — "name" isn't numeric
592
323
  ```
593
324
 
594
- **Using an `includeModel` in the call:**
325
+ `value` is typed as `NonNullable<Entity[Field]>` — it must match the field's own type exactly. A `Decimal` field expects a `Decimal` instance, not a plain `number`/`string`:
595
326
 
596
- ```ts
597
- const user = await userRepository.get(id, { includeModel: "withPosts" });
327
+ ```typescript
328
+ await productRepository.increment(id, "price", new Decimal(10.5)); // ok
329
+ await productRepository.increment(id, "price", 10.5); // compile error — wrap it: new Decimal(10.5)
598
330
  ```
599
331
 
600
- In this case, the default `select` (`selectModels`/`defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
601
-
602
- ### Differences from `selectModels`
332
+ Note that several ORMs (Drizzle, MikroORM, TypeORM) represent `decimal`/`numeric` columns as plain `string` by default, to avoid floating-point precision loss — a `string` field does **not** satisfy `NumericKeys<Entity>` out of the box. Configure the column in a numeric mode (or a transformer) on those ORMs if you want the field to be usable with these 8 methods.
603
333
 
604
- - **Can only be passed in the method call**, via `options.includeModel`. There's no `defaultIncludeModel` or `defaultInclude` — there's no way to configure a default `includeModel` on the repository, unlike what happens with `defaultSelectModel`.
605
- - **`includeModel` and `selectModel` cannot be passed together** in the same call. If an `includeModel` is provided, any `selectModel` (including the default one) is ignored.
334
+ ### Writing an adapter
606
335
 
607
- ```ts
608
- // CORRECT ✅ — includeModel only
609
- await userRepository.get(id, { includeModel: "withPosts" });
336
+ `VSRepoAdapter` mirrors the same 8 operations (`incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `sum`, `average`, `min`, `max` — see [Writing your own adapter](#writing-your-own-adapter)). Each adapter translates them into whatever its ORM/database considers "native": Prisma has a built-in `{ field: { increment: value } }` update shape and an `aggregate()` call; other ORMs typically need a `QueryBuilder`/raw-`sql` expression (e.g. `SET field = field * :value`, `SELECT SUM(field) ...`) instead. The atomic methods must return the record reflecting the state *after* the write — if the ORM's atomic-update API only returns an affected-row count, issue a follow-up read rather than returning a stale in-memory copy.
610
337
 
611
- // CORRECT ✅ — selectModel only
612
- await userRepository.get(id, { selectModel: "public" });
613
-
614
- // WRONG ❌ — combining both is not allowed
615
- await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
616
- ```
338
+ ---
617
339
 
618
- ### Raw `include` (`options.include`)
340
+ ## `select` and `relations`
619
341
 
620
- Besides `includeModel` (named, pre-configured in `includeModels`), you can pass a raw Prisma `include` directly in the call, without registering it beforehand on the repository.
342
+ v1's named, reusable `selectModels`/`defaultSelectModel` are gone. In v2 you pass `select` and `relations` directly on each call — there's nothing to pre-register:
621
343
 
622
- ```ts
344
+ ```typescript
623
345
  const user = await userRepository.get(id, {
624
- include: { posts: true, profile: true },
346
+ select: { id: true, name: true, address: { city: true } },
625
347
  });
626
- ```
627
-
628
- `options.include` accepts any valid `include` for the repository's Prisma model — it's fully typed and offers the same autocomplete/validation as calling `prisma.user.findMany({ include: ... })` directly.
629
-
630
- **Rules and behavior:**
631
-
632
- - **Mutually exclusive with `selectModel` and `includeModel`.** Only one of the three can be provided per call; the types enforce this — passing more than one is a compile-time error.
633
- - **Ad hoc, not reusable.** Unlike `includeModel`, it doesn't need to be declared in `includeModels`. Use it for one-off includes that don't justify a named model.
634
- - **No `selectModel` default is applied.** As with `includeModel`, when `include` is provided the select (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
635
-
636
- ```ts
637
- // CORRECT ✅ — raw include only
638
- await userRepository.get(id, { include: { posts: true } });
639
-
640
- // WRONG ❌ — combining include with selectModel/includeModel is not allowed
641
- await userRepository.get(id, { selectModel: "public", include: { posts: true } });
642
- await userRepository.get(id, { includeModel: "withPosts", include: { posts: true } });
643
- ```
644
-
645
- > **When to use `includeModel` vs. `include`:** prefer `includeModel` for includes reused across multiple calls (defined once in `includeModels`); use `include` for specific, occasional includes that don't need a name.
646
-
647
- ---
648
-
649
- ## Required Where
650
-
651
- `requiredWhere` defines filters that are automatically applied to every query on the repository.
652
-
653
- ```ts
654
- requiredWhere: { active: true },
655
- ```
656
-
657
- Now every query will automatically include `active: true`:
658
-
659
- ```ts
660
- // Internally: WHERE active = true
661
- const users = await userRepository.findMany();
662
-
663
- // Internally: WHERE email = 'john@email.com' AND active = true
664
- const user = await userRepository.findByEmail("john@email.com");
665
- ```
666
-
667
- Useful for manual soft-deletes, multi-tenancy, and global filters of any kind.
668
-
669
- ---
670
-
671
- ## Default Ordering
672
-
673
- `defaultOrdering` defines a default ordering that's automatically applied to every query that accepts `orderBy`, without needing to repeat the `order` argument on every call.
674
-
675
- ```ts
676
- const userRepository = setupVSRepo<User, "user">()(({
677
- tableName: "user",
678
- pkName: "id",
679
- defaultOrdering: { createdAt: "desc" },
680
- }).build(prisma);
681
- ```
682
-
683
- With this, every listing query will already come ordered by `createdAt` descending:
684
-
685
- ```ts
686
- // Internally: ORDER BY createdAt DESC
687
- const users = await userRepository.getAll();
688
-
689
- // Also applies to getAll with pagination
690
- const paginated = await userRepository.getAll({ pagination: { take: 10 } });
691
- ```
692
-
693
- **`defaultOrdering` is ignored when:**
694
348
 
695
- - The method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix — in these cases the `order` argument passed in the call takes priority.
696
- - The dynamic method has `injectOrdering` configured — the method's fixed ordering takes precedence.
697
-
698
- ```ts
699
- methods: {
700
- findManyPaginatedAndOrdered: { map: true }, // order comes from the argument → defaultOrdering ignored
701
- findManyByActive: { map: true }, // no Ordered → defaultOrdering applied
702
- findManyByStatus: {
703
- map: true,
704
- injectOrdering: { name: "asc" }, // injectOrdering → defaultOrdering ignored
705
- },
706
- }
349
+ const userWithAddress = await userRepository.get(id, {
350
+ relations: { address: true },
351
+ });
707
352
  ```
708
353
 
709
- > `defaultOrdering` accepts the same type as Prisma's native `orderBy` for the model — including arrays of chained orderings.
710
-
711
- ---
712
-
713
- ## `see` option
354
+ - `select` mirrors the entity's shape: scalar fields take a `boolean`; relation fields take a `boolean` or a nested `select`.
355
+ - `relations` eagerly loads related records; each relation field takes a `boolean` or a nested `relations` object.
356
+ - Whether `select` and `relations` can be combined depends on the adapter (see below).
714
357
 
715
- When `softRemovekName` is configured, every method accepts the `see` option to control the visibility of soft-deleted records:
716
-
717
- | Value | Behavior |
718
- | ----------- | --------------------------------------------------------------|
719
- | `"active"` | Returns only records that are **not** removed (default) |
720
- | `"removed"` | Returns only removed records |
721
- | `"all"` | Returns all records, regardless of status |
722
-
723
- ```ts
724
- // Returns only active users (default)
725
- const active = await userRepository.getAll();
726
-
727
- // Returns only removed users
728
- const removed = await userRepository.getAll({ see: "removed" });
729
-
730
- // Returns all
731
- const all = await userRepository.getAll({ see: "all" });
732
- ```
733
-
734
- > The `see` option works independently of `requiredWhere` — it's applied on top of the soft-delete filter, not as a replacement for it.
358
+ > ⚠️ **Adapter-dependent behavior for `relations`:**
359
+ >
360
+ > The core only forwards `MethodOptions.select` and `MethodOptions.relations` to the adapter — each adapter decides how to translate them to the underlying ORM:
361
+ >
362
+ > - **TypeORM (`@vsrepo/typeorm-adapter`)** — `relations` is **required** to load any relation, even when you only want a nested projection via `select`. TypeORM will not JOIN/emit the relation unless it is listed in `relations`:
363
+ > ```typescript
364
+ > // TypeORM: select alone is NOT enough
365
+ > await userRepository.get(id, {
366
+ > select: { id: true, address: { city: true } },
367
+ > relations: { address: true }, // ← required in TypeORM
368
+ > });
369
+ > ```
370
+ > - **Prisma 7 (`@vsrepo/prisma7-adapter` / `VSRepoPrisma7Adapter`)** — `relations` is converted to Prisma `include` (`parsePrismaInclude`). **If `select` is present, `relations` is ignored** because Prisma does not allow `select` + `include` in the same query:
371
+ > ```typescript
372
+ > // Prisma7: relations is ignored when select exists
373
+ > await userRepository.get(id, {
374
+ > select: { id: true, name: true },
375
+ > relations: { address: true }, // ← ignored, include = undefined
376
+ > });
377
+ > ```
378
+ >
379
+ > Custom adapters may map `relations` differently — consult the adapter's documentation for the exact semantics.
735
380
 
736
381
  ---
737
382
 
738
383
  ## Dynamic methods
739
384
 
740
- Dynamic methods are defined in the `methods` property and have their behavior inferred from their name.
741
-
742
- ```ts
743
- methods: {
744
- findOneByEmail: { map: true },
745
- findManyPaginated: { map: true },
746
- updateById: { map: true },
747
- deleteManyByIdIn: { map: true },
385
+ Dynamic methods are declared as a `declare` field annotated with `@DynamicMethod()`. Their behavior — which adapter method to call, which filters to apply, and how arguments map to them — is inferred entirely from the field's **name**, following the same convention-over-configuration philosophy as v1.
386
+
387
+ ```typescript
388
+ class UserRepository extends VSRepository<User, string> {
389
+ @DynamicMethod()
390
+ declare findByEmail: (email: string) => Promise<User[]>;
391
+
392
+ @DynamicMethod()
393
+ declare findOneByEmail: (email: string) => Promise<User | null>;
394
+
395
+ @DynamicMethod()
396
+ declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
397
+
398
+ // Where-based: VSRepoWhere<T> as the first param, pagination penultimate, MethodOptions last
399
+ @DynamicMethod()
400
+ declare findWherePaginated: (
401
+ where: VSRepoWhere<User>,
402
+ pagination: Pagination,
403
+ options?: MethodOptions<User>,
404
+ ) => Promise<User[]>;
405
+
406
+ // OrderedAndPaginated: field filters, then order, then pagination, then MethodOptions
407
+ @DynamicMethod()
408
+ declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
409
+ name: string,
410
+ age: [number, number],
411
+ order: Ordering<User>,
412
+ pagination: Pagination,
413
+ options?: MethodOptions<User>,
414
+ ) => Promise<User[]>;
748
415
  }
749
416
  ```
750
417
 
751
- ---
752
-
753
418
  ### Available prefixes
754
419
 
755
- The method name's prefix determines which Prisma operation will be called and which arguments are expected.
756
-
757
- | Prefix | Prisma operation | Return | Notes |
758
- | ---------------------------- | -------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
759
- | `findOneBy` | `findFirst` | `T \| null` | Single return. |
760
- | `findBy` | `findMany` / `findFirst` | `T[]` or `T \| null` | Default is list; use `fbMode: "one"` for a single return (**deprecated**, use `findOneBy`) |
761
- | `findUniqueBy` | `findUnique` | `T \| null` | |
762
- | `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Throws an error if not found |
763
- | `findFirstBy` | `findFirst` | `T \| null` | Accepts fields as filter |
764
- | `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Accepts fields as filter; throws an error if not found |
765
- | `findFirst` | `findFirst` | `T \| null` | No field filters; applies only `requiredWhere` and `pushWhere` |
766
- | `findFirstOrThrow` | `findFirstOrThrow` | `T` | No field filters; applies only `requiredWhere` and `pushWhere`; throws an error if not found |
767
- | `findManyBy` | `findMany` | `T[]` | Accepts fields as filter |
768
- | `findMany` | `findMany` | `T[]` | No field filters; applies only `requiredWhere` and `pushWhere` |
769
- | `findOneWhere` | `findFirst` | `T \| null` | Receives an explicit `where` object as argument |
770
- | `findListWhere` | `findMany` | `T[]` | Receives an explicit `where` object as argument |
771
- | `existsBy` | `findFirst` | `boolean` | Returns `true` if found, `false` otherwise |
772
- | `existsWhere` | `findFirst` | `boolean` | Receives an explicit `where` object and returns whether it exists |
773
- | `countBy` | `count` | `number` | Accepts fields as filter |
774
- | `countWhere` | `count` | `number` | Receives an explicit `where` object as argument |
775
- | `count` | `count` | `number` | No field filters; applies only `requiredWhere` and `pushWhere` |
776
- | `create` | `create` | `T` | Receives `data` as argument |
777
- | `createMany` | `createMany` | `{ count: number }` | Receives `data` as argument; supports `SkipDuplicates` |
778
- | `createManyAndReturn` | `createManyAndReturn` | `T[]` | Receives `data` as argument; supports `SkipDuplicates` |
779
- | `updateBy` | `update` | `T` | Receives `data` as argument |
780
- | `updateManyBy` | `updateMany` | `{ count: number }` | Receives `data` as argument |
781
- | `updateManyWhere` | `updateMany` | `{ count: number }` | Receives a `where` object and a `data` object as arguments |
782
- | `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Receives `data` as argument |
783
- | `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Receives a `where` object and a `data` object as arguments |
784
- | `upsertBy` | `upsert` | `T` | Receives `update` and `create` as arguments |
785
- | `deleteBy` | `delete` | `T` | |
786
- | `deleteManyBy` | `deleteMany` | `{ count: number }` | |
787
- | `deleteManyWhere` | `deleteMany` | `{ count: number }` | Receives an explicit `where` object as argument |
788
- | `aggregate` | `aggregate` | `Dynamic` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
789
- | `groupBy` | `groupBy` | `Dynamic[]` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
790
-
791
- ---
420
+ | Prefix | Adapter method | Notes |
421
+ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
422
+ | `findBy` | `findMany` | Field filters follow the prefix. |
423
+ | `findOneBy` | `findOne` | Field filters follow the prefix; single result. |
424
+ | `findOneOrThrowBy` | `findOneOrThrow` | Throws if no record is found. |
425
+ | `findOneOrThrow` | `findOneOrThrow` | No field filters; applies only soft-delete/`see`. |
426
+ | `findOneOrThrowWhere` | `findOneOrThrow` | Receives a `VSRepoWhere<T>` as the first argument. |
427
+ | `findWhere` | `findMany` | Receives a `VSRepoWhere<T>` as the first argument. |
428
+ | `findOneWhere` | `findOne` | Receives a `VSRepoWhere<T>` as the first argument. |
429
+ | `findOne` | `findOne` | No field filters; applies only soft-delete/`see`. |
430
+ | `countBy` | `count` | Field filters follow the prefix. |
431
+ | `countWhere` | `count` | Receives a `VSRepoWhere<T>` as the first argument. |
432
+ | `count` | `count` | No field filters. |
433
+ | `existsBy` | `exists` | Returns `boolean`. |
434
+ | `existsWhere` | `exists` | Receives a `VSRepoWhere<T>` as the first argument. |
435
+ | `create` | `create` | Receives `data` as argument. |
436
+ | `createMany` | `createMany` | Receives `data[]` as argument; supports `IgnoreConflicts`. |
437
+ | `createManyReturning` | `createManyReturning` | Receives `data[]` as argument; supports `IgnoreConflicts`; returns the created records (`T[]`) instead of `CountResult`. |
438
+ | `updateBy` | `update` | Field filters + `data` as argument. |
439
+ | `updateWhere` | `update` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
440
+ | `updateManyBy` | `updateMany` | Field filters + `data`. |
441
+ | `updateManyWhere` | `updateMany` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
442
+ | `updateManyReturningBy` | `updateManyReturning` | Field filters + `data`; returns updated records. |
443
+ | `updateManyReturningWhere` | `updateManyReturning` | Receives a `VSRepoWhere<T>` as the first argument, then `data`; returns updated records. |
444
+ | `upsertBy` | `upsert` | Field filters + `create`/`update` payloads. |
445
+ | `upsertWhere` | `upsert` | Receives a `VSRepoWhere<T>` as the first argument, then `create`/`update` payloads. |
446
+ | `deleteBy` | `delete` | Field filters follow the prefix. |
447
+ | `deleteWhere` | `delete` | Receives a `VSRepoWhere<T>` as the first argument. |
448
+ | `deleteManyBy` | `deleteMany` | Field filters follow the prefix. |
449
+ | `deleteManyWhere` | `deleteMany` | Receives a `VSRepoWhere<T>` as the first argument. |
450
+ | `deleteManyReturningBy` | `deleteManyReturning` | Field filters follow the prefix; returns deleted records. |
451
+ | `deleteManyReturningWhere` | `deleteManyReturning` | Receives a `VSRepoWhere<T>` as the first argument; returns deleted records. |
452
+
453
+ > `aggregate` and `groupBy` are **not implemented yet** in v2 (they existed in v1). This is planned but not currently available.
792
454
 
793
455
  ### Field filters
794
456
 
795
- Filters are suffixes applied to the field name inside the method. The field itself comes capitalized right after the prefix (or after `By`).
796
-
797
- | Suffix | Prisma operator | Argument required |
798
- | -------------------- | ---------------------- | ---------------------------|
799
- | *(no suffix)* | equality (`=`) | yes |
800
- | `Not` | `not` | yes |
801
- | `In` | `in` | yes (array) |
802
- | `NotIn` | `notIn` | yes (array) |
803
- | `Contains` | `contains` | yes |
804
- | `NotContains` | `not.contains` | yes |
805
- | `StartsWith` | `startsWith` | yes |
806
- | `NotStartsWith` | `not.startsWith` | yes |
807
- | `EndsWith` | `endsWith` | yes |
808
- | `NotEndsWith` | `not.endsWith` | yes |
809
- | `GreaterThan` | `gt` | yes |
810
- | `GreaterThanEqual` | `gte` | yes |
811
- | `LessThan` | `lt` | yes |
812
- | `LessThanEqual` | `lte` | yes |
813
- | `Between` | `gte` + `lte` | yes (tuple `[min, max]`) |
814
- | `NotBetween` | `not.gte` + `not.lte` | yes (tuple `[min, max]`) |
815
- | `IsNull` | `null` | no |
816
- | `IsNotNull` | `not: null` | no |
817
- | `IsTrue` | `true` | no |
818
- | `IsFalse` | `false` | no |
819
- | `Insensitive` | `mode: 'insensitive'` | combinator |
820
-
821
- `Insensitive` is a combinator and can be used together with another text filter:
822
-
823
- ```ts
824
- findByNameContainsInsensitive // { name: { contains: value, mode: 'insensitive' } }
825
- findByEmailStartsWithInsensitive // { email: { startsWith: value, mode: 'insensitive' } }
826
- findByNameInsensitive // { name: { equals: value, mode: 'insensitive' } }
827
- ```
828
-
829
- `Between` and `NotBetween` receive a **tuple `[minValue, maxValue]`**:
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")]);
457
+ Applied as suffixes to the field name inside the method (same idea as v1, one renamed suffix):
458
+
459
+ | Suffix | Meaning | Argument |
460
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
461
+ | _(none)_ | equality (`=`) | yes |
462
+ | `Not` | negation | yes |
463
+ | `In` | is one of | yes (array) |
464
+ | `NotIn` | is none of | yes (array) |
465
+ | `Contains` | substring match | yes |
466
+ | `NotContains` | negated substring match | yes |
467
+ | `StartsWith` | prefix match | yes |
468
+ | `NotStartsWith` | negated prefix match | yes |
469
+ | `EndsWith` | suffix match | yes |
470
+ | `NotEndsWith` | negated suffix match | yes |
471
+ | `GreaterThan` | `>` | yes |
472
+ | `GreaterThanEqual` | `>=` | yes |
473
+ | `LessThan` | `<` | yes |
474
+ | `LessThanEqual` | `<=` | yes |
475
+ | `Between` | inclusive range | yes (`[min, max]` tuple) |
476
+ | `NotBetween` | outside an inclusive range | yes (`[min, max]` tuple) |
477
+ | `IsNull` | field is `null` | no |
478
+ | `IsNotNull` | field is not `null` | no |
479
+ | `IsTrue` | field is `true` | no |
480
+ | `IsFalse` | field is `false` | no |
481
+ | `IgnoreCase` | case-insensitive combinator for text filters | no _(renamed from v1's `Insensitive`)_ |
482
+ | `Optional` | **explicitly** marks the field's argument as optional — it's already optional by default, so this suffix is itself optional and only used to make it explicit | — |
483
+
484
+ ```typescript
485
+ @DynamicMethod()
486
+ declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
487
+
488
+ @DynamicMethod()
489
+ declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
841
490
  ```
842
491
 
843
- The `Optional` suffix can be added to any field to make the argument optional:
844
-
845
- ```ts
846
- findByNameOptionalAndEmail // name is optional, email is required
847
- ```
848
-
849
- ---
850
-
851
492
  ### Logical operators
852
493
 
853
- | Operator | Usage in the name | Example |
854
- | --------- | ------------------------------ | ---------------------------------- |
855
- | `And` | between two fields | `findOneByIdAndEmail` |
856
- | `Or` | between two fields | `findByNameOrEmail` |
857
- | `AND` | separates a final `AND` block | `findByEmailOrNameANDActiveStatus` |
494
+ | Operator | Usage in the name | Example |
495
+ | -------- | ------------------------------- | --------------------------------------------------- |
496
+ | `And` | between two fields | `findOneByIdAndEmail` |
497
+ | `Or` | between two fields | `findByNameOrEmail` |
498
+ | `AND` | splits a final block into `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
858
499
 
859
- `AND` (in caps) has a specific rule:
500
+ `AND` (all caps) rules, same as v1: only one `AND` per method name is allowed; every field connected with `And` after it is nested inside `AND: []`; `Or` cannot appear after an `AND`.
860
501
 
861
- - Only **one** `AND` can exist per method.
862
- - All fields after `AND` are injected inside `AND: []`.
863
- - After an `AND`, there can't be an `Or`.
502
+ ### Relation filters
864
503
 
865
- Example:
504
+ Filter by fields of related entities. Internally these map to the `_some`/`_every`/`_none`/`_with`/`_without` operators of `VSRepoWhere` (see [`select` and `relations`](#select-and-relations) for the eager-loading counterpart).
866
505
 
867
- ```ts
868
- methods: {
869
- findOneByIdAndEmail: { map: true },
870
- findByNameOrEmail: { map: true },
871
- findFirstByIdOrEmailAndName: { map: true },
872
- findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
873
- }
506
+ | Suffix | Meaning | Restriction |
507
+ | -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
508
+ | `Some` | at least one related record matches | to-many relations only |
509
+ | `SomeField` | filters within the related records | to-many relations only |
510
+ | `Every` | every related record matches | to-many relations only (needs `Field` to be a real filter) |
511
+ | `EveryField` | filters within the related records | to-many relations only |
512
+ | `None` | no related record matches | to-many relations only |
513
+ | `NoneField` | filters within the related records | to-many relations only |
514
+ | `With` | related record exists | to-one relations only |
515
+ | `WithField` | filters a field within the related record | to-one relations only |
516
+ | `Without` | related record does not exist | to-one relations only |
517
+ | `WithoutField` | negated filter on a field of the related record | to-one relations only |
874
518
 
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
- ```
880
-
881
- Generates (`findOneByIdAndEmail`):
519
+ ```typescript
520
+ @DynamicMethod()
521
+ declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
882
522
 
883
- ```ts
884
- {
885
- id: 1,
886
- email: "john@email.com"
887
- }
523
+ @DynamicMethod()
524
+ declare findByProductsSome: () => Promise<User[]>;
888
525
  ```
889
526
 
890
- Generates (`findByNameOrEmail`):
891
-
892
- ```ts
893
- {
894
- OR: [
895
- { name: "John" },
896
- { email: "john@email.com" }
897
- ]
898
- }
899
- ```
527
+ ### Ordering, pagination and distinct
900
528
 
901
- Generates (`findFirstByIdOrEmailAndName`):
529
+ | Suffix | Effect |
530
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
531
+ | `Paginated` | Injects a `pagination` argument (`{ limit?, offset? }`) as the **penultimate** parameter (before the optional `MethodOptions`). |
532
+ | `Ordered` | Injects an `order: Ordering<T>` argument as the **penultimate** parameter (before the optional `MethodOptions`). |
533
+ | `OrderedAndPaginated` | Injects `order` as the antepenultimate, then `pagination` as the penultimate — both before `MethodOptions`. |
534
+ | `PaginatedAndOrdered` | Injects `pagination` as the antepenultimate, then `order` as the penultimate — both before `MethodOptions`. |
535
+ | `OrderBy<Field>Asc` / `OrderBy<Field>Desc` | **New in v2.** Bakes a fixed ordering directly into the method name — chain fields with `And` (e.g. `OrderByCreatedAtAscAndNameDesc`). No `order` argument needed. |
536
+ | `Distinct<Field>And<Field>...` | Bakes fixed `distinct` fields directly into the method name (only valid on `findBy`/`findWhere`-family methods). |
537
+ | `IgnoreConflicts` | On `createMany`/`createManyReturning`, skips records that would violate a unique constraint instead of throwing. _(Renamed from v1's `SkipDuplicates`.)_ |
902
538
 
903
- ```ts
904
- {
905
- OR: [
906
- { id: 1 },
907
- {
908
- email: "john@email.com",
909
- name: "John"
910
- }
911
- ]
912
- }
913
- ```
539
+ > ⚠️ **Parameter order:** `pagination` and `order` are always placed **before** the optional `MethodOptions<T>` last argument. When both `order` and `pagination` are present, their relative order follows the suffix name (`OrderedAndPaginated` → order, pagination; `PaginatedAndOrdered` → pagination, order).
914
540
 
915
- Generates (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
541
+ ```typescript
542
+ // Paginated: pagination is the penultimate param (before MethodOptions)
543
+ @DynamicMethod()
544
+ declare findByActiveOrderByCreatedAtDescPaginated:
545
+ (active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
916
546
 
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
- ```
547
+ // OrderedAndPaginated: order, then pagination, then MethodOptions
548
+ @DynamicMethod()
549
+ declare findByNameContainsIgnoreCaseOrderedAndPaginated:
550
+ (name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
929
551
 
930
- ---
552
+ @DynamicMethod()
553
+ declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
931
554
 
932
- ### Relation filters
555
+ // createManyReturning: same as createMany, but returns the created records
556
+ @DynamicMethod()
557
+ declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
933
558
 
934
- Allow filtering by fields of related models.
559
+ // findOne with no filter (equivalent to findOneOrThrow with no filter, but returns null instead of throwing)
560
+ @DynamicMethod()
561
+ declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
562
+ ```
935
563
 
936
- > [!IMPORTANT]
564
+ > ⚠️ **Precedence between `Distinct` and `OrderBy`:** when both are used in the same method name, **`Distinct` must come before `OrderBy`**:
937
565
  >
938
- > - **Relation typing**: For TypeScript to recognize the types of relation fields in dynamic methods, the generic entity type passed to `setupVSRepo` must include the structured relations (e.g. using Prisma's `UserGetPayload<{ include: { profile: true, posts: true } }>`).
939
- > - **Suffix compatibility**:
940
- > - The `Some`, `Every`, and `None` suffixes only work for **to-many** relations (`many-to-many` and `one-to-many`).
941
- > - The `With` and `Without` suffixes only work for **to-one** relations (`one-to-one` and `many-to-one`).
942
-
943
- | Relation suffix | Prisma operator | Note |
944
- | --------------------- | ----------------- | ----------------------------------------------------- |
945
- | `Some` | `some: {}` | Relation has *some* record |
946
- | `SomeField` | `some.field` | Filters within the relation's records |
947
- | `EveryField` | `every.field` | Filters within the relation's records |
948
- | `None` | `none: {}` | Relation has *no* records |
949
- | `NoneField` | `none.field` | Filters within the relation's records |
950
- | `With` | `is: {}` | Relation exists (not null) |
951
- | `WithField` | `is.field` | Filters a field within the relation |
952
- | `Without` | `isNot: {}` | Relation doesn't exist (is null) |
953
- | `WithoutField` | `isNot.field` | Filters a field within the relation with negation |
954
-
955
- Considering `user` with a to-one relation `profile` and a to-many relation `posts`:
956
-
957
- ```ts
958
- methods: {
959
- // to-many (posts)
960
- findByPostsSome: { map: true }, // has at least one post
961
- findByPostsSomeTitle: { map: true }, // has at least one post with that title
962
- findByPostsEveryPublishedIsTrue:{ map: true }, // all posts are published
963
- findByPostsNone: { map: true }, // has no posts
964
- findByPostsNoneTitle: { map: true }, // no post has that title
965
-
966
- // to-one (profile)
967
- findByProfileWith: { map: true }, // has a profile (not null)
968
- findByProfileWithBio: { map: true }, // has a profile with that bio
969
- findByProfileWithout: { map: true }, // has no profile (is null)
970
- findByProfileWithoutBio: { map: true }, // has a profile, but with a different bio than the one provided
971
- }
566
+ > ```typescript
567
+ > @DynamicMethod()
568
+ > declare findByActiveDistinctNameOrderByCreatedAtDesc:
569
+ > (active: boolean) => Promise<User[]>;
570
+ > ```
571
+ >
572
+ > Putting `OrderBy` before `Distinct` (e.g. `findByActiveOrderByCreatedAtDescDistinctName`) is not a valid pattern and won't be parsed as expected.
972
573
 
973
- await userRepository.findByPostsSome();
974
- await userRepository.findByPostsSomeTitle("My first post");
975
- await userRepository.findByPostsEveryPublishedIsTrue();
976
- await userRepository.findByPostsNone();
977
- await userRepository.findByPostsNoneTitle("Draft");
574
+ ### Decorator options
978
575
 
979
- await userRepository.findByProfileWith();
980
- await userRepository.findByProfileWithBio("Hello, world!");
981
- await userRepository.findByProfileWithout();
982
- await userRepository.findByProfileWithoutBio("Old bio");
983
- ```
576
+ `@DynamicMethod<T>(options?)` accepts:
984
577
 
985
- Generates (`findByPostsSomeTitle`):
578
+ | Option | Type | Description |
579
+ | ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
580
+ | `proxyTo` | `string` | Redirects the method's logic to another valid dynamic-method pattern — useful for names that don't follow the naming convention. |
581
+ | `injectOrdering` | `Ordering<T>` | Fixed ordering automatically injected, overriding the repository's `defaultOrdering`. |
986
582
 
987
- ```ts
988
- {
989
- posts: {
990
- some: { title: "My first post" }
991
- }
992
- }
583
+ ```typescript
584
+ @DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
585
+ declare findByStatus: (status: string) => Promise<User[]>;
993
586
  ```
994
587
 
995
- Generates (`findByPostsEveryPublishedIsTrue`):
996
-
997
- ```ts
998
- {
999
- posts: {
1000
- every: { published: true }
1001
- }
1002
- }
1003
- ```
588
+ ---
1004
589
 
1005
- Generates (`findByProfileWithBio`):
590
+ ## Query methods (raw SQL)
1006
591
 
1007
- ```ts
1008
- {
1009
- profile: {
1010
- is: { bio: "Hello, world!" }
1011
- }
1012
- }
1013
- ```
592
+ `@QueryMethod` bypasses the name-parsing engine entirely and executes a raw SQL statement through the adapter's `query()` method. Use `$1`, `$2`, ... placeholders — never interpolate values directly into the SQL string.
1014
593
 
1015
- Generates (`findByProfileWithout`):
594
+ ```typescript
595
+ class UserRepository extends VSRepository<User, string> {
596
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1')
597
+ declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
1016
598
 
1017
- ```ts
1018
- {
1019
- profile: {
1020
- isNot: {}
1021
- }
599
+ @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
600
+ declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
1022
601
  }
1023
602
  ```
1024
603
 
1025
- > `Some`, `None`, `With`, and `Without` (without a field) don't receive an argument — the whole relation is tested for the existence of records (`some`/`none`) or for being `null`/not `null` (`is`/`isNot`). The `SomeField`, `EveryField`, `NoneField`, `WithField`, and `WithoutField` variants receive the filtered field's value as an argument.
1026
-
1027
- ---
1028
-
1029
- ### Pagination and ordering suffixes
1030
-
1031
- Applied at the **end** of the method name, they automatically inject the pagination and ordering arguments.
1032
-
1033
- | Suffix | Additional arguments |
1034
- | ------------------------ | -------------------------------|
1035
- | `Paginated` | `(pagination)` |
1036
- | `Ordered` | `(order)` |
1037
- | `OrderedAndPaginated` | `(order, pagination)` |
1038
- | `PaginatedAndOrdered` | `(pagination, order)` |
1039
-
1040
- For `createMany` and `createManyAndReturn`, the `SkipDuplicates` suffix is available:
1041
-
1042
- | Suffix | Effect |
1043
- | ------------------- | ------------------------------------------ |
1044
- | `SkipDuplicates` | Skips duplicate records during insertion |
1045
-
1046
- ---
1047
-
1048
- ### Distinct
604
+ | Option | Type | Default | Description |
605
+ | ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
606
+ | `modifying` | `boolean` | `false` | When `true`, runs as `INSERT`/`UPDATE`/`DELETE` and the method resolves to the number of affected rows. When `false`, runs as a read query and resolves to the declared return type. |
1049
607
 
1050
- The `Distinct` suffix lets you get only unique records based on one or more fields, equivalent to Prisma's `distinct` option.
608
+ Query methods accept `{ args, db? }` at the call site — `db` lets them participate in a `transaction()` block just like base and dynamic methods.
1051
609
 
1052
- To use it, put `Distinct` in the method name (after the field filters, if any) followed by the desired fields separated by `And`. The first character of each field must be uppercase, just like in regular field filters.
610
+ ### Ad-hoc raw queries with `query()`
1053
611
 
1054
- ```ts
1055
- methods: {
1056
- // Returns unique users combining "age" and "role" (no field filter)
1057
- findManyDistinctAgeAndRole: { map: true },
612
+ For one-off raw SQL that doesn't warrant declaring a `@QueryMethod` on the repository class, call `query()` directly — it's available on every `VSRepository` instance and goes through the same adapter's `query()` implementation under the hood:
1058
613
 
1059
- // Distinct combined with the Paginated suffix
1060
- findManyDistinctNamePaginated: { map: true },
1061
-
1062
- // Distinct combined with a field filter (name) — filters by name and then applies distinct on role
1063
- findManyByNameDistinctRole: { map: true },
1064
- },
614
+ ```typescript
615
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
1065
616
  ```
1066
617
 
1067
- ```ts
1068
- // No arguments: the distinct fields are already fixed in the method name
1069
- await userRepository.findManyDistinctAgeAndRole();
1070
-
1071
- // The pagination argument still works normally
1072
- await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
618
+ ```typescript
619
+ const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
620
+ args: ["maria@email.com"],
621
+ });
1073
622
 
1074
- // The "name" field filter is still passed normally as an argument
1075
- await userRepository.findManyByNameDistinctRole("John");
623
+ const affectedRows = await userRepository.query<number>(
624
+ 'UPDATE "user" SET active = true WHERE id = $1',
625
+ { args: ["123"], modifying: true },
626
+ );
1076
627
  ```
1077
628
 
1078
- > The fields specified after `Distinct` are resolved from the method name at build time — they **don't** become runtime arguments, unlike regular field filters.
629
+ | Option | Type | Default | Description |
630
+ | ----------- | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
631
+ | `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
632
+ | `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
633
+ | `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
1079
634
 
1080
- `Distinct` is available on prefixes that read multiple or single records: `findMany`, `findManyBy`, `findFirst`, `findFirstBy`, `findFirstOrThrow`, `findFirstOrThrowBy`, `findBy`, `findOneBy`, `findWhere`, `findOneWhere`, `findListWhere`, `existsBy`, and `existsWhere`.
635
+ Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
1081
636
 
1082
637
  ---
1083
638
 
1084
- ### Method configuration
1085
-
1086
- Each entry in `methods` accepts the following options:
639
+ ## Transactions
1087
640
 
1088
- | Option | Type | Default | Description |
1089
- | --------------------- | ---------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
1090
- | `map` | `boolean` | — | **Required.** Defines whether the method will be exposed on the repository. |
1091
- | `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combines with `requiredWhere`. `overwrite` ignores `requiredWhere`. |
1092
- | `selectModel` | `keyof SelectModels \| false` | — | Overrides `defaultSelectModel` for this method. |
1093
- | `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Only for `findBy`. `'one'` returns `T \| null`; `'list'` returns `T[]`. |
1094
- | `proxyTo` | `Valid method pattern` | — | Delegates the logic to another valid method pattern. |
1095
- | `pushWhere` | `WhereModel<M>` | — | Extra `where` added to the query in addition to `requiredWhere`. |
1096
- | `injectOrdering` | `OrderingModel<M>` | — | Fixed ordering automatically injected into the query. |
1097
- | `injectPagination` | `PaginationModel<M>` | — | Fixed pagination automatically injected into the query. |
1098
- | `query` | `{ value: string; modifying?: boolean }` | — | Turns the method into a **Query Method** (raw SQL). Ignores every other option above — see [Query Methods](#query-methods). |
641
+ All methods (base, dynamic, and query) accept `options.db` to participate in a shared transaction:
1099
642
 
1100
- ---
643
+ ```typescript
644
+ await userRepository.transaction(async tx => {
645
+ const user = await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1101
646
 
1102
- ### Aggregate and 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);
647
+ await userLogsRepository.save(
648
+ { action: "User created", data: { userId: user.id } },
649
+ { db: tx },
650
+ );
651
+ });
1113
652
  ```
1114
653
 
1115
- > [!NOTE]
1116
- > These methods must have exactly these names (`aggregate` and `groupBy`).
1117
- > Unlike the other dynamic methods, they receive native Prisma arguments and **ignore** the `selectModels`, `pushWhere`, and `requiredWhere` configurations.
1118
-
1119
- ---
1120
-
1121
- ### Query Methods
654
+ Different repositories can share the same transaction as long as their adapters point to the same underlying ORM connection.
1122
655
 
1123
- Query Methods let a method run **raw SQL** directly, completely bypassing the dynamic-method name parser. They're useful for complex queries (heavy joins, CTEs, database-specific functions) that aren't practical to express with the standard prefixes/suffixes.
656
+ `transaction()` accepts an optional `VSRepoTransactionOptions` as its second argument:
1124
657
 
1125
- Internally, VSRepository executes the SQL through Prisma using `$queryRawUnsafe` (for reads) or `$executeRawUnsafe` (for writes), and the values in the `args` array are passed as **positional parameters** (`$1`, `$2`, ...) — the same prepared-statement technique Prisma itself uses. This means the values are never concatenated into the SQL string, which is what actually prevents SQL injection.
658
+ ```typescript
659
+ import { TransactionIsolationLevel } from "vsrepo";
1126
660
 
1127
- > [!WARNING]
1128
- > `$1`, `$2`, ... in your SQL must always represent **values** (data parameters), never column names, table names, or dynamic SQL fragments. Identifier names (columns/tables) can't be passed as a positional parameter — if your method needs to vary those, build the SQL from a fixed, known set of options in your own code, never from untrusted input.
1129
-
1130
- ```ts
1131
- const userRepository = setupVSRepo<User, "user">()({
1132
- tableName: "user",
1133
- pkName: "id",
1134
- methods: {
1135
- // Read query method (non-modifying)
1136
- findActiveUsersRaw: {
1137
- map: true,
1138
- query: {
1139
- value: 'SELECT * FROM "user" WHERE active = $1',
1140
- },
1141
- },
1142
-
1143
- // Write query method (modifying: true)
1144
- deactivateUsersOlderThanRaw: {
1145
- map: true,
1146
- query: {
1147
- value: 'UPDATE "user" SET active = false WHERE "createdAt" < $1',
1148
- modifying: true,
1149
- },
661
+ await userRepository.transaction(
662
+ async tx => {
663
+ await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1150
664
  },
1151
- },
1152
- }).build(prisma);
665
+ { isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
666
+ );
1153
667
  ```
1154
668
 
1155
- **Calling a Query Method:**
1156
-
1157
- Every Query Method takes a single argument shaped as `{ args: [...], db? }`:
1158
-
1159
- ```ts
1160
- // Non-modifying: returns 'any' by default, but accepts a generic to
1161
- // infer/assert the return type right at the call site
1162
- const activeUsers = await userRepository.findActiveUsersRaw<User[]>({
1163
- args: [true],
1164
- });
1165
-
1166
- // Modifying: always returns 'number' (count of affected rows)
1167
- const affected = await userRepository.deactivateUsersOlderThanRaw({
1168
- args: [new Date("2024-01-01")],
1169
- });
1170
-
1171
- // Participating in a transaction, via 'db'
1172
- await userRepository.prisma.$transaction(async (tx) => {
1173
- await userRepository.deactivateUsersOlderThanRaw({
1174
- args: [new Date("2024-01-01")],
1175
- db: tx,
1176
- });
1177
- });
1178
- ```
669
+ | Option | Type | Description |
670
+ | ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
671
+ | `isolationLevel` | `TransactionIsolationLevel` | Isolation level to use for the transaction. Defaults to the underlying ORM's default. |
672
+ | `timeoutMs` | `number` | Maximum time (in ms) the transaction is allowed to run before being aborted. |
1179
673
 
1180
- | Option | Type | Default | Description |
1181
- | ------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1182
- | `value` | `string` | — | **Required.** Raw SQL to execute. Use `$1`, `$2`, ... for the placeholders of the values in `args`. |
1183
- | `modifying` | `boolean` | `false` | When `true`, executes via `$executeRawUnsafe` and the method always resolves to `number`. When `false`, executes via `$queryRawUnsafe` and the method resolves to `TReturn` (`any` by default, inferable via a generic at the call site). |
1184
-
1185
- > [!NOTE]
1186
- > Unlike the other dynamic methods, Query Methods **completely ignore** `selectModels`, `requiredWhere`, `pushWhere`, `whereType`, `injectOrdering`, `injectPagination`, and `proxyTo` — none of that applies, since there's no name parsing or `where`/`select` assembly by VSRepository. Free-form method names (outside the `findBy`, `updateBy`, etc. patterns) also **don't** require `proxyTo`.
1187
-
1188
- The same functionality is available in the class-based approach via the `@QueryMethod` decorator — see [README-DynamicRepo.md](./README-DynamicRepo.md#the-querymethod-decorator).
674
+ `TransactionIsolationLevel` mirrors the standard SQL isolation levels: `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`. Support for a given level depends on the adapter/underlying ORM and database.
1189
675
 
1190
676
  ---
1191
677
 
1192
- ## Relations in save
1193
-
1194
- Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
1195
-
1196
- ```ts
1197
- import type { Prisma } from "../../generated/prisma/client";
678
+ ## Utility types
1198
679
 
1199
- type User = Prisma.userGetPayload<{
1200
- include: { profile: true; posts: true };
1201
- }>;
680
+ Beyond the entity-shaping types covered above (`VSRepoSelect`, `VSRepoRelations`, `VSRepoWhere`), VSRepository exports a set of utility types. They show up throughout the sections above, but here's a consolidated reference. All of them are part of the public API and can be imported directly:
1202
681
 
1203
- const userRepository = setupVSRepo<User, "user">()(({
1204
- tableName: "user",
1205
- pkName: "id",
682
+ ```typescript
683
+ import type {
684
+ MethodOptions,
685
+ RestrictMethodOptions,
686
+ Pagination,
687
+ Ordering,
688
+ OrderByField,
689
+ SortDirection,
690
+ SeeMode,
691
+ DeepPartial,
692
+ CountResult,
693
+ QueryMethodArg,
694
+ KeysOfType,
695
+ NumericKeys,
696
+ NumericLike,
697
+ DecimalLike,
698
+ Primitive,
699
+ VSRepoWhere,
700
+ VSRepoOrmTypes,
701
+ VSRepoTransactionOptions,
702
+ TransactionIsolationLevel,
703
+ } from "vsrepo";
704
+ ```
705
+
706
+ | Type | Description | Used by |
707
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
708
+ | `MethodOptions<T, K>` | Options accepted as the last argument of most base and dynamic methods: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
709
+ | `RestrictMethodOptions<T, K>` | Narrowed `MethodOptions<T, K>` exposing only `see`/`db` — used by methods that don't shape/return an `Entity` (`total`, `has`, `sum`, `average`, `min`, `max`, `removeList`, `softRemoveList`, `restoreList`). | [Base methods](#base-methods), [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
710
+ | `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
711
+ | `Ordering<T>` / `OrderByField<T>` / `SortDirection` | Ordering shape accepted by `getAll`, `defaultOrdering` and `injectOrdering`, and by `Ordered` dynamic methods. A single object or a chained array; nested objects order to-one relations. | [Constructor options](#constructor-options), [Decorator options](#decorator-options), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
712
+ | `SeeMode` | `"active" \| "removed" \| "all"` — controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
713
+ | `DeepPartial<T>` | Recursively makes every property of `T` optional, including nested objects and array elements. | `save`, `saveList`, `patch`, `merge`, and every write method on `VSRepoAdapter`. |
714
+ | `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
715
+ | `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
716
+ | `KeysOfType<T, K>` | Extracts the keys of `T` whose value type is assignable to `K`. | Constrains `pkName` in [Constructor options](#constructor-options) to fields of the entity matching the configured primary-key type. |
717
+ | `NumericKeys<T>` | Extracts the keys of `T` whose (non-nullable) value type is assignable to `NumericLike`. Nullable numeric fields (`number \| null`) are included. | Constrains `field` in [Atomic and aggregate methods](#atomic-and-aggregate-methods) (`increment`, `sum`, etc). |
718
+ | `NumericLike` | `number \| bigint \| DecimalLike`. | [Atomic and aggregate methods](#atomic-and-aggregate-methods). |
719
+ | `DecimalLike` | Structural shape of an arbitrary-precision decimal value (`{ toNumber(): number; decimalPlaces(): number }`), matching e.g. Prisma's `Prisma.Decimal` without importing it directly. | [Which fields are eligible](#which-fields-are-eligible). |
720
+ | `Primitive` | Union of scalar types (`string \| number \| boolean \| bigint \| symbol \| undefined \| null \| Date`) treated as leaves — not relations — when walking an entity's shape. | Used by `Ordering<T>` to tell scalar fields apart from relation fields. |
721
+ | `VSRepoWhere<T>` | ORM-agnostic filter type accepted by `*Where` dynamic methods (e.g. `findWhere`, `findOneWhere`, `updateWhere`). Supports field filters, logical operators (`AND`/`OR`/`NOT`), and relation filters. | [`findWhere`, `findOneWhere` and other `*Where` prefixes](#available-prefixes). |
722
+ | `VSRepoOrmTypes` | `{ dbClient; dbTransaction }` — describes your ORM's client/transaction types. Passed as the third generic to `VSRepository<Entity, PKType, OrmTypes>` to type `getDbClient()`, `transaction()` and the `db` option instead of `any`. | [Creating a repository](#creating-a-repository). |
723
+ | `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options accepted as the second argument of `transaction()`. | [Transactions](#transactions). |
724
+ | `TransactionIsolationLevel` | Enum of standard SQL isolation levels (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) accepted by `VSRepoTransactionOptions.isolationLevel`. | [Transactions](#transactions). |
725
+
726
+ ### `DeepPartial<T>`
727
+
728
+ Recursively makes all properties optional, walking into nested objects and array elements — unlike TypeScript's built-in `Partial<T>`, which only makes the top level optional:
729
+
730
+ ```typescript
731
+ type User = { id: string; name: string; address: { city: string; zip: string } };
732
+
733
+ const patch: DeepPartial<User> = {
734
+ address: { city: "São Paulo" }, // zip can be omitted; city keeps its type
735
+ };
1206
736
 
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",
1217
- },
1218
- },
1219
- }).build(prisma);
737
+ await userRepository.patch(id, patch);
1220
738
  ```
1221
739
 
1222
- **Relation modes:**
1223
-
1224
- | Mode | Relation |
1225
- | ----- | -------------- |
1226
- | `oto` | one-to-one |
1227
- | `otm` | one-to-many |
1228
- | `mto` | many-to-one |
1229
- | `mtm` | many-to-many |
740
+ ### `KeysOfType<T, K>`
1230
741
 
1231
- **Restrictions:**
742
+ Filters an object type down to the keys whose value matches a given type — this is what lets `pkName` accept only fields of the entity that are actually assignable to the repository's primary-key type:
1232
743
 
1233
- | Restriction | Behavior on update |
1234
- | ------------ | ------------------------------------------------------ |
1235
- | `set` | Fully replaces (removes the ones that weren't sent) |
1236
- | `add` | Adds/updates without removing existing ones |
744
+ ```typescript
745
+ type User = { id: string; age: number; name: string };
746
+ type StringKeys = KeysOfType<User, string>; // "id" | "name"
747
+ ```
1237
748
 
1238
- > [!WARNING]
1239
- > **`set` means different things depending on the relation's `mode` — and this can cause data loss if you're not careful.**
1240
- >
1241
- > In relations where the related record **belongs** to the parent record (`oto` and `otm`), "removing the ones that weren't sent" means **deleting the record from the database** (`delete`/`deleteMany`). In relations where the related record is **independent** (`mto` and `mtm`), "removing" just means **unlinking** (`disconnect`/`set: []`) — the related record continues to exist in the database, it just stops pointing to the parent (or being in the join table).
1242
- >
1243
- > | Mode | `restriction: "set"` when an item is omitted | Does the item continue to exist in the database? |
1244
- > | ----- | ------------------------------------------------ | ----------------------------------------------------- |
1245
- > | `oto` | Passing `null` in the field → **deletes** the related record (`delete: true`) | No |
1246
- > | `otm` | Items outside the sent list → **deleted** (`deleteMany` with `notIn`) | No |
1247
- > | `mto` | Passing `null` in the field (with `nullable: true`) → **unlinks** (`disconnect: true`) | Yes |
1248
- > | `mtm` | Items outside the sent list → **unlinked** from the join table (`set: []`) | Yes |
1249
- >
1250
- > Practical example: if `posts` is `otm` with `restriction: "set"`, a `save`/`patch` that sends the user with only 2 of the 5 existing posts will **delete the other 3 posts from the database**, not just unlink them from the user. If the expected behavior is just to unlink without deleting, use `restriction: "add"` (which never removes anything) and handle removal manually.
749
+ ### `Ordering<T>`
1251
750
 
1252
- **`mto` relation with nullable:**
751
+ Accepts either a single ordering object or an array of them, applied in the order they're declared:
1253
752
 
1254
- Use `nullable` (lowercase) to allow unlinking a many-to-one relation:
753
+ ```typescript
754
+ const order: Ordering<User> = { createdAt: "desc" };
755
+ const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
1255
756
 
1256
- ```ts
1257
- relations: {
1258
- category: {
1259
- pk: "id",
1260
- mode: "mto",
1261
- restriction: "set",
1262
- nullable: true, // allows passing null to unlink
1263
- },
1264
- }
757
+ await userRepository.getAll({ order: chained });
1265
758
  ```
1266
759
 
1267
760
  ---
1268
761
 
1269
- ## Transactions
762
+ ## Writing your own adapter
763
+
764
+ Because the core is ORM-agnostic and ships without a bundled adapter, adding support for an ORM/database — whether that's a stopgap for your own project or a candidate for a future `@vsrepo/*-adapter` package — means implementing the `VSRepoAdapter<T>` abstract class:
765
+
766
+ ```typescript
767
+ export abstract class VSRepoAdapter<T> {
768
+ abstract runInTransaction<R>(
769
+ fn: (tx: any) => Promise<R>,
770
+ options?: VSRepoTransactionOptions,
771
+ ): Promise<R>;
772
+ abstract getDbClient(): any;
773
+ abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
774
+ abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
775
+ abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
776
+ abstract findMany(
777
+ where: VSRepoWhere<T>,
778
+ options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
779
+ ): Promise<T[]>;
780
+ abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
781
+ abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
782
+ abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
783
+ abstract createMany(
784
+ objs: DeepPartial<T>[],
785
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
786
+ ): Promise<CountResult>;
787
+ abstract createManyReturning(
788
+ objs: DeepPartial<T>[],
789
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
790
+ ): Promise<T[]>;
791
+ abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
792
+ abstract deleteMany(
793
+ where: VSRepoWhere<T>,
794
+ options?: AdapterMethodOptions<T>,
795
+ ): Promise<CountResult>;
796
+ abstract deleteManyReturning(
797
+ where: VSRepoWhere<T>,
798
+ options?: AdapterMethodOptions<T>,
799
+ ): Promise<T[]>;
800
+ abstract update(
801
+ where: VSRepoWhere<T>,
802
+ obj: DeepPartial<T>,
803
+ options?: AdapterMethodOptions<T>,
804
+ ): Promise<T>;
805
+ abstract updateMany(
806
+ where: VSRepoWhere<T>,
807
+ obj: DeepPartial<T>,
808
+ options?: AdapterMethodOptions<T>,
809
+ ): Promise<CountResult>;
810
+ abstract updateManyReturning(
811
+ where: VSRepoWhere<T>,
812
+ obj: DeepPartial<T>,
813
+ options?: AdapterMethodOptions<T>,
814
+ ): Promise<T[]>;
815
+ abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
816
+ abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
817
+ abstract merge<K>(
818
+ where: VSRepoWhere<T>,
819
+ obj: DeepPartial<T>,
820
+ options?: AdapterMethodOptions<T>,
821
+ ): Promise<K & T>;
822
+ abstract upsert(
823
+ where: VSRepoWhere<T>,
824
+ create: DeepPartial<T>,
825
+ update: DeepPartial<T>,
826
+ options?: AdapterMethodOptions<T>,
827
+ ): Promise<T>;
828
+
829
+ abstract incrementOne<K extends NumericKeys<T>>(
830
+ field: K,
831
+ value: NonNullable<T[K]>,
832
+ where: VSRepoWhere<T>,
833
+ options?: AdapterMethodOptions<T>,
834
+ ): Promise<T>;
835
+ abstract decrementOne<K extends NumericKeys<T>>(
836
+ field: K,
837
+ value: NonNullable<T[K]>,
838
+ where: VSRepoWhere<T>,
839
+ options?: AdapterMethodOptions<T>,
840
+ ): Promise<T>;
841
+ abstract multiplyOne<K extends NumericKeys<T>>(
842
+ field: K,
843
+ value: NonNullable<T[K]>,
844
+ where: VSRepoWhere<T>,
845
+ options?: AdapterMethodOptions<T>,
846
+ ): Promise<T>;
847
+ abstract divideOne<K extends NumericKeys<T>>(
848
+ field: K,
849
+ value: NonNullable<T[K]>,
850
+ where: VSRepoWhere<T>,
851
+ options?: AdapterMethodOptions<T>,
852
+ ): Promise<T>;
853
+ abstract sum(
854
+ field: NumericKeys<T>,
855
+ where?: VSRepoWhere<T>,
856
+ options?: AdapterMethodOptions<T>,
857
+ ): Promise<number | null>;
858
+ abstract average(
859
+ field: NumericKeys<T>,
860
+ where?: VSRepoWhere<T>,
861
+ options?: AdapterMethodOptions<T>,
862
+ ): Promise<number | null>;
863
+ abstract min(
864
+ field: NumericKeys<T>,
865
+ where?: VSRepoWhere<T>,
866
+ options?: AdapterMethodOptions<T>,
867
+ ): Promise<number | null>;
868
+ abstract max(
869
+ field: NumericKeys<T>,
870
+ where?: VSRepoWhere<T>,
871
+ options?: AdapterMethodOptions<T>,
872
+ ): Promise<number | null>;
873
+ }
874
+ ```
1270
875
 
1271
- All methods accept `options.db` to participate in a transaction:
876
+ `VSRepository` never talks to the ORM directly — it only calls these methods with an already-resolved `VSRepoWhere<T>` and `AdapterMethodOptions<T>`. Once an adapter implements this contract, every base method, dynamic method, and query method works against it automatically. For a full, working implementation, see the external [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) repo.
1272
877
 
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
- );
878
+ ### Logging from your adapter
1279
879
 
1280
- await userLogsRepository.save(
1281
- { action: "User registration", data: { registeredUser: user.id } },
1282
- { db: tx }
1283
- );
1284
- });
1285
- ```
880
+ `vsrepo` exports the same `VSLogger` class the core uses internally, so your adapter can log in the same format/style (timestamps, colored level labels, slow-operation warnings) instead of rolling its own:
1286
881
 
1287
- For `saveList` and `patchList`, the `db` field must be a `DbTransaction`:
882
+ ```typescript
883
+ import { VSLogger, VSLogLevel } from "vsrepo";
1288
884
 
1289
- ```ts
1290
- await prisma.$transaction(async (tx) => {
1291
- // CORRECT: tx is a DbTransaction
1292
- const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
885
+ export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
886
+ private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
1293
887
 
1294
- await userLogsRepository.save(
1295
- { action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
1296
- { db: tx }
1297
- );
1298
- });
888
+ async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
889
+ const start = this.logger.startPerformLog("adapter findOne");
890
+ try {
891
+ // ... talk to the ORM ...
892
+ this.logger.endPerformLog(start);
893
+ return result;
894
+ } catch (err) {
895
+ this.logger.endPerformLog(start);
896
+ this.logger.logError("adapter findOne failed", err);
897
+ throw err;
898
+ }
899
+ }
900
+ }
1299
901
  ```
1300
902
 
1301
- ---
1302
-
1303
- ## Extending a repository
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
- },
903
+ | Method | Description |
904
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
905
+ | `new VSLogger(logLevel, name, slowThresholdMs?)` | Creates a logger; `name` prefixes every line, `slowThresholdMs` defaults to 300. |
906
+ | `logDebug/logInfo/logWarn(text, obj?)` | Logs at the given level if `logLevel` allows it; `obj` is appended as pretty-printed JSON. |
907
+ | `logError(text, err?)` | Logs at `ERROR`; if `err` is an `Error`, only `name`/`message`/`stack`/`cause` are logged. |
908
+ | `startPerformLog(operation)` / `endPerformLog(data)` | Bracket a block to log its duration, escalating to `WARN` if it exceeds `slowThresholdMs`. |
909
+ | `getLogLevel()` | Returns the logger's configured `VSLogLevel`. |
1318
910
 
1319
- activateMultiple: async (ids: string[]) => {
1320
- return repo.patchList(ids.map(id => [id, { active: true }]));
1321
- },
1322
- }));
1323
- ```
911
+ This is purely a convenience for adapter authors — nothing in the core requires your adapter to use it.
1324
912
 
1325
913
  ---
1326
914
 
1327
915
  ## Error handling
1328
916
 
1329
- VSRepository throws `VSRepoError` and its subclasses in specific situations (Prisma errors are not overridden):
917
+ v2 simplifies the error hierarchy from v1: instead of several subclasses, there's a base `VSRepoError` class carrying a `type: VSRepoErrorType`, plus a dedicated `VSRepoAdapterError` subclass (see below) for failures coming from the underlying ORM/database.
1330
918
 
1331
- ```ts
1332
- import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
919
+ ```typescript
920
+ import { VSRepoError } from "vsrepo";
1333
921
 
1334
922
  try {
1335
- const user = await userRepository.getOrThrow("id-that-does-not-exist");
923
+ await userRepository.get(id);
1336
924
  } 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
- }
925
+ if (error instanceof VSRepoError) {
926
+ console.error(`[${error.type}] ${error.message}`);
927
+ }
1344
928
  }
1345
929
  ```
1346
930
 
1347
- **Available subclasses:**
1348
-
1349
- | Class | When it's thrown |
1350
- | ----------------------- | ------------------------------------------------------------------------ |
1351
- | `VSRepoConfigError` | Invalid configuration in `setupVSRepo` |
1352
- | `VSRepoBuildError` | Invalid method name, field type, or configuration in `build` |
1353
- | `VSRepoExtendError` | Invalid argument in `extend` |
1354
- | `VSRepoDecoratorError` | Invalid argument passed to `@DynamicMethod` or `@QueryMethod` |
1355
- | `VSRepoRuntimeError` | Runtime error during an operation |
1356
-
1357
- `VSRepoRuntimeError` has a `code: VSRepoRuntimeErrorCode` property for programmatic identification, instead of having to parse the (human-readable, and possibly localized) message:
1358
-
1359
- | Code | Meaning |
1360
- | ---- | ------- |
1361
- | `"65706"` | A required argument is missing or has an invalid shape — e.g. a missing `pk`, a `pks`/`objs`/`tuples` that isn't an array, or an `options`/`obj` that isn't a valid object. |
1362
- | `"20727"` | No record was found for the provided primary key (`getOrThrow` when fetching the base record). |
1363
- | `"67542"` | Validation (zod) of a method's `options`, or of a `@QueryMethod` argument, failed. |
1364
- | `"91868"` | A relation passed to `save`/`patch`/`merge` has an invalid shape for the configured `mode`/`restriction` (e.g. `null` on a `mtm`/`otm` relation, or an array on a `oto`/`mto` relation). |
1365
- | `"48670"` | A dynamic method (`config.methods`) was called with fewer positional arguments than its `where` fields require. |
1366
-
1367
- ---
1368
-
1369
- ## Utility types
1370
-
1371
- ### Client types
1372
-
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
- ```
1380
-
1381
- ### Soft-delete visibility type
1382
-
1383
- ```ts
1384
- import type { SeeMode } from "../../generated/vsrepo";
1385
-
1386
- type SeeMode = "active" | "removed" | "all";
1387
- ```
1388
-
1389
- ### Types derived from the Prisma model
1390
-
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";
1403
- ```
1404
-
1405
- ### Method options types
1406
-
1407
- ```ts
1408
- import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
1409
-
1410
- // MethodOptions<S, IM> — options passed into the repository's methods
1411
- type Opts = MethodOptions<"public" | "minimal", "withPosts">;
1412
-
1413
- // MethodOptionsModel<TRepo> — derived from a configured VSRepository instance
1414
- const userVSRepo = setupVSRepo<User, "user">()(config);
1415
- type OptsModel = MethodOptionsModel<typeof userVSRepo>;
1416
- ```
1417
-
1418
- > The second parameter of `MethodOptions` (`IM`) represents the valid keys of `includeModels`. When provided, `selectModel` and `includeModel` become mutually exclusive in the type — it's not possible to pass both in the same call.
1419
- >
1420
- > `MethodOptions` also accepts two further generic parameters, `RI` and `RS`, for typing raw `include` and raw `select` respectively (both default to `never`, meaning they're not accepted unless explicitly typed): `MethodOptions<"public", "withPosts", "user", IncludeModel<"user">, SelectModel<"user">>`. `MethodOptionsModel`, derived directly from a configured repository, does not expose `RI`/`RS` — use `MethodOptions` directly if you need to type raw `include`/`select` options.
931
+ | `VSRepoErrorType` | Raised when |
932
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
933
+ | `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
934
+ | `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
935
+ | `DYNAMIC` | A resolved dynamic method failed at runtime (e.g. missing arguments). |
936
+ | `VALIDATOR` | Invalid method options or arguments were detected during validation. |
937
+ | `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
938
+ | `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database — always thrown as `VSRepoAdapterError`. |
1421
939
 
1422
- ### Configuration types
940
+ ### `VSRepoAdapterError` and `AdapterErrorCode`
1423
941
 
1424
- ```ts
1425
- import type {
1426
- MethodConfig,
1427
- RepoConfig,
1428
- BuildConfig,
1429
- RepositoryRelations,
1430
- ExtractRelationConfig,
1431
- } from "../../generated/vsrepo";
1432
- ```
942
+ When an adapter talks to the underlying ORM/database and that operation fails, the adapter wraps the failure in a `VSRepoAdapterError` — a subclass of `VSRepoError` with `type: VSRepoErrorType.ADAPTER`. It carries a **stable, adapter-agnostic** `code: AdapterErrorCode` plus the raw error thrown by the ORM/driver, so callers can react to failures without depending on any single ORM's error shape:
1433
943
 
1434
- ### Built repository type
944
+ ```typescript
945
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1435
946
 
1436
- ```ts
1437
- import type { RepositoryOf } from "../../generated/vsrepo";
1438
-
1439
- const userVSRepo = setupVSRepo<User, "user">()({ ... });
1440
- type UserRepository = RepositoryOf<typeof userVSRepo>;
1441
- ```
1442
-
1443
- `RepositoryOf` accepts three parameters:
947
+ try {
948
+ await userRepository.save({ name: "Maria" });
949
+ } catch (error) {
950
+ if (error instanceof VSRepoAdapterError) {
951
+ console.error(`[${error.code}] ${error.message}`, error.originalError);
1444
952
 
1445
- ```ts
1446
- type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
953
+ if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
954
+ // handle a duplicate key, e.g. return a friendly message
955
+ }
956
+ }
957
+ }
1447
958
  ```
1448
959
 
1449
- ### `save` and `patch` payload types
960
+ | Property | Type | Description |
961
+ | --------------- | ------------------ | ----------------------------------------------------------------------------------- |
962
+ | `code` | `AdapterErrorCode` | Stable, adapter-agnostic code classifying the failure. |
963
+ | `originalError` | `unknown` | The raw error (or `null`/`undefined`) thrown by the underlying ORM/database driver. |
964
+ | `message` | `string` | Human-readable description of the adapter failure. |
965
+ | `type` | `VSRepoErrorType` | Always `VSRepoErrorType.ADAPTER`. |
966
+ | `cause` | `unknown` | Optional root cause the error was chained from. |
967
+
968
+ Adapter implementations construct it directly when mapping an ORM failure:
969
+
970
+ ```typescript
971
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
972
+
973
+ throw new VSRepoAdapterError(
974
+ "user creation failed",
975
+ AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
976
+ originalError, // raw DB/driver error
977
+ );
978
+ ```
979
+
980
+ #### `AdapterErrorCode`
981
+
982
+ `AdapterErrorCode` is an enum of granular, adapter-agnostic codes an adapter can raise through `VSRepoAdapterError`. They mirror the most common failures thrown by ORMs and database drivers so any ORM's errors can be mapped to the same stable code:
983
+
984
+ ```typescript
985
+ import { AdapterErrorCode } from "vsrepo";
986
+
987
+ console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
988
+ ```
989
+
990
+ | Code | Meaning |
991
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
992
+ | `UNKNOWN` | Unclassified/unknown error; the fallback when no more specific code matches. |
993
+ | `MISSING_DB_CLIENT` | Database client (or connection pool) not provided or could not be resolved. |
994
+ | `CONNECTION_FAILED` | Could not reach/connect to the database, or an established connection was lost/terminated. |
995
+ | `CONNECTION_POOL_EXHAUSTED` | Connection pool exhausted/depleted — no connection available, all busy or the limit was reached. |
996
+ | `TIMEOUT` | Database did not respond in time; a query exceeded its allowed timeout. |
997
+ | `UNIQUE_CONSTRAINT_VIOLATION` | Unique constraint (duplicate key) violated. E.g. Postgres/SQLite `23505`, MySQL `1062`. |
998
+ | `FOREIGN_KEY_VIOLATION` | Foreign key constraint violated (referenced row missing). |
999
+ | `NOT_NULL_VIOLATION` | NOT NULL constraint violated. |
1000
+ | `CHECK_VIOLATION` | CHECK constraint violated. |
1001
+ | `CONSTRAINT_VIOLATION` | General integrity/constraint violation not covered by a more specific code. |
1002
+ | `NOT_FOUND` | Requested record not found (e.g. a `findOneOrThrow`-style operation). |
1003
+ | `INVALID_DATA` | Field value invalid for its type/length, or a required value is missing. |
1004
+ | `VALUE_TOO_LONG` | Provided value exceeds the column/field length limit. |
1005
+ | `CONVERSION_ERROR` | Value could not be converted/cast to the target type. E.g. Postgres `22P02`, MySQL `1366`. |
1006
+ | `INVALID_QUERY` | SQL query/stored procedure is malformed or invalid. |
1007
+ | `TABLE_OR_COLUMN_NOT_FOUND` | Referenced table/column/relation does not exist. |
1008
+ | `DEADLOCK` | Operation aborted by a lock timeout or deadlock between concurrent transactions. |
1009
+ | `LOCK_TIMEOUT` | Could not acquire a required database lock in time. |
1010
+ | `LOCKED` | Record is locked and cannot be modified. |
1011
+ | `ACCESS_DENIED` | Current user/role does not have permission for the operation. |
1012
+ | `INVALID_CREDENTIALS` | Invalid connection credentials (host/user/password). |
1013
+ | `ROW_NOT_ALLOWED` | Authenticated user does not own the record / row-level security rejected it. |
1014
+ | `MODEL_NOT_FOUND` | Entity/model or table not defined/mapped in the ORM, or the adapter lacks model metadata to build the query. |
1015
+ | `FIELD_NOT_FOUND` | Field/column name in the data or `where` does not exist on the entity/model. |
1016
+ | `TRANSACTION_CLOSED` | Transaction used after it was committed/rolled back. |
1017
+ | `TRANSACTION_ALREADY_STARTED` | A nested transaction could not be opened (e.g. nested `transaction()` calls). |
1018
+ | `TRANSACTION_CONFLICT` | Transaction failed to commit and was rolled back. |
1019
+ | `TRANSACTION_NOT_STARTED` | No active transaction when one was required. |
1020
+ | `CONNECTION_CLOSED` | Connection closed/terminated while a transaction or query was in progress. |
1021
+ | `INVALID_PARTIAL` | `merge`/`upsert`/`update` received a partial object that is invalid or missing required keys. |
1022
+ | `NOT_SUPPORTED` | Unsupported feature/operation requested from the adapter (e.g. raw `query()` not supported). |
1023
+ | `INVALID_ADAPTER_CONFIG` | Adapter configuration invalid or incomplete (missing required options, or options with an invalid type/value). |
1024
+ | `INTERNAL` | Internal adapter bug or unrecoverable state; should rarely be used — prefer a more specific code. |
1025
+
1026
+ #### `VSRepoError` vs. raw ORM errors
1027
+
1028
+ Non-adapter usage/config mistakes throw the base `VSRepoError`. Failures raised _by the underlying ORM_ while an adapter method runs are **wrapped** in `VSRepoAdapterError` (classified by an `AdapterErrorCode`, with the original error preserved in `originalError`) instead of propagating raw — this is what makes callers independent of any specific ORM's error shape.
1450
1029
 
1451
- ```ts
1452
- import type { SaveObject, PatchObject } from "../../generated/vsrepo";
1030
+ ---
1453
1031
 
1454
- const userVSRepo = setupVSRepo<User, "user">()(({
1455
- tableName: "user",
1456
- pkName: "id",
1457
- relations: {
1458
- profile: { pk: "id", mode: "oto", restriction: "set" },
1459
- },
1460
- });
1032
+ ## Logging
1461
1033
 
1462
- type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
1463
- type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
1464
- ```
1034
+ Every repository has an internal logger, configured via `logLevel` and `logSlowThresholdMs` on the constructor options:
1465
1035
 
1466
- ---
1036
+ ```typescript
1037
+ import { VSLogLevel } from "vsrepo";
1467
1038
 
1468
- ## API Reference
1469
-
1470
- ### `setupVSRepo<T, M>()(config)`
1471
-
1472
- ```ts
1473
- setupVSRepo<TPayload, TTableName>()({
1474
- tableName: Uncapitalize<M>; // Table name in Prisma
1475
- pkName: keyof T; // Primary key name
1476
- softRemovekName?: keyof T & string; // DateTime field for soft-delete
1477
- selectModels?: SelectModels<M>; // Named data projections (select)
1478
- defaultSelectModel?: keyof SM; // Select applied by default
1479
- includeModels?: IncludeModels<M>; // Named data projections (include) — no default, only in the call
1480
- requiredWhere?: WhereModel<M>; // Always-applied filters
1481
- defaultOrdering?: OrderingModel<M>; // Default ordering for queries without Ordered/injectOrdering
1482
- relations?: RepositoryRelations<T>; // Relation configuration
1483
- methods?: Record<string, MethodConfig<M, SM>>; // Dynamic methods
1039
+ super({
1040
+ pkName: "id",
1041
+ adapter,
1042
+ logLevel: VSLogLevel.DEBUG,
1043
+ logSlowThresholdMs: 200,
1484
1044
  });
1485
1045
  ```
1486
1046
 
1487
- ### `.build(prisma, config?)`
1488
-
1489
- ```ts
1490
- vsRepo.build(prisma, {
1491
- showWorking?: boolean; // Shows internal logs on the console (default = false)
1492
-
1493
- baseMethods?: {
1494
- // Methods that can use a 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
- // Methods that do NOT accept 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
- ```
1047
+ | Level | Meaning |
1048
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
1049
+ | `DEBUG` | Verbose internal details, including every resolved query — very useful for debugging dynamic methods. |
1050
+ | `INFO` | High-level lifecycle events, such as repository initialization. |
1051
+ | `WARN` (default) | Recoverable issues and slow operations (see `logSlowThresholdMs`, defaults to 300ms). |
1052
+ | `ERROR` | Failures raised while executing an operation. |
1517
1053
 
1518
- ### `.extend(fn)`
1054
+ ---
1519
1055
 
1520
- ```ts
1521
- repo.extend((repo) => ({
1522
- myMethod: () => { ... }
1523
- }));
1524
- ```
1056
+ ## Development
1525
1057
 
1526
- ---
1058
+ The v2 core is built and packed from this branch as a standard npm package:
1527
1059
 
1528
- ## Practical examples
1529
-
1530
- Besides this README, the repository has an **[`examples/`](https://github.com/jaobrabo123/VSRepository/tree/main/examples)** folder with practical, commented, ready-to-run examples — it's the best place to see VSRepository being used in real scenarios.
1531
-
1532
- ```text
1533
- examples/
1534
- ├── prisma.ts # PrismaClient instance used by the examples
1535
- ├── repositories.ts # Repository configuration (User, Address, Product) with setupVSRepo
1536
- └── tests/
1537
- ├── base-methods.test.ts # Base methods: get, save, patch, remove, getAll, total, has...
1538
- ├── relations.test.ts # How to configure and use relations in save/patch and in filters
1539
- ├── required-where.test.ts # How requiredWhere is automatically applied to queries
1540
- ├── dynamic-methods.test.ts # Prefixes, field filters, logical operators, and pagination/ordering
1541
- ├── transactions.test.ts # Transactions with options.db and instance access via repository.prisma
1542
- ├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList and SeeMode
1543
- └── batch-methods.test.ts # Batch operations: getList, saveList, patchList and merge
1544
- ```
1060
+ ```bash
1061
+ # 1. Install dependencies
1062
+ pnpm install
1545
1063
 
1546
- Each file in `tests/` is an independent, runnable script (via `tsx`) that demonstrates a specific set of features, with `console.log` at each step so you can follow the result in the terminal. The folder itself has a [README](https://github.com/jaobrabo123/VSRepository/blob/main/examples/README.md) explaining the suggested reading order, how to set up the environment, and how to run the tests.
1064
+ # 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
1065
+ pnpm build
1547
1066
 
1548
- ---
1067
+ # 3. (Optional) Inspect what would be published without writing a tarball
1068
+ npm pack --dry-run
1549
1069
 
1550
- ## Contributing
1070
+ # 4. Produce the installable tarball (runs `prepack` -> `pnpm build` automatically)
1071
+ npm pack
1551
1072
 
1552
- Contributions are welcome! If you found a bug, have an improvement idea, or want to help with the documentation, feel free to get involved (**[GitHub Repository](https://github.com/jaobrabo123/VSRepository)**):
1073
+ # 5. Consume it locally in another project
1074
+ npm install ../path/to/vsrepo-1.4.0.tgz
1075
+ ```
1553
1076
 
1554
- 1. **Fork** the project.
1555
- 2. Create a new branch with your change: `git checkout -b fixing-bug`.
1556
- 3. Push to your branch: `git push origin fixing-bug`.
1557
- 4. Open a **Pull Request**.
1077
+ Notes:
1558
1078
 
1559
- To report issues or suggest new features, open an **Issue**.
1079
+ - `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
1080
+ - The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). The adapters will live in their own `@vsrepo/*-adapter` packages.
1081
+ - The core is ORM-agnostic and has no `@prisma/client` peer dependency.
1560
1082
 
1561
1083
  ---
1562
1084
 
1563
1085
  ## Requirements
1564
1086
 
1565
- - Node.js 18+ (ESM)
1566
- - Prisma
1567
- - TypeScript (optional, but strongly recommended)
1568
- - `"moduleResolution": "bundler"` or `"nodenext"` in tsconfig
1569
-
1570
- Recommended `tsconfig.json`:
1087
+ - Node.js 18+
1088
+ - TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
1571
1089
 
1572
1090
  ```json
1573
1091
  {
1574
- "compilerOptions": {
1575
- "target": "ES2020",
1576
- "module": "NodeNext",
1577
- "moduleResolution": "NodeNext",
1578
- "strict": true,
1579
- "skipLibCheck": true,
1580
- "lib": ["ES2020"]
1581
- }
1092
+ "compilerOptions": {
1093
+ "experimentalDecorators": true
1094
+ }
1582
1095
  }
1583
1096
  ```
1584
1097
 
1585
- ---
1586
-
1587
- ## Troubleshooting
1588
-
1589
- **Generic types not inferred** — Check that `strict: true` and `moduleResolution: "bundler"` or `"nodenext"` are set in `tsconfig.json`.
1590
-
1591
- **Dynamic method doesn't exist at runtime** — The field referenced in the method name must exist in the Prisma model. E.g.: `findByEmail` requires the model to have an `email` field.
1592
-
1593
- **`proxyTo` required** — Names outside the standard patterns (e.g. `searchByEmail`) aren't parsed directly. Use `proxyTo: "findByEmail"` in these cases.
1098
+ - `reflect-metadata` (bundled as a dependency, imported internally — you don't need to import it yourself)
1099
+ - At least one working `VSRepoAdapter` for your database — on Prisma 7, install the published [`@vsrepo/prisma7-adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) (see [Adapter status](#adapter-status)); official adapters for other ORMs are planned but not published yet, so for now this means writing your own (see [Writing your own adapter](#writing-your-own-adapter)) — and if you publish it, contributing it back to the project is welcome
1594
1100
 
1595
- **Select model returns unexpected fields** — Check that the select model defines exactly the fields your TypeScript type expects.
1596
-
1597
- **`selectModel`, `includeModel` and `include` together in the same call** — Not allowed. Only one of the three can be provided per call: if `includeModel` or `include` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
1598
-
1599
- **`includeModel` doesn't appear as a default repository option** — This is expected. Unlike `defaultSelectModel`, there's no `defaultIncludeModel`/`defaultInclude`. An `includeModel` can only be set in the method call, via `options.includeModel`. A raw, ad hoc include can be set via `options.include`, without needing to be registered in `includeModels`.
1101
+ ---
1600
1102
 
1601
- **`softRemovekName` throws an error at build** — The provided field must be of type `DateTime` in the Prisma schema. Types like `Boolean` or `String` are not accepted.
1103
+ ## Contributing
1602
1104
 
1603
- **`defaultOrdering` isn't being applied** — Check whether the method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix, and whether it has `injectOrdering` configured. Both take priority over the default ordering.
1105
+ Contributions are welcome, especially towards finishing the Prisma and TypeORM adapters! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
1604
1106
 
1605
- **`Distinct` suffix not recognized** — `Distinct` is only resolved on read prefixes (`findMany`, `findFirst`, `findBy`, `existsBy`, etc). In methods like `count`, `createMany`, `updateMany`, or `deleteMany` the suffix is ignored.
1107
+ 1. **Fork** the project.
1108
+ 2. Create a branch off `v2` for your change: `git checkout -b v2-my-change`.
1109
+ 3. Push your branch: `git push origin v2-my-change`.
1110
+ 4. Open a **Pull Request** against `v2`.
1606
1111
 
1607
- **`saveList`/`patchList` with invalid `db`** — The `db` field in these methods only accepts a `DbTransaction` (the return of `prisma.$transaction`), not the main client. Passing the `PrismaClient` directly will cause unexpected behavior.
1112
+ To report issues or suggest features, open an **Issue**.