vsrepo 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +723 -1338
  2. package/README.pt-BR.md +729 -1341
  3. package/dist/VSRepoAdapter.d.ts +71 -0
  4. package/dist/VSRepoAdapter.js +18 -0
  5. package/dist/VSRepository.d.ts +135 -1201
  6. package/dist/VSRepository.js +273 -237
  7. package/dist/decorators/dynamic-method.decorator.d.ts +25 -0
  8. package/dist/decorators/dynamic-method.decorator.js +38 -0
  9. package/dist/decorators/query-method.decorator.d.ts +27 -0
  10. package/dist/decorators/query-method.decorator.js +45 -0
  11. package/dist/errors/VSRepoAdapterError.d.ts +22 -0
  12. package/dist/errors/VSRepoAdapterError.js +31 -0
  13. package/dist/errors/VSRepoError.d.ts +15 -0
  14. package/dist/errors/VSRepoError.js +21 -0
  15. package/dist/index.d.ts +34 -1
  16. package/dist/index.js +28 -15
  17. package/dist/internal/constants/debug-arg-symbol.constant.d.ts +1 -0
  18. package/dist/internal/constants/debug-arg-symbol.constant.js +4 -0
  19. package/dist/internal/constants/dynamic-methods-key.constant.d.ts +1 -0
  20. package/dist/internal/constants/query-methods-key.constant.d.ts +1 -0
  21. package/dist/internal/constants/query-methods-key.constant.js +4 -0
  22. package/dist/internal/enums/adapter-error-code.enum.d.ts +125 -0
  23. package/dist/internal/enums/adapter-error-code.enum.js +129 -0
  24. package/dist/internal/enums/transaction-isolation-level.enum.d.ts +16 -0
  25. package/dist/internal/enums/transaction-isolation-level.enum.js +20 -0
  26. package/dist/internal/enums/vs-log-level.enum.d.ts +18 -0
  27. package/dist/internal/enums/vs-log-level.enum.js +22 -0
  28. package/dist/internal/enums/vsrepo-error-type.enum.d.ts +19 -0
  29. package/dist/internal/enums/vsrepo-error-type.enum.js +23 -0
  30. package/dist/internal/resolvers/dynamic-methods.resolver.d.ts +23 -0
  31. package/dist/internal/resolvers/dynamic-methods.resolver.js +910 -0
  32. package/dist/internal/resolvers/merge-wheres.resolver.d.ts +7 -0
  33. package/dist/internal/resolvers/merge-wheres.resolver.js +26 -0
  34. package/dist/internal/utils/uncapitalize.util.d.ts +1 -0
  35. package/dist/internal/utils/vs-logger.util.d.ts +25 -0
  36. package/dist/internal/utils/vs-logger.util.js +139 -0
  37. package/dist/internal/validators/decorators.validator.d.ts +8 -0
  38. package/dist/internal/validators/decorators.validator.js +75 -0
  39. package/dist/internal/validators/schemas/ordering.schema.d.ts +3 -0
  40. package/dist/internal/validators/schemas/ordering.schema.js +38 -0
  41. package/dist/internal/validators/schemas/pagination.schema.d.ts +6 -0
  42. package/dist/internal/validators/schemas/pagination.schema.js +40 -0
  43. package/dist/internal/validators/schemas/where.schema.d.ts +7 -0
  44. package/dist/internal/validators/schemas/where.schema.js +42 -0
  45. package/dist/internal/validators/vsrepo.validator.d.ts +33 -0
  46. package/dist/internal/validators/vsrepo.validator.js +160 -0
  47. package/dist/types/adapter/adapter-method-options.type.d.ts +24 -0
  48. package/dist/types/adapter/adapter-query-options.type.d.ts +5 -0
  49. package/dist/types/decorators/dynamic-method-options.type.d.ts +14 -0
  50. package/dist/types/decorators/query-method-options.type.d.ts +15 -0
  51. package/dist/types/dynamic-methods/dynamic-method-customization.type.d.ts +7 -0
  52. package/dist/types/dynamic-methods/dynamic-method-info.type.d.ts +18 -0
  53. package/dist/types/dynamic-methods/dynamic-method-where-ops.type.d.ts +6 -0
  54. package/dist/types/utils/count-result.type.d.ts +9 -0
  55. package/dist/types/utils/deep-partial.type.d.ts +14 -0
  56. package/dist/types/utils/keys-of-type.type.d.ts +20 -0
  57. package/dist/types/utils/methods-options.type.d.ts +23 -0
  58. package/dist/types/utils/ordering.type.d.ts +39 -0
  59. package/dist/types/utils/pagination.type.d.ts +11 -0
  60. package/dist/types/utils/perform-data.type.d.ts +4 -0
  61. package/dist/types/utils/primitive.type.d.ts +6 -0
  62. package/dist/types/utils/query-method-arg.type.d.ts +27 -0
  63. package/dist/types/utils/see-mode.type.d.ts +12 -0
  64. package/dist/types/vsrepo/vsrepo-args.type.d.ts +9 -0
  65. package/dist/types/vsrepo/vsrepo-method.type.d.ts +4 -0
  66. package/dist/types/vsrepo/vsrepo-options.type.d.ts +34 -0
  67. package/dist/types/vsrepo/vsrepo-orm-types.type.d.ts +17 -0
  68. package/dist/types/vsrepo/vsrepo-pretty-where.type.d.ts +7 -0
  69. package/dist/types/vsrepo/vsrepo-query-options.type.d.ts +17 -0
  70. package/dist/types/vsrepo/vsrepo-query-options.type.js +2 -0
  71. package/dist/types/vsrepo/vsrepo-query.type.d.ts +5 -0
  72. package/dist/types/vsrepo/vsrepo-query.type.js +2 -0
  73. package/dist/types/vsrepo/vsrepo-relations.type.d.ts +26 -0
  74. package/dist/types/vsrepo/vsrepo-relations.type.js +2 -0
  75. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.d.ts +19 -0
  76. package/dist/types/vsrepo/vsrepo-resolve-args-data.type.js +2 -0
  77. package/dist/types/vsrepo/vsrepo-select.type.d.ts +15 -0
  78. package/dist/types/vsrepo/vsrepo-select.type.js +2 -0
  79. package/dist/types/vsrepo/vsrepo-transaction-options.type.d.ts +12 -0
  80. package/dist/types/vsrepo/vsrepo-transaction-options.type.js +2 -0
  81. package/dist/types/vsrepo/vsrepo-ugly-where.type.d.ts +9 -0
  82. package/dist/types/vsrepo/vsrepo-ugly-where.type.js +2 -0
  83. package/dist/types/vsrepo/vsrepo-where.type.d.ts +99 -0
  84. package/dist/types/vsrepo/vsrepo-where.type.js +2 -0
  85. package/package.json +16 -37
  86. package/README-DynamicRepo.md +0 -625
  87. package/README-DynamicRepo.pt-BR.md +0 -625
  88. package/dist/DynamicRepository.d.ts +0 -497
  89. package/dist/DynamicRepository.js +0 -26
  90. package/dist/VSRepoError.d.ts +0 -83
  91. package/dist/VSRepoError.js +0 -17
  92. package/dist/internal/decorators/dynamic-method.decorator.js +0 -14
  93. package/dist/internal/decorators/query-method.decorator.js +0 -20
  94. package/dist/internal/entities/dynamic-method-metadata.entity.js +0 -26
  95. package/dist/internal/errors/vs-repo.error.js +0 -31
  96. package/dist/internal/resolvers/base-methods.resolve.js +0 -536
  97. package/dist/internal/resolvers/create-update-payloads-with-relations.resolve.js +0 -143
  98. package/dist/internal/resolvers/data-payload-with-relations.resolve.js +0 -60
  99. package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +0 -63
  100. package/dist/internal/resolvers/dynamic-method-customization.resolve.js +0 -57
  101. package/dist/internal/resolvers/dynamic-method-info.resolve.js +0 -279
  102. package/dist/internal/resolvers/dynamic-methods-metadata.resolve.js +0 -15
  103. package/dist/internal/resolvers/merge-wheres.resolve.js +0 -22
  104. package/dist/internal/resolvers/pretty-wheres.resolve.js +0 -87
  105. package/dist/internal/resolvers/select.resolve.js +0 -7
  106. package/dist/internal/resolvers/specific-where.resolve.js +0 -84
  107. package/dist/internal/resolvers/ugly-where.resolve.js +0 -178
  108. package/dist/internal/utils/logger.util.js +0 -21
  109. package/dist/internal/utils/schemas.util.js +0 -31
  110. package/dist/internal/validation/build-config.validate.js +0 -84
  111. package/dist/internal/validation/constructor-config.validate.js +0 -64
  112. package/dist/internal/validation/dynamic-method-config.validate.js +0 -19
  113. package/dist/internal/validation/extension.validate.js +0 -15
  114. package/dist/internal/validation/is-object.validate.js +0 -6
  115. package/dist/internal/validation/method-options.validate.js +0 -42
  116. package/dist/internal/validation/obj-with-relations.validate.js +0 -37
  117. package/dist/internal/validation/prisma-client.validate.js +0 -10
  118. package/dist/internal/validation/query-method-arg.validate.js +0 -22
  119. package/dist/internal/validation/query-method-options.validate.js +0 -24
  120. package/scripts/configure-prisma-import.mjs +0 -283
  121. package/scripts/copy-types.mjs +0 -24
  122. /package/dist/{internal/decorators/types/dynamic-method-config.type.js → types/adapter/adapter-method-options.type.js} +0 -0
  123. /package/dist/{internal/errors/types/vs-repo-error-type.type.js → types/adapter/adapter-query-options.type.js} +0 -0
  124. /package/dist/{internal/errors/types/vs-repo-runtime-error-code.type.js → types/decorators/dynamic-method-options.type.js} +0 -0
  125. /package/dist/{internal/validation/types → types/decorators}/query-method-options.type.js +0 -0
  126. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-customization.type.js +0 -0
  127. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-info.type.js +0 -0
  128. /package/dist/{internal/resolvers/types → types/dynamic-methods}/dynamic-method-where-ops.type.js +0 -0
  129. /package/dist/{internal/resolvers/types/base-method-function.type.js → types/utils/count-result.type.js} +0 -0
  130. /package/dist/{internal/resolvers/types/pretty-where.type.js → types/utils/deep-partial.type.js} +0 -0
  131. /package/dist/{internal/resolvers/types/prisma-args.type.js → types/utils/keys-of-type.type.js} +0 -0
  132. /package/dist/{internal/resolvers/types/repository-build-instance.type.js → types/utils/methods-options.type.js} +0 -0
  133. /package/dist/{internal/resolvers/types/resolve-db-and-prisma-args-data.type.js → types/utils/ordering.type.js} +0 -0
  134. /package/dist/{internal/validation/types → types/utils}/pagination.type.js +0 -0
  135. /package/dist/{internal/resolvers/types/ugly-where.type.js → types/utils/perform-data.type.js} +0 -0
  136. /package/dist/{internal/validation/types/base-methods.type.js → types/utils/primitive.type.js} +0 -0
  137. /package/dist/{internal/validation/types → types/utils}/query-method-arg.type.js +0 -0
  138. /package/dist/{internal/validation/types → types/utils}/see-mode.type.js +0 -0
  139. /package/dist/{internal/validation/types/build-config.type.js → types/vsrepo/vsrepo-args.type.js} +0 -0
  140. /package/dist/{internal/validation/types/constructor-config.type.js → types/vsrepo/vsrepo-method.type.js} +0 -0
  141. /package/dist/{internal/validation/types/method-options.type.js → types/vsrepo/vsrepo-options.type.js} +0 -0
  142. /package/dist/{internal/validation/types/method.type.js → types/vsrepo/vsrepo-orm-types.type.js} +0 -0
  143. /package/dist/{internal/validation/types/relation.type.js → types/vsrepo/vsrepo-pretty-where.type.js} +0 -0
package/README.md CHANGED
@@ -9,139 +9,124 @@
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](./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
+ - [`select` and `relations`](#select-and-relations)
51
43
  - [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)
44
+ - [Available prefixes](#available-prefixes)
45
+ - [Field filters](#field-filters)
46
+ - [Logical operators](#logical-operators)
47
+ - [Relation filters](#relation-filters)
48
+ - [Ordering, pagination and distinct](#ordering-pagination-and-distinct)
49
+ - [Decorator options](#decorator-options)
50
+ - [Query methods (raw SQL)](#query-methods-raw-sql)
51
+ - [Ad-hoc raw queries with `query()`](#ad-hoc-raw-queries-with-query)
62
52
  - [Transactions](#transactions)
63
- - [Extending a repository](#extending-a-repository)
64
- - [Error handling](#error-handling)
65
53
  - [Utility types](#utility-types)
66
- - [API Reference](#api-reference)
67
- - [Practical examples](#practical-examples)
68
- - [Contributing](#contributing)
54
+ - [Writing your own adapter](#writing-your-own-adapter)
55
+ - [Error handling](#error-handling)
56
+ - [`VSRepoAdapterError` and `AdapterErrorCode`](#vsrepoadaptererror-and-adaptererrorcode)
57
+ - [Logging](#logging)
58
+ - [Development](#development)
69
59
  - [Requirements](#requirements)
70
- - [Troubleshooting](#troubleshooting)
60
+ - [Contributing](#contributing)
71
61
 
72
62
  ---
73
63
 
74
- ## Installation
64
+ ## What changed from v1
75
65
 
76
- ```bash
77
- npm i vsrepo @prisma/client
78
- ```
66
+ If you're coming from the [v1](./v1) code/docs, here's the short version. See each linked section for details.
79
67
 
80
- Generate the Prisma Client:
81
-
82
- ```bash
83
- npx prisma generate
84
- ```
68
+ | Area | v1 | v2 |
69
+ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
70
+ | 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 |
71
+ | Defining a repository | Functional `setupVSRepo<T, M>()({...}).build(prisma)`, **or** a `DynamicRepository` class | A single **class-based** API: `extends VSRepository<Entity, PKType, OrmTypes>` |
72
+ | Dynamic methods | `methods: { findByEmail: { map: true } }` config object | `@DynamicMethod()` decorator on a `declare` field |
73
+ | Data projections | Named, reusable `selectModels` + `defaultSelectModel` | Ad-hoc `select`/`relations` passed per call (no named models) |
74
+ | Eager loading | `include`/`includeModels` (Prisma-specific) | ORM-agnostic `relations` option |
75
+ | Global filters | `requiredWhere` (any arbitrary filter, always applied) | **Removed**; Now it only accepts `softRemoveKey` + `see: "active" \| "removed" \| "all"` |
76
+ | Case-insensitive filter suffix | `Insensitive` | `IgnoreCase` |
77
+ | 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 |
78
+ | Duplicate handling on `createMany` | `SkipDuplicates` suffix | `IgnoreConflicts` suffix |
79
+ | `aggregate` / `groupBy` | Supported (Prisma-native passthrough) | **Not implemented yet** |
80
+ | 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 |
81
+ | Debug logging | `showWorking: true` boolean | `logLevel: VSLogLevel` (`DEBUG`/`INFO`/`WARN`/`ERROR`) + `logSlowThresholdMs` for slow-query warnings |
82
+ | `vsrepo generate` CLI (type generation step) | Required before use | Not part of the v2 core — types come directly from your entity/ORM types |
83
+ | 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
84
 
86
85
  ---
87
86
 
88
- ## Generating the types
89
-
90
- VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
87
+ ## Adapter status
91
88
 
92
- ```bash
93
- npx vsrepo generate
94
- ```
89
+ 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:
95
90
 
96
- Equivalent to:
91
+ - `@vsrepo/prisma7-adapter`
92
+ - `@vsrepo/prisma8-adapter`
93
+ - `@vsrepo/typeorm-adapter`
94
+ - `@vsrepo/drizzle-adapter`
97
95
 
98
- ```bash
99
- npx vsrepo generate \
100
- --output generated/vsrepo \
101
- --prisma generated/prisma
102
- ```
96
+ 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.
103
97
 
104
- **Available flags:**
98
+ | Adapter | Status |
99
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | Prisma 7 (`@vsrepo/prisma7-adapter`) | 🟢 **Released** — published to npm, implements the entire `VSRepoAdapter` contract (CRUD, relations, transactions, `merge`, logging) with tests; see [`VSRepoPrisma7Adapter`](https://github.com/jaobrabo123/VSRepoPrisma7Adapter) for source and docs. |
101
+ | 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. |
102
+ | 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. |
103
+ | 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. |
105
104
 
106
- | Flag | Alias | Default |
107
- | ---------- | ----- | -------------------- |
108
- | `--output` | `-o` | `generated/vsrepo` |
109
- | `--prisma` | `-p` | `generated/prisma` |
105
+ 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.
110
106
 
111
- **Generated files:**
112
-
113
- ```text
114
- generated/vsrepo/
115
- ├── DynamicRepository.ts
116
- ├── DynamicRepository.types.d.ts
117
- ├── VSRepoError.ts
118
- ├── VSRepoError.types.d.ts
119
- ├── VSRepository.ts
120
- ├── VSRepository.types.d.ts
121
- └── index.ts
122
- ```
107
+ ---
123
108
 
124
- After generating, always import from the generated folder:
109
+ ## Installation
125
110
 
126
- ```ts
127
- // CORRECT ✅
128
- import { setupVSRepo } from "../../generated/vsrepo";
111
+ v2 is installed as the core package plus one adapter package for your ORM, for example:
129
112
 
130
- // WRONG ❌
131
- import { setupVSRepo } from "vsrepo";
113
+ ```bash
114
+ npm i vsrepo @vsrepo/prisma7-adapter
132
115
  ```
133
116
 
117
+ > `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)).
118
+
134
119
  ---
135
120
 
136
121
  ## Basic usage
137
122
 
138
- ### Configuring the Prisma Client
123
+ ### Implementing/choosing an adapter
139
124
 
140
- ```ts
125
+ ```typescript
141
126
  // src/configs/db.ts
142
- import { PrismaClient } from '../../generated/prisma/client';
143
- import { PrismaPg } from '@prisma/adapter-pg';
144
- import 'dotenv/config';
127
+ import { PrismaClient } from "../../generated/prisma/client";
128
+ import { PrismaPg } from "@prisma/adapter-pg";
129
+ import "dotenv/config";
145
130
 
146
131
  const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
147
132
  const prisma = new PrismaClient({ adapter });
@@ -151,1457 +136,857 @@ export default prisma;
151
136
 
152
137
  ### Creating a repository
153
138
 
154
- ```ts
155
- // src/repositories/userRepository.ts
139
+ ```typescript
140
+ // src/repositories/user.repository.ts
141
+ import { VSRepository, DynamicMethod } from "vsrepo";
142
+ import { VSRepoPrisma7Adapter } from "@vsrepo/prisma7-adapter";
156
143
  import prisma from "../configs/db";
157
- import { setupVSRepo } from "../../generated/vsrepo";
158
- import type { User } from "../../generated/prisma/client";
159
-
160
- const userRepository = setupVSRepo<User, "User">()(({
161
- tableName: "user",
162
- pkName: "id",
163
- selectModels: {
164
- public: { id: true, name: true, email: true },
165
- },
166
- defaultSelectModel: "public",
167
- }).build(prisma);
168
-
169
- export default userRepository;
170
- ```
171
-
172
- ### Using the repository
173
-
174
- ```ts
175
- import userRepository from "./repositories/userRepository";
176
-
177
- const user = await userRepository.save({
178
- name: "John",
179
- email: "john@email.com",
180
- password: "password",
181
- });
182
-
183
- const found = await userRepository.get(user.id);
184
- const all = await userRepository.getAll();
185
-
186
- user.name = "John Smith";
187
-
188
- await userRepository.save(user);
189
- await userRepository.remove(user.id);
190
- ```
191
-
192
- ---
193
-
194
- ## 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
- ---
144
+ import type { UserGetPayload } from "../../generated/prisma/models";
201
145
 
202
- ## NestJS integration
146
+ type User = UserGetPayload<{ include: { address: true } }>;
203
147
 
204
- VSRepository can be easily integrated into NestJS projects through providers. Below is a complete example using NestJS's dependency injection pattern.
205
-
206
- ### Configuring the repository as a provider
207
-
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";
214
-
215
- const userVSRepo = setupVSRepo<
216
- UserGetPayload<{ include: { profile: true } }>,
217
- "User"
218
- >()(({
219
- tableName: "user",
220
- pkName: "id",
221
- selectModels: {
222
- public: {
223
- id: true,
224
- email: true,
225
- createdAt: true,
226
- updatedAt: true,
227
- },
228
- auth: {
229
- id: true,
230
- email: true,
231
- password: true,
232
- },
233
- },
234
- defaultSelectModel: "public",
235
- requiredWhere: {
236
- deletedAt: null,
237
- },
238
- relations: {
239
- profile: {
240
- mode: "oto",
241
- pk: "id",
242
- restriction: "add",
243
- },
244
- },
245
- methods: {
246
- findAuthByEmail: {
247
- map: true,
248
- proxyTo: "findUniqueByEmail",
249
- selectModel: "auth",
250
- },
251
- findByEmailEndsWith: {
252
- map: true,
253
- }
254
- },
255
- });
256
-
257
- const setupUserRepository = (prisma: PrismaService) => {
258
- return userVSRepo.build(prisma);
259
- };
260
-
261
- export type UserRepository = ReturnType<typeof setupUserRepository>;
262
- /*
263
- 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
- ```
279
-
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 {}
296
- ```
297
-
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
- }
319
-
320
- async createUser(data: { email: string; password: string; name: string }) {
321
- return this.userRepository.save({
322
- email: data.email,
323
- password: data.password,
324
- name: data.name,
148
+ class UserRepository extends VSRepository<User, string> {
149
+ constructor() {
150
+ super({
151
+ pkName: "id",
152
+ adapter: new VSRepoPrisma7Adapter<User>(prisma, { tableName: "user", pkName: "id" }),
153
+ softRemoveKey: "deletedAt",
154
+ defaultOrdering: { createdAt: "desc" },
325
155
  });
326
156
  }
327
- }
328
- ```
329
157
 
330
- **Benefits of this approach:**
158
+ @DynamicMethod()
159
+ declare findByEmail: (email: string) => Promise<User[]>;
331
160
 
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`
337
-
338
- ---
339
-
340
- ## Base methods
341
-
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
- ]);
161
+ @DynamicMethod()
162
+ declare findOneByEmail: (email: string) => Promise<User | null>;
163
+ }
396
164
 
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
- ]);
165
+ export default new UserRepository();
402
166
  ```
403
167
 
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
- ```
168
+ > 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.
412
169
 
413
- ### Merge
170
+ > **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`:
171
+ > ```typescript
172
+ > type PrismaOrmTypes = { dbClient: PrismaClient; dbTransaction: Prisma.TransactionClient };
173
+ >
174
+ > class UserRepository extends VSRepository<User, string, PrismaOrmTypes> {
175
+ > // getDbClient() now returns PrismaClient, and transaction(fn) types `tx` as Prisma.TransactionClient
176
+ > }
177
+ > ```
178
+ > If omitted, it defaults to `VSRepoOrmTypes` (`dbClient`/`dbTransaction` both `any`).
414
179
 
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.
180
+ ### Using the repository
416
181
 
417
- ```ts
418
- const existing = await userRepository.get(1);
419
- // existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
182
+ ```typescript
183
+ import userRepository from "./repositories/user.repository";
420
184
 
421
- const merged = await userRepository.merge(1, {
422
- profile: { bio: "Updated bio" },
185
+ const user = await userRepository.save({
186
+ name: "Joao",
187
+ email: "joao@email.com",
188
+ password: "password",
423
189
  });
424
- // merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
425
190
 
426
- // To persist, pass it to save or patch:
427
- await userRepository.save(merged);
428
- ```
191
+ const found = await userRepository.get(user.id);
192
+ const all = await userRepository.getAll();
193
+ const byEmail = await userRepository.findByEmail("joao@email.com");
429
194
 
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
- ],
449
- });
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
- // }
195
+ await userRepository.patch(user.id, { name: "Joao Pedro" });
196
+ await userRepository.remove(user.id);
458
197
  ```
459
198
 
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`.
465
-
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,
199
+ ---
471
200
 
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,
201
+ ## Constructor options
477
202
 
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
- });
511
- ```
203
+ `VSRepoOptions<T, K>`, passed to `super(...)` inside your repository's constructor:
512
204
 
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`.
205
+ | Option | Type | Description |
206
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
207
+ | `adapter` | `VSRepoAdapter<T>` | **Required.** The adapter instance that translates repository calls into calls against the underlying ORM/database. |
208
+ | `pkName` | `keyof T` | **Required.** Name of the field that represents the entity's primary key. |
209
+ | `softRemoveKey` | `keyof T` | Optional. When set, enables `softRemove`, `softRemoveList`, `restore` and `restoreList`. |
210
+ | `defaultOrdering` | `Ordering<T>` | Optional. Default ordering applied automatically to queries that accept `order`, unless overridden per call. |
211
+ | `logLevel` | `VSLogLevel` | Optional. Minimum severity printed by the internal logger. Defaults to `VSLogLevel.WARN`. |
212
+ | `logSlowThresholdMs` | `number` | Optional. Duration (ms) above which a finished operation is logged as `WARN` instead of `DEBUG`. Defaults to 300ms. |
514
213
 
515
214
  ---
516
215
 
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`.
216
+ ## Base methods
531
217
 
532
- **Using a specific select in the call:**
218
+ Available automatically on every `VSRepository` subclass:
533
219
 
534
- ```ts
535
- const user = await userRepository.get(id, { selectModel: "minimal" });
536
- ```
220
+ | Method | Description |
221
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
222
+ | `get(pk, options?)` | Fetches a record by primary key. |
223
+ | `getOrThrow(pk, options?)` | Fetches a record by primary key, throwing if not found. |
224
+ | `getList(pks, options?)` | Fetches multiple records by a list of primary keys. |
225
+ | `getAll(options?)` | Fetches all records; accepts `pagination` and `order` in `options`. |
226
+ | `save(obj, options?)` | Creates or updates (upsert) a single record. |
227
+ | `saveList(objs, options?)` | Creates or updates (upsert) multiple records in one call. |
228
+ | `patch(pk, obj, options?)` | Partially updates a record by primary key. |
229
+ | `merge(pk, obj, options?)` | Fetches a record and returns it deep-merged, in memory, with the given object — does **not** persist anything. |
230
+ | `remove(pk, options?)` | Deletes a record by primary key. |
231
+ | `removeList(pks, options?)` | Deletes multiple records by primary key, returning `{ count }`. |
232
+ | `total(options?)` | Returns the total number of records. |
233
+ | `has(pk, options?)` | Checks whether a record exists, returning `boolean`. |
234
+ | `transaction(fn, options?)` | Runs `fn` inside a native transaction of the underlying ORM. |
235
+ | `getDbClient()` | Returns the underlying ORM client instance used outside of transactions. |
236
+ | `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). |
537
237
 
538
- **Returning Prisma's default payload (without select):**
238
+ All of the above (except `transaction`, `query`, and `getDbClient`, which accept their own options or none at all) accept a `MethodOptions<Entity, OrmTypes>` object as their last argument (`select`, `relations`, `see`, `db`).
539
239
 
540
- ```ts
541
- const fullUser = await userRepository.get(id, { selectModel: false });
542
- ```
240
+ ---
543
241
 
544
- ### Raw `select` (`options.select`)
242
+ ## Soft-delete
545
243
 
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.
244
+ Soft-delete is now a **first-class, built-in concept**. Configure `softRemoveKey` once on the repository:
547
245
 
548
- ```ts
549
- const user = await userRepository.get(id, {
550
- select: { id: true, name: true },
246
+ ```typescript
247
+ super({
248
+ pkName: "id",
249
+ adapter,
250
+ softRemoveKey: "deletedAt",
551
251
  });
552
252
  ```
553
253
 
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:**
254
+ This unlocks four extra methods:
557
255
 
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.
256
+ | Method | Effect |
257
+ | ------------------------------- | ------------------------------------- |
258
+ | `softRemove(pk, options?)` | Sets `deletedAt` to the current date. |
259
+ | `softRemoveList(pks, options?)` | Same, in batch — returns `{ count }`. |
260
+ | `restore(pk, options?)` | Sets `deletedAt` back to `null`. |
261
+ | `restoreList(pks, options?)` | Same, in batch — returns `{ count }`. |
561
262
 
562
- ```ts
563
- // CORRECT ✅ — raw select only
564
- await userRepository.get(id, { select: { id: true, name: true } });
263
+ Every other method accepts a `see` option controlling visibility of soft-deleted rows:
565
264
 
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 } });
265
+ ```typescript
266
+ await userRepository.getAll({ see: "active" }); // default — only non-deleted records
267
+ await userRepository.getAll({ see: "removed" }); // only soft-deleted records
268
+ await userRepository.getAll({ see: "all" }); // everything, ignoring soft-delete
569
269
  ```
570
270
 
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.
572
-
573
271
  ---
574
272
 
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);
592
- ```
593
-
594
- **Using an `includeModel` in the call:**
595
-
596
- ```ts
597
- const user = await userRepository.get(id, { includeModel: "withPosts" });
598
- ```
599
-
600
- In this case, the default `select` (`selectModels`/`defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
273
+ ## `select` and `relations`
601
274
 
602
- ### Differences from `selectModels`
603
-
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.
606
-
607
- ```ts
608
- // CORRECT ✅ — includeModel only
609
- await userRepository.get(id, { includeModel: "withPosts" });
610
-
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
- ```
275
+ 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:
617
276
 
618
- ### Raw `include` (`options.include`)
619
-
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.
621
-
622
- ```ts
277
+ ```typescript
623
278
  const user = await userRepository.get(id, {
624
- include: { posts: true, profile: true },
279
+ select: { id: true, name: true, address: { city: true } },
625
280
  });
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
281
 
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
-
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
- }
282
+ const userWithAddress = await userRepository.get(id, {
283
+ relations: { address: true },
284
+ });
707
285
  ```
708
286
 
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
287
+ - `select` mirrors the entity's shape: scalar fields take a `boolean`; relation fields take a `boolean` or a nested `select`.
288
+ - `relations` eagerly loads related records; each relation field takes a `boolean` or a nested `relations` object.
289
+ - Whether `select` and `relations` can be combined depends on the adapter (see below).
714
290
 
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.
291
+ > ⚠️ **Adapter-dependent behavior for `relations`:**
292
+ >
293
+ > The core only forwards `MethodOptions.select` and `MethodOptions.relations` to the adapter — each adapter decides how to translate them to the underlying ORM:
294
+ >
295
+ > - **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`:
296
+ > ```typescript
297
+ > // TypeORM: select alone is NOT enough
298
+ > await userRepository.get(id, {
299
+ > select: { id: true, address: { city: true } },
300
+ > relations: { address: true }, // ← required in TypeORM
301
+ > });
302
+ > ```
303
+ > - **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:
304
+ > ```typescript
305
+ > // Prisma7: relations is ignored when select exists
306
+ > await userRepository.get(id, {
307
+ > select: { id: true, name: true },
308
+ > relations: { address: true }, // ← ignored, include = undefined
309
+ > });
310
+ > ```
311
+ >
312
+ > Custom adapters may map `relations` differently — consult the adapter's documentation for the exact semantics.
735
313
 
736
314
  ---
737
315
 
738
316
  ## Dynamic methods
739
317
 
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 },
318
+ 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.
319
+
320
+ ```typescript
321
+ class UserRepository extends VSRepository<User, string> {
322
+ @DynamicMethod()
323
+ declare findByEmail: (email: string) => Promise<User[]>;
324
+
325
+ @DynamicMethod()
326
+ declare findOneByEmail: (email: string) => Promise<User | null>;
327
+
328
+ @DynamicMethod()
329
+ declare updateById: (id: string, data: DeepPartial<User>) => Promise<User>;
330
+
331
+ // Where-based: VSRepoWhere<T> as the first param, pagination penultimate, MethodOptions last
332
+ @DynamicMethod()
333
+ declare findWherePaginated: (
334
+ where: VSRepoWhere<User>,
335
+ pagination: Pagination,
336
+ options?: MethodOptions<User>,
337
+ ) => Promise<User[]>;
338
+
339
+ // OrderedAndPaginated: field filters, then order, then pagination, then MethodOptions
340
+ @DynamicMethod()
341
+ declare findByNameIgnoreCaseOrAgeBetweenOrderByCreatedAtAscPaginated: (
342
+ name: string,
343
+ age: [number, number],
344
+ order: Ordering<User>,
345
+ pagination: Pagination,
346
+ options?: MethodOptions<User>,
347
+ ) => Promise<User[]>;
748
348
  }
749
349
  ```
750
350
 
751
- ---
752
-
753
351
  ### Available prefixes
754
352
 
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
- ---
353
+ | Prefix | Adapter method | Notes |
354
+ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
355
+ | `findBy` | `findMany` | Field filters follow the prefix. |
356
+ | `findOneBy` | `findOne` | Field filters follow the prefix; single result. |
357
+ | `findOneOrThrowBy` | `findOneOrThrow` | Throws if no record is found. |
358
+ | `findOneOrThrow` | `findOneOrThrow` | No field filters; applies only soft-delete/`see`. |
359
+ | `findOneOrThrowWhere` | `findOneOrThrow` | Receives a `VSRepoWhere<T>` as the first argument. |
360
+ | `findWhere` | `findMany` | Receives a `VSRepoWhere<T>` as the first argument. |
361
+ | `findOneWhere` | `findOne` | Receives a `VSRepoWhere<T>` as the first argument. |
362
+ | `findOne` | `findOne` | No field filters; applies only soft-delete/`see`. |
363
+ | `countBy` | `count` | Field filters follow the prefix. |
364
+ | `countWhere` | `count` | Receives a `VSRepoWhere<T>` as the first argument. |
365
+ | `count` | `count` | No field filters. |
366
+ | `existsBy` | `exists` | Returns `boolean`. |
367
+ | `existsWhere` | `exists` | Receives a `VSRepoWhere<T>` as the first argument. |
368
+ | `create` | `create` | Receives `data` as argument. |
369
+ | `createMany` | `createMany` | Receives `data[]` as argument; supports `IgnoreConflicts`. |
370
+ | `createManyReturning` | `createManyReturning` | Receives `data[]` as argument; supports `IgnoreConflicts`; returns the created records (`T[]`) instead of `CountResult`. |
371
+ | `updateBy` | `update` | Field filters + `data` as argument. |
372
+ | `updateWhere` | `update` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
373
+ | `updateManyBy` | `updateMany` | Field filters + `data`. |
374
+ | `updateManyWhere` | `updateMany` | Receives a `VSRepoWhere<T>` as the first argument, then `data`. |
375
+ | `updateManyReturningBy` | `updateManyReturning` | Field filters + `data`; returns updated records. |
376
+ | `updateManyReturningWhere` | `updateManyReturning` | Receives a `VSRepoWhere<T>` as the first argument, then `data`; returns updated records. |
377
+ | `upsertBy` | `upsert` | Field filters + `create`/`update` payloads. |
378
+ | `upsertWhere` | `upsert` | Receives a `VSRepoWhere<T>` as the first argument, then `create`/`update` payloads. |
379
+ | `deleteBy` | `delete` | Field filters follow the prefix. |
380
+ | `deleteWhere` | `delete` | Receives a `VSRepoWhere<T>` as the first argument. |
381
+ | `deleteManyBy` | `deleteMany` | Field filters follow the prefix. |
382
+ | `deleteManyWhere` | `deleteMany` | Receives a `VSRepoWhere<T>` as the first argument. |
383
+ | `deleteManyReturningBy` | `deleteManyReturning` | Field filters follow the prefix; returns deleted records. |
384
+ | `deleteManyReturningWhere` | `deleteManyReturning` | Receives a `VSRepoWhere<T>` as the first argument; returns deleted records. |
385
+
386
+ > `aggregate` and `groupBy` are **not implemented yet** in v2 (they existed in v1). This is planned but not currently available.
792
387
 
793
388
  ### Field filters
794
389
 
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")]);
841
- ```
842
-
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
390
+ Applied as suffixes to the field name inside the method (same idea as v1, one renamed suffix):
391
+
392
+ | Suffix | Meaning | Argument |
393
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
394
+ | _(none)_ | equality (`=`) | yes |
395
+ | `Not` | negation | yes |
396
+ | `In` | is one of | yes (array) |
397
+ | `NotIn` | is none of | yes (array) |
398
+ | `Contains` | substring match | yes |
399
+ | `NotContains` | negated substring match | yes |
400
+ | `StartsWith` | prefix match | yes |
401
+ | `NotStartsWith` | negated prefix match | yes |
402
+ | `EndsWith` | suffix match | yes |
403
+ | `NotEndsWith` | negated suffix match | yes |
404
+ | `GreaterThan` | `>` | yes |
405
+ | `GreaterThanEqual` | `>=` | yes |
406
+ | `LessThan` | `<` | yes |
407
+ | `LessThanEqual` | `<=` | yes |
408
+ | `Between` | inclusive range | yes (`[min, max]` tuple) |
409
+ | `NotBetween` | outside an inclusive range | yes (`[min, max]` tuple) |
410
+ | `IsNull` | field is `null` | no |
411
+ | `IsNotNull` | field is not `null` | no |
412
+ | `IsTrue` | field is `true` | no |
413
+ | `IsFalse` | field is `false` | no |
414
+ | `IgnoreCase` | case-insensitive combinator for text filters | no _(renamed from v1's `Insensitive`)_ |
415
+ | `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 | — |
416
+
417
+ ```typescript
418
+ @DynamicMethod()
419
+ declare findByNameContainsIgnoreCase: (name: string) => Promise<User[]>;
420
+
421
+ @DynamicMethod()
422
+ declare findByAgeBetween: (age: [number, number]) => Promise<User[]>;
847
423
  ```
848
424
 
849
- ---
850
-
851
425
  ### Logical operators
852
426
 
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` |
858
-
859
- `AND` (in caps) has a specific rule:
427
+ | Operator | Usage in the name | Example |
428
+ | -------- | ------------------------------- | --------------------------------------------------- |
429
+ | `And` | between two fields | `findOneByIdAndEmail` |
430
+ | `Or` | between two fields | `findByNameOrEmail` |
431
+ | `AND` | splits a final block into `AND` | `findByEmailOrNameANDActiveStatusAndAgeGreaterThan` |
860
432
 
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`.
433
+ `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`.
864
434
 
865
- Example:
435
+ ### Relation filters
866
436
 
867
- ```ts
868
- methods: {
869
- findOneByIdAndEmail: { map: true },
870
- findByNameOrEmail: { map: true },
871
- findFirstByIdOrEmailAndName: { map: true },
872
- findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
873
- }
437
+ 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).
874
438
 
875
- await userRepository.findOneByIdAndEmail(1, "john@email.com");
876
- await userRepository.findByNameOrEmail("John", "john@email.com");
877
- await userRepository.findFirstByIdOrEmailAndName(1, "john@email.com", "John");
878
- await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
879
- ```
439
+ | Suffix | Meaning | Restriction |
440
+ | -------------- | ----------------------------------------------- | ---------------------------------------------------------- |
441
+ | `Some` | at least one related record matches | to-many relations only |
442
+ | `SomeField` | filters within the related records | to-many relations only |
443
+ | `Every` | every related record matches | to-many relations only (needs `Field` to be a real filter) |
444
+ | `EveryField` | filters within the related records | to-many relations only |
445
+ | `None` | no related record matches | to-many relations only |
446
+ | `NoneField` | filters within the related records | to-many relations only |
447
+ | `With` | related record exists | to-one relations only |
448
+ | `WithField` | filters a field within the related record | to-one relations only |
449
+ | `Without` | related record does not exist | to-one relations only |
450
+ | `WithoutField` | negated filter on a field of the related record | to-one relations only |
880
451
 
881
- Generates (`findOneByIdAndEmail`):
452
+ ```typescript
453
+ @DynamicMethod()
454
+ declare findByAddressWithCityStartsWithIgnoreCase: (city: string) => Promise<User[]>;
882
455
 
883
- ```ts
884
- {
885
- id: 1,
886
- email: "john@email.com"
887
- }
456
+ @DynamicMethod()
457
+ declare findByProductsSome: () => Promise<User[]>;
888
458
  ```
889
459
 
890
- Generates (`findByNameOrEmail`):
460
+ ### Ordering, pagination and distinct
891
461
 
892
- ```ts
893
- {
894
- OR: [
895
- { name: "John" },
896
- { email: "john@email.com" }
897
- ]
898
- }
899
- ```
462
+ | Suffix | Effect |
463
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
464
+ | `Paginated` | Injects a `pagination` argument (`{ limit?, offset? }`) as the **penultimate** parameter (before the optional `MethodOptions`). |
465
+ | `Ordered` | Injects an `order: Ordering<T>` argument as the **penultimate** parameter (before the optional `MethodOptions`). |
466
+ | `OrderedAndPaginated` | Injects `order` as the antepenultimate, then `pagination` as the penultimate — both before `MethodOptions`. |
467
+ | `PaginatedAndOrdered` | Injects `pagination` as the antepenultimate, then `order` as the penultimate — both before `MethodOptions`. |
468
+ | `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. |
469
+ | `Distinct<Field>And<Field>...` | Bakes fixed `distinct` fields directly into the method name (only valid on `findBy`/`findWhere`-family methods). |
470
+ | `IgnoreConflicts` | On `createMany`/`createManyReturning`, skips records that would violate a unique constraint instead of throwing. _(Renamed from v1's `SkipDuplicates`.)_ |
900
471
 
901
- Generates (`findFirstByIdOrEmailAndName`):
902
-
903
- ```ts
904
- {
905
- OR: [
906
- { id: 1 },
907
- {
908
- email: "john@email.com",
909
- name: "John"
910
- }
911
- ]
912
- }
913
- ```
472
+ > ⚠️ **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
473
 
915
- Generates (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
474
+ ```typescript
475
+ // Paginated: pagination is the penultimate param (before MethodOptions)
476
+ @DynamicMethod()
477
+ declare findByActiveOrderByCreatedAtDescPaginated:
478
+ (active: boolean, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
916
479
 
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
- ```
480
+ // OrderedAndPaginated: order, then pagination, then MethodOptions
481
+ @DynamicMethod()
482
+ declare findByNameContainsIgnoreCaseOrderedAndPaginated:
483
+ (name: string, order: Ordering<User>, pagination: Pagination, options?: MethodOptions<User>) => Promise<User[]>;
929
484
 
930
- ---
485
+ @DynamicMethod()
486
+ declare createManyIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<{ count: number }>;
931
487
 
932
- ### Relation filters
488
+ // createManyReturning: same as createMany, but returns the created records
489
+ @DynamicMethod()
490
+ declare createManyReturningIgnoreConflicts: (data: DeepPartial<User>[]) => Promise<User[]>;
933
491
 
934
- Allow filtering by fields of related models.
492
+ // findOne with no filter (equivalent to findOneOrThrow with no filter, but returns null instead of throwing)
493
+ @DynamicMethod()
494
+ declare findOne: (options?: MethodOptions<User>) => Promise<User | null>;
495
+ ```
935
496
 
936
- > [!IMPORTANT]
497
+ > ⚠️ **Precedence between `Distinct` and `OrderBy`:** when both are used in the same method name, **`Distinct` must come before `OrderBy`**:
937
498
  >
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
- }
499
+ > ```typescript
500
+ > @DynamicMethod()
501
+ > declare findByActiveDistinctNameOrderByCreatedAtDesc:
502
+ > (active: boolean) => Promise<User[]>;
503
+ > ```
504
+ >
505
+ > Putting `OrderBy` before `Distinct` (e.g. `findByActiveOrderByCreatedAtDescDistinctName`) is not a valid pattern and won't be parsed as expected.
972
506
 
973
- await userRepository.findByPostsSome();
974
- await userRepository.findByPostsSomeTitle("My first post");
975
- await userRepository.findByPostsEveryPublishedIsTrue();
976
- await userRepository.findByPostsNone();
977
- await userRepository.findByPostsNoneTitle("Draft");
507
+ ### Decorator options
978
508
 
979
- await userRepository.findByProfileWith();
980
- await userRepository.findByProfileWithBio("Hello, world!");
981
- await userRepository.findByProfileWithout();
982
- await userRepository.findByProfileWithoutBio("Old bio");
983
- ```
509
+ `@DynamicMethod<T>(options?)` accepts:
984
510
 
985
- Generates (`findByPostsSomeTitle`):
511
+ | Option | Type | Description |
512
+ | ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
513
+ | `proxyTo` | `string` | Redirects the method's logic to another valid dynamic-method pattern — useful for names that don't follow the naming convention. |
514
+ | `injectOrdering` | `Ordering<T>` | Fixed ordering automatically injected, overriding the repository's `defaultOrdering`. |
986
515
 
987
- ```ts
988
- {
989
- posts: {
990
- some: { title: "My first post" }
991
- }
992
- }
516
+ ```typescript
517
+ @DynamicMethod<User>({ injectOrdering: { createdAt: "desc" } })
518
+ declare findByStatus: (status: string) => Promise<User[]>;
993
519
  ```
994
520
 
995
- Generates (`findByPostsEveryPublishedIsTrue`):
521
+ ---
996
522
 
997
- ```ts
998
- {
999
- posts: {
1000
- every: { published: true }
1001
- }
1002
- }
1003
- ```
523
+ ## Query methods (raw SQL)
1004
524
 
1005
- Generates (`findByProfileWithBio`):
525
+ `@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.
1006
526
 
1007
- ```ts
1008
- {
1009
- profile: {
1010
- is: { bio: "Hello, world!" }
1011
- }
1012
- }
1013
- ```
1014
-
1015
- Generates (`findByProfileWithout`):
527
+ ```typescript
528
+ class UserRepository extends VSRepository<User, string> {
529
+ @QueryMethod('SELECT * FROM "user" WHERE email = $1')
530
+ declare findByEmailRaw: (arg: QueryMethodArg<[email: string]>) => Promise<User[]>;
1016
531
 
1017
- ```ts
1018
- {
1019
- profile: {
1020
- isNot: {}
1021
- }
532
+ @QueryMethod('UPDATE "user" SET active = true WHERE id = $1', { modifying: true })
533
+ declare activateUser: (arg: QueryMethodArg<[id: string]>) => Promise<number>;
1022
534
  }
1023
535
  ```
1024
536
 
1025
- > `Some`, `None`, `With`, 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)` |
537
+ | Option | Type | Default | Description |
538
+ | ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
539
+ | `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. |
1039
540
 
1040
- For `createMany` and `createManyAndReturn`, the `SkipDuplicates` suffix is available:
1041
-
1042
- | Suffix | Effect |
1043
- | ------------------- | ------------------------------------------ |
1044
- | `SkipDuplicates` | Skips duplicate records during insertion |
1045
-
1046
- ---
541
+ Query methods accept `{ args, db? }` at the call site — `db` lets them participate in a `transaction()` block just like base and dynamic methods.
1047
542
 
1048
- ### Distinct
543
+ ### Ad-hoc raw queries with `query()`
1049
544
 
1050
- The `Distinct` suffix lets you get only unique records based on one or more fields, equivalent to Prisma's `distinct` option.
545
+ 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:
1051
546
 
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.
1053
-
1054
- ```ts
1055
- methods: {
1056
- // Returns unique users combining "age" and "role" (no field filter)
1057
- findManyDistinctAgeAndRole: { map: true },
1058
-
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
- },
547
+ ```typescript
548
+ query<T = any>(query: string, options?: { args?: any[]; db?: any; modifying?: boolean }): Promise<T>;
1065
549
  ```
1066
550
 
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 });
551
+ ```typescript
552
+ const users = await userRepository.query<User[]>('SELECT * FROM "user" WHERE email = $1', {
553
+ args: ["maria@email.com"],
554
+ });
1073
555
 
1074
- // The "name" field filter is still passed normally as an argument
1075
- await userRepository.findManyByNameDistinctRole("John");
556
+ const affectedRows = await userRepository.query<number>(
557
+ 'UPDATE "user" SET active = true WHERE id = $1',
558
+ { args: ["123"], modifying: true },
559
+ );
1076
560
  ```
1077
561
 
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.
562
+ | Option | Type | Default | Description |
563
+ | ----------- | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
564
+ | `args` | `any[]` | `undefined` | Positional parameters injected into `$1`, `$2`, ... placeholders. Never interpolate values directly into the SQL string. |
565
+ | `db` | `any` | Repository's default client | Database client or transaction to run this query in. |
566
+ | `modifying` | `boolean` | `false` | When `true`, treats the statement as `INSERT`/`UPDATE`/`DELETE`. |
1079
567
 
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`.
568
+ Just like base, dynamic and query methods, `query()` accepts `db` in `options` to participate in a `transaction()` block.
1081
569
 
1082
570
  ---
1083
571
 
1084
- ### Method configuration
1085
-
1086
- Each entry in `methods` accepts the following options:
1087
-
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). |
1099
-
1100
- ---
1101
-
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);
1113
- ```
1114
-
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
1122
-
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.
1124
-
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.
1126
-
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
- },
1150
- },
1151
- },
1152
- }).build(prisma);
1153
- ```
1154
-
1155
- **Calling a Query Method:**
572
+ ## Transactions
1156
573
 
1157
- Every Query Method takes a single argument shaped as `{ args: [...], db? }`:
574
+ All methods (base, dynamic, and query) accept `options.db` to participate in a shared transaction:
1158
575
 
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
- });
576
+ ```typescript
577
+ await userRepository.transaction(async tx => {
578
+ const user = await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1165
579
 
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
- });
580
+ await userLogsRepository.save(
581
+ { action: "User created", data: { userId: user.id } },
582
+ { db: tx },
583
+ );
1177
584
  });
1178
585
  ```
1179
586
 
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).
1189
-
1190
- ---
587
+ Different repositories can share the same transaction as long as their adapters point to the same underlying ORM connection.
1191
588
 
1192
- ## Relations in save
589
+ `transaction()` accepts an optional `VSRepoTransactionOptions` as its second argument:
1193
590
 
1194
- Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
591
+ ```typescript
592
+ import { TransactionIsolationLevel } from "vsrepo";
1195
593
 
1196
- ```ts
1197
- import type { Prisma } from "../../generated/prisma/client";
1198
-
1199
- type User = Prisma.userGetPayload<{
1200
- include: { profile: true; posts: true };
1201
- }>;
1202
-
1203
- const userRepository = setupVSRepo<User, "user">()(({
1204
- tableName: "user",
1205
- pkName: "id",
1206
-
1207
- relations: {
1208
- profile: {
1209
- pk: "id",
1210
- mode: "oto",
1211
- restriction: "set",
594
+ await userRepository.transaction(
595
+ async tx => {
596
+ await userRepository.save({ name: "Maria", email: "maria@email.com" }, { db: tx });
1212
597
  },
1213
- posts: {
1214
- pk: "id",
1215
- mode: "otm",
1216
- restriction: "add",
1217
- },
1218
- },
1219
- }).build(prisma);
598
+ { isolationLevel: TransactionIsolationLevel.SERIALIZABLE, timeoutMs: 5000 },
599
+ );
1220
600
  ```
1221
601
 
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 |
602
+ | Option | Type | Description |
603
+ | ----------------- | -------------------------- | -------------------------------------------------------------------------------- |
604
+ | `isolationLevel` | `TransactionIsolationLevel` | Isolation level to use for the transaction. Defaults to the underlying ORM's default. |
605
+ | `timeoutMs` | `number` | Maximum time (in ms) the transaction is allowed to run before being aborted. |
1230
606
 
1231
- **Restrictions:**
607
+ `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.
1232
608
 
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 |
609
+ ---
1237
610
 
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.
611
+ ## Utility types
1251
612
 
1252
- **`mto` relation with nullable:**
613
+ 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:
1253
614
 
1254
- Use `nullable` (lowercase) to allow unlinking a many-to-one relation:
615
+ ```typescript
616
+ import type {
617
+ MethodOptions,
618
+ Pagination,
619
+ Ordering,
620
+ OrderByField,
621
+ SortDirection,
622
+ SeeMode,
623
+ DeepPartial,
624
+ CountResult,
625
+ QueryMethodArg,
626
+ KeysOfType,
627
+ Primitive,
628
+ VSRepoWhere,
629
+ VSRepoOrmTypes,
630
+ VSRepoTransactionOptions,
631
+ TransactionIsolationLevel,
632
+ } from "vsrepo";
633
+ ```
634
+
635
+ | Type | Description | Used by |
636
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
637
+ | `MethodOptions<T, K>` | Options accepted as the last argument of every base and dynamic method: `select`, `relations`, `see`, `db`. | [Base methods](#base-methods), [Dynamic methods](#dynamic-methods). |
638
+ | `Pagination` | `{ limit?, offset? }` accepted by `getAll` and by `Paginated` dynamic methods. | [Base methods](#base-methods), [Ordering, pagination and distinct](#ordering-pagination-and-distinct). |
639
+ | `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). |
640
+ | `SeeMode` | `"active" \| "removed" \| "all"` — controls visibility of soft-deleted records. | [Soft-delete](#soft-delete). |
641
+ | `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`. |
642
+ | `CountResult` | `{ count: number }` — the shape returned by batch operations. | `removeList`, `softRemoveList`, `restoreList`, `createManyIgnoreConflicts`. |
643
+ | `QueryMethodArg<T>` | `{ args?: T, db? }` — positional SQL parameters (`$1`, `$2`, ...) and transaction client for `@QueryMethod`. | [Query methods (raw SQL)](#query-methods-raw-sql). |
644
+ | `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. |
645
+ | `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. |
646
+ | `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). |
647
+ | `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). |
648
+ | `VSRepoTransactionOptions` | `{ isolationLevel?, timeoutMs? }` — options accepted as the second argument of `transaction()`. | [Transactions](#transactions). |
649
+ | `TransactionIsolationLevel` | Enum of standard SQL isolation levels (`READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`) accepted by `VSRepoTransactionOptions.isolationLevel`. | [Transactions](#transactions). |
650
+
651
+ ### `DeepPartial<T>`
652
+
653
+ 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:
654
+
655
+ ```typescript
656
+ type User = { id: string; name: string; address: { city: string; zip: string } };
657
+
658
+ const patch: DeepPartial<User> = {
659
+ address: { city: "São Paulo" }, // zip can be omitted; city keeps its type
660
+ };
1255
661
 
1256
- ```ts
1257
- relations: {
1258
- category: {
1259
- pk: "id",
1260
- mode: "mto",
1261
- restriction: "set",
1262
- nullable: true, // allows passing null to unlink
1263
- },
662
+ await userRepository.patch(id, patch);
663
+ ```
664
+
665
+ ### `KeysOfType<T, K>`
666
+
667
+ 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:
668
+
669
+ ```typescript
670
+ type User = { id: string; age: number; name: string };
671
+ type StringKeys = KeysOfType<User, string>; // "id" | "name"
672
+ ```
673
+
674
+ ### `Ordering<T>`
675
+
676
+ Accepts either a single ordering object or an array of them, applied in the order they're declared:
677
+
678
+ ```typescript
679
+ const order: Ordering<User> = { createdAt: "desc" };
680
+ const chained: Ordering<User> = [{ name: "asc" }, { createdAt: "desc" }];
681
+
682
+ await userRepository.getAll({ order: chained });
683
+ ```
684
+
685
+ ---
686
+
687
+ ## Writing your own adapter
688
+
689
+ 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:
690
+
691
+ ```typescript
692
+ export abstract class VSRepoAdapter<T> {
693
+ abstract runInTransaction<R>(
694
+ fn: (tx: any) => Promise<R>,
695
+ options?: VSRepoTransactionOptions,
696
+ ): Promise<R>;
697
+ abstract getDbClient(): any;
698
+ abstract query<T = any>(query: string, options?: AdapterQueryOptions): Promise<T>;
699
+ abstract findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T | null>;
700
+ abstract findOneOrThrow(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
701
+ abstract findMany(
702
+ where: VSRepoWhere<T>,
703
+ options?: AdapterMethodOptions<T> & { distinct?: (keyof T)[] },
704
+ ): Promise<T[]>;
705
+ abstract save(obj: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
706
+ abstract saveMany(objs: DeepPartial<T>[], options?: AdapterMethodOptions<T>): Promise<T[]>;
707
+ abstract create(objs: DeepPartial<T>, options?: AdapterMethodOptions<T>): Promise<T>;
708
+ abstract createMany(
709
+ objs: DeepPartial<T>[],
710
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
711
+ ): Promise<CountResult>;
712
+ abstract createManyReturning(
713
+ objs: DeepPartial<T>[],
714
+ options?: AdapterMethodOptions<T> & { ignoreConflicts?: boolean },
715
+ ): Promise<T[]>;
716
+ abstract delete(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<T>;
717
+ abstract deleteMany(
718
+ where: VSRepoWhere<T>,
719
+ options?: AdapterMethodOptions<T>,
720
+ ): Promise<CountResult>;
721
+ abstract deleteManyReturning(
722
+ where: VSRepoWhere<T>,
723
+ options?: AdapterMethodOptions<T>,
724
+ ): Promise<T[]>;
725
+ abstract update(
726
+ where: VSRepoWhere<T>,
727
+ obj: DeepPartial<T>,
728
+ options?: AdapterMethodOptions<T>,
729
+ ): Promise<T>;
730
+ abstract updateMany(
731
+ where: VSRepoWhere<T>,
732
+ obj: DeepPartial<T>,
733
+ options?: AdapterMethodOptions<T>,
734
+ ): Promise<CountResult>;
735
+ abstract updateManyReturning(
736
+ where: VSRepoWhere<T>,
737
+ obj: DeepPartial<T>,
738
+ options?: AdapterMethodOptions<T>,
739
+ ): Promise<T[]>;
740
+ abstract count(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<number>;
741
+ abstract exists(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>): Promise<boolean>;
742
+ abstract merge<K>(
743
+ where: VSRepoWhere<T>,
744
+ obj: DeepPartial<T>,
745
+ options?: AdapterMethodOptions<T>,
746
+ ): Promise<K & T>;
747
+ abstract upsert(
748
+ where: VSRepoWhere<T>,
749
+ create: DeepPartial<T>,
750
+ update: DeepPartial<T>,
751
+ options?: AdapterMethodOptions<T>,
752
+ ): Promise<T>;
1264
753
  }
1265
754
  ```
1266
755
 
1267
- ---
756
+ `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.
1268
757
 
1269
- ## Transactions
758
+ ### Logging from your adapter
1270
759
 
1271
- All methods accept `options.db` to participate in a transaction:
760
+ `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:
1272
761
 
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
- );
762
+ ```typescript
763
+ import { VSLogger, VSLogLevel } from "vsrepo";
1279
764
 
1280
- await userLogsRepository.save(
1281
- { action: "User registration", data: { registeredUser: user.id } },
1282
- { db: tx }
1283
- );
1284
- });
1285
- ```
765
+ export class MyOrmAdapter<T> extends VSRepoAdapter<T> {
766
+ private readonly logger = new VSLogger(VSLogLevel.WARN, "MyOrmAdapterLogger");
1286
767
 
1287
- For `saveList` and `patchList`, the `db` field must be a `DbTransaction`:
1288
-
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 });
1293
-
1294
- await userLogsRepository.save(
1295
- { action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
1296
- { db: tx }
1297
- );
1298
- });
768
+ async findOne(where: VSRepoWhere<T>, options?: AdapterMethodOptions<T>) {
769
+ const start = this.logger.startPerformLog("adapter findOne");
770
+ try {
771
+ // ... talk to the ORM ...
772
+ this.logger.endPerformLog(start);
773
+ return result;
774
+ } catch (err) {
775
+ this.logger.endPerformLog(start);
776
+ this.logger.logError("adapter findOne failed", err);
777
+ throw err;
778
+ }
779
+ }
780
+ }
1299
781
  ```
1300
782
 
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
- },
783
+ | Method | Description |
784
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
785
+ | `new VSLogger(logLevel, name, slowThresholdMs?)` | Creates a logger; `name` prefixes every line, `slowThresholdMs` defaults to 300. |
786
+ | `logDebug/logInfo/logWarn(text, obj?)` | Logs at the given level if `logLevel` allows it; `obj` is appended as pretty-printed JSON. |
787
+ | `logError(text, err?)` | Logs at `ERROR`; if `err` is an `Error`, only `name`/`message`/`stack`/`cause` are logged. |
788
+ | `startPerformLog(operation)` / `endPerformLog(data)` | Bracket a block to log its duration, escalating to `WARN` if it exceeds `slowThresholdMs`. |
789
+ | `getLogLevel()` | Returns the logger's configured `VSLogLevel`. |
1318
790
 
1319
- activateMultiple: async (ids: string[]) => {
1320
- return repo.patchList(ids.map(id => [id, { active: true }]));
1321
- },
1322
- }));
1323
- ```
791
+ This is purely a convenience for adapter authors — nothing in the core requires your adapter to use it.
1324
792
 
1325
793
  ---
1326
794
 
1327
795
  ## Error handling
1328
796
 
1329
- VSRepository throws `VSRepoError` and its subclasses in specific situations (Prisma errors are not overridden):
797
+ 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
798
 
1331
- ```ts
1332
- import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
799
+ ```typescript
800
+ import { VSRepoError } from "vsrepo";
1333
801
 
1334
802
  try {
1335
- const user = await userRepository.getOrThrow("id-that-does-not-exist");
803
+ await userRepository.get(id);
1336
804
  } 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
- }
805
+ if (error instanceof VSRepoError) {
806
+ console.error(`[${error.type}] ${error.message}`);
807
+ }
1344
808
  }
1345
809
  ```
1346
810
 
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";
811
+ | `VSRepoErrorType` | Raised when |
812
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
813
+ | `DECORATOR` | Invalid arguments were passed to `@DynamicMethod` or `@QueryMethod`. |
814
+ | `RESOLVER` | The library failed to resolve a dynamic/query method's configuration into a callable method (e.g. an unknown method name). |
815
+ | `DYNAMIC` | A resolved dynamic method failed at runtime (e.g. missing arguments). |
816
+ | `VALIDATOR` | Invalid method options or arguments were detected during validation. |
817
+ | `BASE` | Invalid usage of a base method (`get`, `save`, `remove`, etc). |
818
+ | `ADAPTER` | A `VSRepoAdapter` failed while talking to the underlying ORM/database — always thrown as `VSRepoAdapterError`. |
1375
819
 
1376
- type DbClient = PrismaClient;
1377
- type DbTransaction = Prisma.TransactionClient;
1378
- type ClientOrTransaction = DbClient | DbTransaction;
1379
- ```
1380
-
1381
- ### Soft-delete visibility type
820
+ ### `VSRepoAdapterError` and `AdapterErrorCode`
1382
821
 
1383
- ```ts
1384
- import type { SeeMode } from "../../generated/vsrepo";
822
+ 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:
1385
823
 
1386
- type SeeMode = "active" | "removed" | "all";
1387
- ```
824
+ ```typescript
825
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1388
826
 
1389
- ### Types derived from the Prisma model
827
+ try {
828
+ await userRepository.save({ name: "Maria" });
829
+ } catch (error) {
830
+ if (error instanceof VSRepoAdapterError) {
831
+ console.error(`[${error.code}] ${error.message}`, error.originalError);
1390
832
 
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";
833
+ if (error.code === AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION) {
834
+ // handle a duplicate key, e.g. return a friendly message
835
+ }
836
+ }
837
+ }
1403
838
  ```
1404
839
 
1405
- ### Method options types
840
+ | Property | Type | Description |
841
+ | --------------- | ------------------ | ----------------------------------------------------------------------------------- |
842
+ | `code` | `AdapterErrorCode` | Stable, adapter-agnostic code classifying the failure. |
843
+ | `originalError` | `unknown` | The raw error (or `null`/`undefined`) thrown by the underlying ORM/database driver. |
844
+ | `message` | `string` | Human-readable description of the adapter failure. |
845
+ | `type` | `VSRepoErrorType` | Always `VSRepoErrorType.ADAPTER`. |
846
+ | `cause` | `unknown` | Optional root cause the error was chained from. |
1406
847
 
1407
- ```ts
1408
- import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
848
+ Adapter implementations construct it directly when mapping an ORM failure:
1409
849
 
1410
- // MethodOptions<S, IM> — options passed into the repository's methods
1411
- type Opts = MethodOptions<"public" | "minimal", "withPosts">;
850
+ ```typescript
851
+ import { VSRepoAdapterError, AdapterErrorCode } from "vsrepo";
1412
852
 
1413
- // MethodOptionsModel<TRepo> — derived from a configured VSRepository instance
1414
- const userVSRepo = setupVSRepo<User, "user">()(config);
1415
- type OptsModel = MethodOptionsModel<typeof userVSRepo>;
853
+ throw new VSRepoAdapterError(
854
+ "user creation failed",
855
+ AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
856
+ originalError, // raw DB/driver error
857
+ );
1416
858
  ```
1417
859
 
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.
1421
-
1422
- ### Configuration types
860
+ #### `AdapterErrorCode`
1423
861
 
1424
- ```ts
1425
- import type {
1426
- MethodConfig,
1427
- RepoConfig,
1428
- BuildConfig,
1429
- RepositoryRelations,
1430
- ExtractRelationConfig,
1431
- } from "../../generated/vsrepo";
1432
- ```
1433
-
1434
- ### Built repository type
862
+ `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:
1435
863
 
1436
- ```ts
1437
- import type { RepositoryOf } from "../../generated/vsrepo";
864
+ ```typescript
865
+ import { AdapterErrorCode } from "vsrepo";
1438
866
 
1439
- const userVSRepo = setupVSRepo<User, "user">()({ ... });
1440
- type UserRepository = RepositoryOf<typeof userVSRepo>;
867
+ console.log(AdapterErrorCode.UNIQUE_CONSTRAINT_VIOLATION); // "UNIQUE_CONSTRAINT_VIOLATION"
1441
868
  ```
1442
869
 
1443
- `RepositoryOf` accepts three parameters:
870
+ | Code | Meaning |
871
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
872
+ | `UNKNOWN` | Unclassified/unknown error; the fallback when no more specific code matches. |
873
+ | `MISSING_DB_CLIENT` | Database client (or connection pool) not provided or could not be resolved. |
874
+ | `CONNECTION_FAILED` | Could not reach/connect to the database, or an established connection was lost/terminated. |
875
+ | `CONNECTION_POOL_EXHAUSTED` | Connection pool exhausted/depleted — no connection available, all busy or the limit was reached. |
876
+ | `TIMEOUT` | Database did not respond in time; a query exceeded its allowed timeout. |
877
+ | `UNIQUE_CONSTRAINT_VIOLATION` | Unique constraint (duplicate key) violated. E.g. Postgres/SQLite `23505`, MySQL `1062`. |
878
+ | `FOREIGN_KEY_VIOLATION` | Foreign key constraint violated (referenced row missing). |
879
+ | `NOT_NULL_VIOLATION` | NOT NULL constraint violated. |
880
+ | `CHECK_VIOLATION` | CHECK constraint violated. |
881
+ | `CONSTRAINT_VIOLATION` | General integrity/constraint violation not covered by a more specific code. |
882
+ | `NOT_FOUND` | Requested record not found (e.g. a `findOneOrThrow`-style operation). |
883
+ | `INVALID_DATA` | Field value invalid for its type/length, or a required value is missing. |
884
+ | `VALUE_TOO_LONG` | Provided value exceeds the column/field length limit. |
885
+ | `CONVERSION_ERROR` | Value could not be converted/cast to the target type. E.g. Postgres `22P02`, MySQL `1366`. |
886
+ | `INVALID_QUERY` | SQL query/stored procedure is malformed or invalid. |
887
+ | `TABLE_OR_COLUMN_NOT_FOUND` | Referenced table/column/relation does not exist. |
888
+ | `DEADLOCK` | Operation aborted by a lock timeout or deadlock between concurrent transactions. |
889
+ | `LOCK_TIMEOUT` | Could not acquire a required database lock in time. |
890
+ | `LOCKED` | Record is locked and cannot be modified. |
891
+ | `ACCESS_DENIED` | Current user/role does not have permission for the operation. |
892
+ | `INVALID_CREDENTIALS` | Invalid connection credentials (host/user/password). |
893
+ | `ROW_NOT_ALLOWED` | Authenticated user does not own the record / row-level security rejected it. |
894
+ | `MODEL_NOT_FOUND` | Entity/model or table not defined/mapped in the ORM, or the adapter lacks model metadata to build the query. |
895
+ | `FIELD_NOT_FOUND` | Field/column name in the data or `where` does not exist on the entity/model. |
896
+ | `TRANSACTION_CLOSED` | Transaction used after it was committed/rolled back. |
897
+ | `TRANSACTION_ALREADY_STARTED` | A nested transaction could not be opened (e.g. nested `transaction()` calls). |
898
+ | `TRANSACTION_CONFLICT` | Transaction failed to commit and was rolled back. |
899
+ | `TRANSACTION_NOT_STARTED` | No active transaction when one was required. |
900
+ | `CONNECTION_CLOSED` | Connection closed/terminated while a transaction or query was in progress. |
901
+ | `INVALID_PARTIAL` | `merge`/`upsert`/`update` received a partial object that is invalid or missing required keys. |
902
+ | `NOT_SUPPORTED` | Unsupported feature/operation requested from the adapter (e.g. raw `query()` not supported). |
903
+ | `INVALID_ADAPTER_CONFIG` | Adapter configuration invalid or incomplete (missing required options, or options with an invalid type/value). |
904
+ | `INTERNAL` | Internal adapter bug or unrecoverable state; should rarely be used — prefer a more specific code. |
1444
905
 
1445
- ```ts
1446
- type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
1447
- ```
906
+ #### `VSRepoError` vs. raw ORM errors
1448
907
 
1449
- ### `save` and `patch` payload types
908
+ 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
909
 
1451
- ```ts
1452
- import type { SaveObject, PatchObject } from "../../generated/vsrepo";
910
+ ---
1453
911
 
1454
- const userVSRepo = setupVSRepo<User, "user">()(({
1455
- tableName: "user",
1456
- pkName: "id",
1457
- relations: {
1458
- profile: { pk: "id", mode: "oto", restriction: "set" },
1459
- },
1460
- });
912
+ ## Logging
1461
913
 
1462
- type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
1463
- type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
1464
- ```
914
+ Every repository has an internal logger, configured via `logLevel` and `logSlowThresholdMs` on the constructor options:
1465
915
 
1466
- ---
916
+ ```typescript
917
+ import { VSLogLevel } from "vsrepo";
1467
918
 
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
919
+ super({
920
+ pkName: "id",
921
+ adapter,
922
+ logLevel: VSLogLevel.DEBUG,
923
+ logSlowThresholdMs: 200,
1484
924
  });
1485
925
  ```
1486
926
 
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
- ```
927
+ | Level | Meaning |
928
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
929
+ | `DEBUG` | Verbose internal details, including every resolved query — very useful for debugging dynamic methods. |
930
+ | `INFO` | High-level lifecycle events, such as repository initialization. |
931
+ | `WARN` (default) | Recoverable issues and slow operations (see `logSlowThresholdMs`, defaults to 300ms). |
932
+ | `ERROR` | Failures raised while executing an operation. |
1517
933
 
1518
- ### `.extend(fn)`
934
+ ---
1519
935
 
1520
- ```ts
1521
- repo.extend((repo) => ({
1522
- myMethod: () => { ... }
1523
- }));
1524
- ```
936
+ ## Development
1525
937
 
1526
- ---
938
+ The v2 core is built and packed from this branch as a standard npm package:
1527
939
 
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
- ```
940
+ ```bash
941
+ # 1. Install dependencies
942
+ pnpm install
1545
943
 
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.
944
+ # 2. Compile the TypeScript sources into dist/ (removes a previous dist/ first)
945
+ pnpm build
1547
946
 
1548
- ---
947
+ # 3. (Optional) Inspect what would be published without writing a tarball
948
+ npm pack --dry-run
1549
949
 
1550
- ## Contributing
950
+ # 4. Produce the installable tarball (runs `prepack` -> `pnpm build` automatically)
951
+ npm pack
1551
952
 
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)**):
953
+ # 5. Consume it locally in another project
954
+ npm install ../path/to/vsrepo-1.4.0.tgz
955
+ ```
1553
956
 
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**.
957
+ Notes:
1558
958
 
1559
- To report issues or suggest new features, open an **Issue**.
959
+ - `pnpm build` runs `tsc -p tsconfig.build.json`, which outputs the compiled JS and generated type declarations into `dist/` with `rootDir: src`.
960
+ - The published package contains **only** the `dist/` folder plus the READMEs and `LICENSE` (see `files` in `package.json`). Source, tests, the `v1/` folder and `generated/` are **not** shipped — the adapters will live in their own `@vsrepo/*-adapter` packages.
961
+ - The core is ORM-agnostic and has no `@prisma/client` peer dependency.
1560
962
 
1561
963
  ---
1562
964
 
1563
965
  ## Requirements
1564
966
 
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`:
967
+ - Node.js 18+
968
+ - TypeScript, with **legacy/experimental decorators** enabled (required by `@DynamicMethod`/`@QueryMethod`):
1571
969
 
1572
970
  ```json
1573
971
  {
1574
- "compilerOptions": {
1575
- "target": "ES2020",
1576
- "module": "NodeNext",
1577
- "moduleResolution": "NodeNext",
1578
- "strict": true,
1579
- "skipLibCheck": true,
1580
- "lib": ["ES2020"]
1581
- }
972
+ "compilerOptions": {
973
+ "experimentalDecorators": true
974
+ }
1582
975
  }
1583
976
  ```
1584
977
 
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.
978
+ - `reflect-metadata` (bundled as a dependency, imported internally — you don't need to import it yourself)
979
+ - 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
980
 
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`.
981
+ ---
1600
982
 
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.
983
+ ## Contributing
1602
984
 
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.
985
+ Contributions are welcome, especially towards finishing the Prisma and TypeORM adapters! (**[GitHub repository](https://github.com/jaobrabo123/VSRepository)**):
1604
986
 
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.
987
+ 1. **Fork** the project.
988
+ 2. Create a branch off `v2` for your change: `git checkout -b v2-my-change`.
989
+ 3. Push your branch: `git push origin v2-my-change`.
990
+ 4. Open a **Pull Request** against `v2`.
1606
991
 
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.
992
+ To report issues or suggest features, open an **Issue**.