vsrepo 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +605 -567
- package/dist/VSRepository.d.ts +14 -5
- package/dist/VSRepository.js +1 -0
- package/dist/internal/resolvers/dbAndPrismaArgs.resolve.js +4 -1
- package/dist/internal/resolvers/dinamic-method-customization.resolve.js +14 -2
- package/dist/internal/resolvers/dinamic-method-info.resolve.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,66 +4,67 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Repository pattern library for projects using **Prisma**, with full **TypeScript** support and automatic **type inference**.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
VSRepository lets you create strongly-typed repositories with:
|
|
10
10
|
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **Type safety**
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
11
|
+
- Automatic **base methods**: `get`, `getOrThrow`, `getList`, `save`, `saveList`, `remove`, `removeList`, `patch`, `patchList`, `merge`, `getAll`, `total`, `has`
|
|
12
|
+
- **Native soft-delete**: `softRemove`, `softRemoveList`, `restore`, `restoreList`
|
|
13
|
+
- **Dynamic methods** inferred from their name: `findOneByEmail`, `findManyPaginated`, `updateById`, `deleteManyByNameStartsWith`
|
|
14
|
+
- Reusable **select models** for different data projections
|
|
15
|
+
- **Type safety** across 100% of operations
|
|
16
|
+
- Native Prisma **transactions** (automatic in `saveList` and `patchList`)
|
|
17
|
+
- **Extensibility** with custom methods
|
|
18
18
|
|
|
19
|
-
> 💡
|
|
19
|
+
> 💡 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.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Table of contents
|
|
24
24
|
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
25
|
+
- [Installation](#installation)
|
|
26
|
+
- [Generating the types](#generating-the-types)
|
|
27
|
+
- [Basic usage](#basic-usage)
|
|
28
|
+
- [NestJS integration](#nestjs-integration)
|
|
29
|
+
- [Base methods](#base-methods)
|
|
30
30
|
- [Soft-delete](#soft-delete)
|
|
31
|
-
- [
|
|
31
|
+
- [Batch operations](#batch-operations)
|
|
32
32
|
- [Merge](#merge)
|
|
33
|
-
- [
|
|
33
|
+
- [Configuring the base methods](#configuring-the-base-methods)
|
|
34
34
|
- [Select Models](#select-models)
|
|
35
35
|
- [Include Models](#include-models)
|
|
36
|
-
- [Required Where](#
|
|
36
|
+
- [Required Where](#required-where)
|
|
37
37
|
- [Default Ordenation](#default-ordenation)
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
- [
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
38
|
+
- [`see` option](#see-option)
|
|
39
|
+
- [Dynamic methods](#dynamic-methods)
|
|
40
|
+
- [Available prefixes](#available-prefixes)
|
|
41
|
+
- [Field filters](#field-filters)
|
|
42
|
+
- [Logical operators](#logical-operators)
|
|
43
|
+
- [Relation filters](#relation-filters)
|
|
44
|
+
- [Pagination and ordering suffixes](#pagination-and-ordering-suffixes)
|
|
45
|
+
- [Distinct](#distinct)
|
|
46
|
+
- [Method configuration](#method-configuration)
|
|
47
|
+
- [Aggregate and GroupBy](#aggregate-and-groupby)
|
|
48
|
+
- [Relations in save](#relations-in-save)
|
|
49
|
+
- [Transactions](#transactions)
|
|
50
|
+
- [Extending a repository](#extending-a-repository)
|
|
51
|
+
- [Error handling](#error-handling)
|
|
52
|
+
- [Utility types](#utility-types)
|
|
52
53
|
- [API Reference](#api-reference)
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
54
|
+
- [Practical examples](#practical-examples)
|
|
55
|
+
- [Contributing](#contributing)
|
|
56
|
+
- [Requirements](#requirements)
|
|
56
57
|
- [Troubleshooting](#troubleshooting)
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
60
|
-
##
|
|
61
|
+
## Installation
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
npm i vsrepo @prisma/client
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
Generate the Prisma Client:
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
npx prisma generate
|
|
@@ -71,15 +72,15 @@ npx prisma generate
|
|
|
71
72
|
|
|
72
73
|
---
|
|
73
74
|
|
|
74
|
-
##
|
|
75
|
+
## Generating the types
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
VSRepository needs to know the real path of your Prisma Client to generate the typings correctly.
|
|
77
78
|
|
|
78
79
|
```bash
|
|
79
80
|
npx vsrepo generate
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
Equivalent to:
|
|
83
84
|
|
|
84
85
|
```bash
|
|
85
86
|
npx vsrepo generate \
|
|
@@ -87,14 +88,14 @@ npx vsrepo generate \
|
|
|
87
88
|
--prisma generated/prisma
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
**
|
|
91
|
+
**Available flags:**
|
|
91
92
|
|
|
92
|
-
| Flag | Alias |
|
|
93
|
-
| ---------- | ----- |
|
|
94
|
-
| `--output` | `-o` | `generated/vsrepo`
|
|
95
|
-
| `--prisma` | `-p` | `generated/prisma`
|
|
93
|
+
| Flag | Alias | Default |
|
|
94
|
+
| ---------- | ----- | -------------------- |
|
|
95
|
+
| `--output` | `-o` | `generated/vsrepo` |
|
|
96
|
+
| `--prisma` | `-p` | `generated/prisma` |
|
|
96
97
|
|
|
97
|
-
**
|
|
98
|
+
**Generated files:**
|
|
98
99
|
|
|
99
100
|
```
|
|
100
101
|
generated/vsrepo/
|
|
@@ -105,21 +106,21 @@ generated/vsrepo/
|
|
|
105
106
|
└── index.ts
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
After generating, always import from the generated folder:
|
|
109
110
|
|
|
110
111
|
```ts
|
|
111
|
-
//
|
|
112
|
+
// CORRECT ✅
|
|
112
113
|
import { setupVSRepo } from "../../generated/vsrepo";
|
|
113
114
|
|
|
114
|
-
//
|
|
115
|
+
// WRONG ❌
|
|
115
116
|
import { setupVSRepo } from "vsrepo";
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
---
|
|
119
120
|
|
|
120
|
-
##
|
|
121
|
+
## Basic usage
|
|
121
122
|
|
|
122
|
-
###
|
|
123
|
+
### Configuring the Prisma Client
|
|
123
124
|
|
|
124
125
|
```ts
|
|
125
126
|
// src/configs/db.ts
|
|
@@ -133,56 +134,56 @@ const prisma = new PrismaClient({ adapter });
|
|
|
133
134
|
export default prisma;
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
###
|
|
137
|
+
### Creating a repository
|
|
137
138
|
|
|
138
139
|
```ts
|
|
139
|
-
// src/repositories/
|
|
140
|
+
// src/repositories/userRepository.ts
|
|
140
141
|
import prisma from "../configs/db";
|
|
141
142
|
import { setupVSRepo } from "../../generated/vsrepo";
|
|
142
|
-
import type {
|
|
143
|
+
import type { User } from "../../generated/prisma/client";
|
|
143
144
|
|
|
144
|
-
const
|
|
145
|
-
tableName: "
|
|
145
|
+
const userRepository = setupVSRepo<User, "User">()(({
|
|
146
|
+
tableName: "user",
|
|
146
147
|
pkName: "id",
|
|
147
148
|
selectModels: {
|
|
148
|
-
public: { id: true,
|
|
149
|
+
public: { id: true, name: true, email: true },
|
|
149
150
|
},
|
|
150
151
|
defaultSelectModel: "public",
|
|
151
152
|
}).build(prisma);
|
|
152
153
|
|
|
153
|
-
export default
|
|
154
|
+
export default userRepository;
|
|
154
155
|
```
|
|
155
156
|
|
|
156
|
-
###
|
|
157
|
+
### Using the repository
|
|
157
158
|
|
|
158
159
|
```ts
|
|
159
|
-
import
|
|
160
|
+
import userRepository from "./repositories/userRepository";
|
|
160
161
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
email: "
|
|
164
|
-
|
|
162
|
+
const user = await userRepository.save({
|
|
163
|
+
name: "John",
|
|
164
|
+
email: "john@email.com",
|
|
165
|
+
password: "password",
|
|
165
166
|
});
|
|
166
167
|
|
|
167
|
-
const
|
|
168
|
-
const
|
|
168
|
+
const found = await userRepository.get(user.id);
|
|
169
|
+
const all = await userRepository.getAll();
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
user.name = "John Smith";
|
|
171
172
|
|
|
172
|
-
await
|
|
173
|
-
await
|
|
173
|
+
await userRepository.save(user);
|
|
174
|
+
await userRepository.remove(user.id);
|
|
174
175
|
```
|
|
175
176
|
|
|
176
177
|
---
|
|
177
178
|
|
|
178
|
-
##
|
|
179
|
+
## NestJS integration
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
VSRepository can be easily integrated into NestJS projects through providers. Below is a complete example using NestJS's dependency injection pattern.
|
|
181
182
|
|
|
182
|
-
###
|
|
183
|
+
### Configuring the repository as a provider
|
|
183
184
|
|
|
184
185
|
```ts
|
|
185
|
-
// src/
|
|
186
|
+
// src/modules/user/user.repository.ts
|
|
186
187
|
import { Provider } from "@nestjs/common";
|
|
187
188
|
import { PrismaService } from "../../database/prisma.service";
|
|
188
189
|
import { UserGetPayload } from "../../../generated/prisma/models";
|
|
@@ -236,12 +237,12 @@ const setupUserRepository = (prisma: PrismaService) => {
|
|
|
236
237
|
|
|
237
238
|
export type UserRepository = ReturnType<typeof setupUserRepository>;
|
|
238
239
|
/*
|
|
239
|
-
|
|
240
|
+
The type can also be inferred using VSRepository's `RepositoryOf`, passing the `userVSRepo` type:
|
|
240
241
|
|
|
241
242
|
export type UserRepository = RepositoryOf<typeof userVSRepo>;
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
NOTE: If you use `.extend` to extend the repository or configure the base methods,
|
|
245
|
+
using `ReturnType` is recommended since it's simpler to infer the type
|
|
245
246
|
*/
|
|
246
247
|
|
|
247
248
|
export const USER_REPOSITORY = Symbol("USER_REPOSITORY");
|
|
@@ -253,10 +254,10 @@ export const UserRepositoryProvider: Provider = {
|
|
|
253
254
|
};
|
|
254
255
|
```
|
|
255
256
|
|
|
256
|
-
###
|
|
257
|
+
### Registering the provider in the module
|
|
257
258
|
|
|
258
259
|
```ts
|
|
259
|
-
// src/
|
|
260
|
+
// src/modules/user/user.module.ts
|
|
260
261
|
import { Module } from "@nestjs/common";
|
|
261
262
|
import { UserRepositoryProvider } from "./user.repository";
|
|
262
263
|
import { UserService } from "./user.service";
|
|
@@ -271,12 +272,12 @@ import { UserController } from "./user.controller";
|
|
|
271
272
|
export class UserModule {}
|
|
272
273
|
```
|
|
273
274
|
|
|
274
|
-
###
|
|
275
|
+
### Using the repository in a service
|
|
275
276
|
|
|
276
277
|
```ts
|
|
277
|
-
// src/
|
|
278
|
+
// src/modules/user/user.service.ts
|
|
278
279
|
import { Injectable, Inject } from "@nestjs/common";
|
|
279
|
-
import { USER_REPOSITORY, UserRepository } from "./user.repository";
|
|
280
|
+
import { USER_REPOSITORY, type UserRepository } from "./user.repository";
|
|
280
281
|
|
|
281
282
|
@Injectable()
|
|
282
283
|
export class UserService {
|
|
@@ -303,182 +304,182 @@ export class UserService {
|
|
|
303
304
|
}
|
|
304
305
|
```
|
|
305
306
|
|
|
306
|
-
**
|
|
307
|
+
**Benefits of this approach:**
|
|
307
308
|
|
|
308
|
-
- ✅ Type-safe repositories
|
|
309
|
-
- ✅
|
|
310
|
-
- ✅
|
|
311
|
-
- ✅
|
|
312
|
-
- ✅
|
|
309
|
+
- ✅ Type-safe repositories with dependency injection
|
|
310
|
+
- ✅ Easy to test (mock the `USER_REPOSITORY`)
|
|
311
|
+
- ✅ Isolation of persistence logic
|
|
312
|
+
- ✅ Repository reuse across multiple services
|
|
313
|
+
- ✅ Transaction support via `PrismaService`
|
|
313
314
|
|
|
314
315
|
---
|
|
315
316
|
|
|
316
|
-
##
|
|
317
|
+
## Base methods
|
|
317
318
|
|
|
318
|
-
|
|
319
|
+
When calling `.build(prisma)`, the base methods below are automatically made available:
|
|
319
320
|
|
|
320
|
-
|
|
|
321
|
-
| ------------------------ |
|
|
322
|
-
| `get(pk)` |
|
|
323
|
-
| `getOrThrow(pk)` |
|
|
324
|
-
| `getList(pks)` |
|
|
325
|
-
| `save(obj)` |
|
|
326
|
-
| `saveList(objs)` |
|
|
327
|
-
| `patch(pk, obj)` |
|
|
328
|
-
| `patchList(tuples)` |
|
|
329
|
-
| `merge(pk, obj)` |
|
|
330
|
-
| `remove(pk)` |
|
|
331
|
-
| `removeList(pks)` |
|
|
332
|
-
| `getAll()` |
|
|
333
|
-
| `total()` |
|
|
334
|
-
| `has(pk)` |
|
|
321
|
+
| Method | Description |
|
|
322
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------|
|
|
323
|
+
| `get(pk)` | Fetches a record by its primary key |
|
|
324
|
+
| `getOrThrow(pk)` | Fetches a record by its primary key; throws `VSRepoRuntimeError` (code `"20727"`) if not found |
|
|
325
|
+
| `getList(pks)` | Fetches multiple records from a list of primary keys |
|
|
326
|
+
| `save(obj)` | Creates or updates — if the object has a `pk` it performs an `upsert`, otherwise a `create` |
|
|
327
|
+
| `saveList(objs)` | Saves an array of objects in a single automatic transaction |
|
|
328
|
+
| `patch(pk, obj)` | Partially updates a record by its primary key |
|
|
329
|
+
| `patchList(tuples)` | Partially updates multiple records via an array of `[pk, obj]` tuples in an automatic transaction |
|
|
330
|
+
| `merge(pk, obj)` | Fetches a record and deep merges it in memory — **does not persist**, returns the merged object |
|
|
331
|
+
| `remove(pk)` | Removes a record by its primary key |
|
|
332
|
+
| `removeList(pks)` | Removes several records by a list of primary keys — returns `{ count }` |
|
|
333
|
+
| `getAll()` | Returns all records (accepts `pagination` and `order` in `options`) |
|
|
334
|
+
| `total()` | Returns the total number of records |
|
|
335
|
+
| `has(pk)` | Checks whether a record exists by its primary key — returns `boolean` |
|
|
335
336
|
|
|
336
|
-
|
|
337
|
+
All of them accept `options` as the last argument.
|
|
337
338
|
|
|
338
339
|
### Soft-delete
|
|
339
340
|
|
|
340
|
-
|
|
341
|
+
When `softRemovekName` is configured on the repository, the following additional methods become available:
|
|
341
342
|
|
|
342
|
-
|
|
|
343
|
-
| -------------------------- |
|
|
344
|
-
| `softRemove(pk)` |
|
|
345
|
-
| `softRemoveList(pks)` |
|
|
346
|
-
| `restore(pk)` |
|
|
347
|
-
| `restoreList(pks)` |
|
|
343
|
+
| Method | Description |
|
|
344
|
+
| -------------------------- | ------------------------------------------------------------------------------------|
|
|
345
|
+
| `softRemove(pk)` | Marks a record as removed by filling `softRemovekName` with the current date |
|
|
346
|
+
| `softRemoveList(pks)` | Marks multiple records as removed in batch — returns `{ count }` |
|
|
347
|
+
| `restore(pk)` | Restores a soft-deleted record, clearing the `softRemovekName` field |
|
|
348
|
+
| `restoreList(pks)` | Restores multiple soft-deleted records in batch — returns `{ count }` |
|
|
348
349
|
|
|
349
350
|
```ts
|
|
350
|
-
const
|
|
351
|
-
tableName: "
|
|
351
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
352
|
+
tableName: "user",
|
|
352
353
|
pkName: "id",
|
|
353
|
-
softRemovekName: "deletedAt", //
|
|
354
|
+
softRemovekName: "deletedAt", // must be a DateTime field in the Prisma schema
|
|
354
355
|
}).build(prisma);
|
|
355
356
|
|
|
356
|
-
await
|
|
357
|
-
await
|
|
357
|
+
await userRepository.softRemove(1);
|
|
358
|
+
await userRepository.restore(1);
|
|
358
359
|
```
|
|
359
360
|
|
|
360
|
-
>
|
|
361
|
+
> 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.
|
|
361
362
|
|
|
362
|
-
###
|
|
363
|
+
### Batch operations
|
|
363
364
|
|
|
364
|
-
`saveList`
|
|
365
|
+
`saveList` and `patchList` automatically run all operations inside a single Prisma transaction. If any operation fails, all previous ones are rolled back.
|
|
365
366
|
|
|
366
367
|
```ts
|
|
367
|
-
// saveList —
|
|
368
|
-
const
|
|
369
|
-
{
|
|
370
|
-
{ id: 2,
|
|
368
|
+
// saveList — creates or updates multiple objects in an automatic transaction
|
|
369
|
+
const users = await userRepository.saveList([
|
|
370
|
+
{ name: "Mary", email: "mary@email.com" },
|
|
371
|
+
{ id: 2, name: "John Updated", email: "john@email.com" },
|
|
371
372
|
]);
|
|
372
373
|
|
|
373
|
-
// patchList —
|
|
374
|
-
const
|
|
375
|
-
[1, {
|
|
376
|
-
[2, {
|
|
374
|
+
// patchList — partially updates multiple records via [pk, obj] tuples
|
|
375
|
+
const updated = await userRepository.patchList([
|
|
376
|
+
[1, { active: false }],
|
|
377
|
+
[2, { name: "New Name" }],
|
|
377
378
|
]);
|
|
378
379
|
```
|
|
379
380
|
|
|
380
|
-
|
|
381
|
+
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):
|
|
381
382
|
|
|
382
383
|
```ts
|
|
383
384
|
await prisma.$transaction(async (tx) => {
|
|
384
|
-
await
|
|
385
|
-
await
|
|
385
|
+
await userRepository.saveList([{ name: "Mary" }, { name: "Gus" }], { db: tx });
|
|
386
|
+
await userRepository.patchList([[1, { active: false }], [2, { active: true }]], { db: tx });
|
|
386
387
|
});
|
|
387
388
|
```
|
|
388
389
|
|
|
389
390
|
### Merge
|
|
390
391
|
|
|
391
|
-
|
|
392
|
+
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.
|
|
392
393
|
|
|
393
394
|
```ts
|
|
394
|
-
const
|
|
395
|
-
//
|
|
395
|
+
const existing = await userRepository.get(1);
|
|
396
|
+
// existing: { id: 1, name: "Mary", profile: { bio: "Hi", age: 25 } }
|
|
396
397
|
|
|
397
|
-
const
|
|
398
|
-
|
|
398
|
+
const merged = await userRepository.merge(1, {
|
|
399
|
+
profile: { bio: "Updated bio" },
|
|
399
400
|
});
|
|
400
|
-
//
|
|
401
|
+
// merged: { id: 1, name: "Mary", profile: { bio: "Updated bio", age: 25 } }
|
|
401
402
|
|
|
402
|
-
//
|
|
403
|
-
await
|
|
403
|
+
// To persist, pass it to save or patch:
|
|
404
|
+
await userRepository.save(merged);
|
|
404
405
|
```
|
|
405
406
|
|
|
406
|
-
|
|
407
|
+
Returns `null` if the record is not found.
|
|
407
408
|
|
|
408
|
-
**
|
|
409
|
+
**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.
|
|
409
410
|
|
|
410
411
|
```ts
|
|
411
|
-
const
|
|
412
|
-
//
|
|
412
|
+
const existing = await userRepository.get(1);
|
|
413
|
+
// existing: {
|
|
413
414
|
// id: 1,
|
|
414
|
-
//
|
|
415
|
-
// { id: 10,
|
|
416
|
-
// { id: 11,
|
|
415
|
+
// posts: [
|
|
416
|
+
// { id: 10, title: "Post A", published: false },
|
|
417
|
+
// { id: 11, title: "Post B", published: true },
|
|
417
418
|
// ],
|
|
418
419
|
// }
|
|
419
420
|
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
{ id: 10,
|
|
423
|
-
{
|
|
421
|
+
const merged = await userRepository.merge(1, {
|
|
422
|
+
posts: [
|
|
423
|
+
{ id: 10, published: true }, // same PK (id: 10) → merges with the existing item
|
|
424
|
+
{ title: "Post C" }, // no PK → added as a new item
|
|
424
425
|
],
|
|
425
426
|
});
|
|
426
|
-
//
|
|
427
|
+
// merged: {
|
|
427
428
|
// id: 1,
|
|
428
|
-
//
|
|
429
|
-
// { id: 10,
|
|
430
|
-
// { id: 11,
|
|
431
|
-
// {
|
|
429
|
+
// posts: [
|
|
430
|
+
// { id: 10, title: "Post A", published: true }, // merged
|
|
431
|
+
// { id: 11, title: "Post B", published: true }, // kept, wasn't in the sent array
|
|
432
|
+
// { title: "Post C" }, // added
|
|
432
433
|
// ],
|
|
433
434
|
// }
|
|
434
435
|
```
|
|
435
436
|
|
|
436
|
-
> Note
|
|
437
|
+
> 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.
|
|
437
438
|
|
|
438
|
-
###
|
|
439
|
+
### Configuring the base methods
|
|
439
440
|
|
|
440
|
-
|
|
441
|
+
The second argument of `.build(prisma, config)` lets you adjust the repository's global behavior and customize each base method individually through `baseMethods`.
|
|
441
442
|
|
|
442
443
|
```ts
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
//
|
|
444
|
+
userVSRepo.build(prisma, {
|
|
445
|
+
// Shows VSRepository's internal logs on the console (built queries, detected prefix,
|
|
446
|
+
// applied filters, etc). Great for debugging dynamic methods. Default = false.
|
|
446
447
|
showWorking: true,
|
|
447
448
|
|
|
448
449
|
baseMethods: {
|
|
449
450
|
get: {
|
|
450
|
-
//
|
|
451
|
-
//
|
|
451
|
+
// Enables/disables the method on the final repository. If `false`, the method
|
|
452
|
+
// doesn't even appear in the repository's type (it's not just a runtime error). Default = true.
|
|
452
453
|
active: true,
|
|
453
454
|
|
|
454
|
-
// Select model
|
|
455
|
-
//
|
|
455
|
+
// Select model applied by default when the method is called without `options.selectModel`.
|
|
456
|
+
// Overrides the `defaultSelectModel` from setupVSRepo for this method only.
|
|
456
457
|
defaultSelect: "public",
|
|
457
458
|
},
|
|
458
459
|
remove: {
|
|
459
460
|
active: true,
|
|
460
461
|
defaultSelect: "minimal",
|
|
461
462
|
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// global (
|
|
463
|
+
// When `true`, ignores the `requiredWhere` configured in setupVSRepo for
|
|
464
|
+
// this specific method — useful when a method needs to "punch through" a
|
|
465
|
+
// global filter (e.g. multi-tenancy) in a specific case. Default = false.
|
|
465
466
|
ignoreRequiredWhere: false,
|
|
466
467
|
},
|
|
467
468
|
save: {
|
|
468
|
-
//
|
|
469
|
-
//
|
|
469
|
+
// Here only `ignoreRequiredWhere` is set — `active` and `defaultSelect`
|
|
470
|
+
// keep their defaults (true and the global `defaultSelectModel`).
|
|
470
471
|
ignoreRequiredWhere: true,
|
|
471
472
|
},
|
|
472
473
|
patch: {
|
|
473
|
-
//
|
|
474
|
+
// Only the select is overridden; the method stays active normally.
|
|
474
475
|
defaultSelect: "minimal",
|
|
475
476
|
},
|
|
476
477
|
has: {
|
|
477
|
-
active: false, //
|
|
478
|
+
active: false, // Disables 'has' (default = true) — the method disappears from the repository
|
|
478
479
|
},
|
|
479
480
|
softRemove: {
|
|
480
|
-
//
|
|
481
|
-
// `ignoreRequiredWhere`).
|
|
481
|
+
// Soft-delete methods follow the same options (`active`, `defaultSelect`,
|
|
482
|
+
// `ignoreRequiredWhere`). They're only available if `softRemovekName` is configured.
|
|
482
483
|
active: true,
|
|
483
484
|
defaultSelect: "minimal",
|
|
484
485
|
},
|
|
@@ -486,176 +487,176 @@ usuarioVSRepo.build(prisma, {
|
|
|
486
487
|
});
|
|
487
488
|
```
|
|
488
489
|
|
|
489
|
-
>
|
|
490
|
+
> 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`.
|
|
490
491
|
|
|
491
492
|
---
|
|
492
493
|
|
|
493
494
|
## Select Models
|
|
494
495
|
|
|
495
|
-
`selectModels`
|
|
496
|
+
`selectModels` defines named, reusable data projections.
|
|
496
497
|
|
|
497
498
|
```ts
|
|
498
499
|
selectModels: {
|
|
499
|
-
public: { id: true,
|
|
500
|
-
internal: { id: true,
|
|
500
|
+
public: { id: true, name: true, email: true },
|
|
501
|
+
internal: { id: true, name: true, email: true, password: true },
|
|
501
502
|
minimal: { id: true },
|
|
502
503
|
},
|
|
503
504
|
defaultSelectModel: "public",
|
|
504
505
|
```
|
|
505
506
|
|
|
506
|
-
`defaultSelectModel`
|
|
507
|
+
`defaultSelectModel` defines which select is used automatically when none is specified in the call. It's recommended to always define it together with `selectModels`.
|
|
507
508
|
|
|
508
|
-
**
|
|
509
|
+
**Using a specific select in the call:**
|
|
509
510
|
|
|
510
511
|
```ts
|
|
511
|
-
const
|
|
512
|
+
const user = await userRepository.get(id, { selectModel: "minimal" });
|
|
512
513
|
```
|
|
513
514
|
|
|
514
|
-
**
|
|
515
|
+
**Returning Prisma's default payload (without select):**
|
|
515
516
|
|
|
516
517
|
```ts
|
|
517
|
-
const
|
|
518
|
+
const fullUser = await userRepository.get(id, { selectModel: false });
|
|
518
519
|
```
|
|
519
520
|
|
|
520
521
|
---
|
|
521
522
|
|
|
522
523
|
## Include Models
|
|
523
524
|
|
|
524
|
-
`includeModels`
|
|
525
|
+
`includeModels` works similarly to `selectModels`, but instead of receiving a `select`, it receives a valid Prisma `include`.
|
|
525
526
|
|
|
526
527
|
```ts
|
|
527
|
-
const
|
|
528
|
-
tableName: "
|
|
528
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
529
|
+
tableName: "user",
|
|
529
530
|
pkName: "id",
|
|
530
531
|
selectModels: {
|
|
531
|
-
public: { id: true,
|
|
532
|
+
public: { id: true, name: true, email: true },
|
|
532
533
|
},
|
|
533
534
|
defaultSelectModel: "public",
|
|
534
535
|
includeModels: {
|
|
535
|
-
|
|
536
|
-
|
|
536
|
+
withPosts: { posts: true },
|
|
537
|
+
withPostsAndProfile: { posts: true, profile: true },
|
|
537
538
|
},
|
|
538
539
|
}).build(prisma);
|
|
539
540
|
```
|
|
540
541
|
|
|
541
|
-
**
|
|
542
|
+
**Using an `includeModel` in the call:**
|
|
542
543
|
|
|
543
544
|
```ts
|
|
544
|
-
const
|
|
545
|
+
const user = await userRepository.get(id, { includeModel: "withPosts" });
|
|
545
546
|
```
|
|
546
547
|
|
|
547
|
-
|
|
548
|
+
In this case, the default `select` (`selectModels`/`defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
|
|
548
549
|
|
|
549
|
-
###
|
|
550
|
+
### Differences from `selectModels`
|
|
550
551
|
|
|
551
|
-
- **
|
|
552
|
-
- **`includeModel`
|
|
552
|
+
- **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`.
|
|
553
|
+
- **`includeModel` and `selectModel` cannot be passed together** in the same call. If an `includeModel` is provided, any `selectModel` (including the default one) is ignored.
|
|
553
554
|
|
|
554
555
|
```ts
|
|
555
|
-
//
|
|
556
|
-
await
|
|
556
|
+
// CORRECT ✅ — includeModel only
|
|
557
|
+
await userRepository.get(id, { includeModel: "withPosts" });
|
|
557
558
|
|
|
558
|
-
//
|
|
559
|
-
await
|
|
559
|
+
// CORRECT ✅ — selectModel only
|
|
560
|
+
await userRepository.get(id, { selectModel: "public" });
|
|
560
561
|
|
|
561
|
-
//
|
|
562
|
-
await
|
|
562
|
+
// WRONG ❌ — combining both is not allowed
|
|
563
|
+
await userRepository.get(id, { selectModel: "public", includeModel: "withPosts" });
|
|
563
564
|
```
|
|
564
565
|
|
|
565
566
|
---
|
|
566
567
|
|
|
567
568
|
## Required Where
|
|
568
569
|
|
|
569
|
-
`requiredWhere`
|
|
570
|
+
`requiredWhere` defines filters that are automatically applied to every query on the repository.
|
|
570
571
|
|
|
571
572
|
```ts
|
|
572
|
-
requiredWhere: {
|
|
573
|
+
requiredWhere: { active: true },
|
|
573
574
|
```
|
|
574
575
|
|
|
575
|
-
|
|
576
|
+
Now every query will automatically include `active: true`:
|
|
576
577
|
|
|
577
578
|
```ts
|
|
578
|
-
//
|
|
579
|
-
const
|
|
579
|
+
// Internally: WHERE active = true
|
|
580
|
+
const users = await userRepository.findMany();
|
|
580
581
|
|
|
581
|
-
//
|
|
582
|
-
const
|
|
582
|
+
// Internally: WHERE email = 'john@email.com' AND active = true
|
|
583
|
+
const user = await userRepository.findByEmail("john@email.com");
|
|
583
584
|
```
|
|
584
585
|
|
|
585
|
-
|
|
586
|
+
Useful for manual soft-deletes, multi-tenancy, and global filters of any kind.
|
|
586
587
|
|
|
587
588
|
---
|
|
588
589
|
|
|
589
590
|
## Default Ordenation
|
|
590
591
|
|
|
591
|
-
`defaultOrdenation`
|
|
592
|
+
`defaultOrdenation` defines a default ordering that's automatically applied to every query that accepts `orderBy`, without needing to repeat the `order` argument on every call.
|
|
592
593
|
|
|
593
594
|
```ts
|
|
594
|
-
const
|
|
595
|
-
tableName: "
|
|
595
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
596
|
+
tableName: "user",
|
|
596
597
|
pkName: "id",
|
|
597
|
-
defaultOrdenation: {
|
|
598
|
+
defaultOrdenation: { createdAt: "desc" },
|
|
598
599
|
}).build(prisma);
|
|
599
600
|
```
|
|
600
601
|
|
|
601
|
-
|
|
602
|
+
With this, every listing query will already come ordered by `createdAt` descending:
|
|
602
603
|
|
|
603
604
|
```ts
|
|
604
|
-
//
|
|
605
|
-
const
|
|
605
|
+
// Internally: ORDER BY createdAt DESC
|
|
606
|
+
const users = await userRepository.getAll();
|
|
606
607
|
|
|
607
|
-
//
|
|
608
|
-
const
|
|
608
|
+
// Also applies to getAll with pagination
|
|
609
|
+
const paginated = await userRepository.getAll({ pagination: { take: 10 } });
|
|
609
610
|
```
|
|
610
611
|
|
|
611
|
-
|
|
612
|
+
**`defaultOrdenation` is ignored when:**
|
|
612
613
|
|
|
613
|
-
-
|
|
614
|
-
-
|
|
614
|
+
- The method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix — in these cases the `order` argument passed in the call takes priority.
|
|
615
|
+
- The dynamic method has `injectOrdenation` configured — the method's fixed ordering takes precedence.
|
|
615
616
|
|
|
616
617
|
```ts
|
|
617
618
|
methods: {
|
|
618
|
-
findManyPaginatedAndOrdered: { map: true }, //
|
|
619
|
-
|
|
619
|
+
findManyPaginatedAndOrdered: { map: true }, // order comes from the argument → defaultOrdenation ignored
|
|
620
|
+
findManyByActive: { map: true }, // no Ordered → defaultOrdenation applied
|
|
620
621
|
findManyByStatus: {
|
|
621
622
|
map: true,
|
|
622
|
-
injectOrdenation: {
|
|
623
|
+
injectOrdenation: { name: "asc" }, // injectOrdenation → defaultOrdenation ignored
|
|
623
624
|
},
|
|
624
625
|
}
|
|
625
626
|
```
|
|
626
627
|
|
|
627
|
-
> `defaultOrdenation`
|
|
628
|
+
> `defaultOrdenation` accepts the same type as Prisma's native `orderBy` for the model — including arrays of chained orderings.
|
|
628
629
|
|
|
629
630
|
---
|
|
630
631
|
|
|
631
|
-
##
|
|
632
|
+
## `see` option
|
|
632
633
|
|
|
633
|
-
|
|
634
|
+
When `softRemovekName` is configured, every method accepts the `see` option to control the visibility of soft-deleted records:
|
|
634
635
|
|
|
635
|
-
|
|
|
636
|
-
| ----------- |
|
|
637
|
-
| `"active"` |
|
|
638
|
-
| `"removed"` |
|
|
639
|
-
| `"all"` |
|
|
636
|
+
| Value | Behavior |
|
|
637
|
+
| ----------- | --------------------------------------------------------------|
|
|
638
|
+
| `"active"` | Returns only records that are **not** removed (default) |
|
|
639
|
+
| `"removed"` | Returns only removed records |
|
|
640
|
+
| `"all"` | Returns all records, regardless of status |
|
|
640
641
|
|
|
641
642
|
```ts
|
|
642
|
-
//
|
|
643
|
-
const
|
|
643
|
+
// Returns only active users (default)
|
|
644
|
+
const active = await userRepository.getAll();
|
|
644
645
|
|
|
645
|
-
//
|
|
646
|
-
const
|
|
646
|
+
// Returns only removed users
|
|
647
|
+
const removed = await userRepository.getAll({ see: "removed" });
|
|
647
648
|
|
|
648
|
-
//
|
|
649
|
-
const
|
|
649
|
+
// Returns all
|
|
650
|
+
const all = await userRepository.getAll({ see: "all" });
|
|
650
651
|
```
|
|
651
652
|
|
|
652
|
-
>
|
|
653
|
+
> The `see` option works independently of `requiredWhere` — it's applied on top of the soft-delete filter, not as a replacement for it.
|
|
653
654
|
|
|
654
655
|
---
|
|
655
656
|
|
|
656
|
-
##
|
|
657
|
+
## Dynamic methods
|
|
657
658
|
|
|
658
|
-
|
|
659
|
+
Dynamic methods are defined in the `methods` property and have their behavior inferred from their name.
|
|
659
660
|
|
|
660
661
|
```ts
|
|
661
662
|
methods: {
|
|
@@ -668,323 +669,358 @@ methods: {
|
|
|
668
669
|
|
|
669
670
|
---
|
|
670
671
|
|
|
671
|
-
###
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
|
676
|
-
|
|
|
677
|
-
| `findOneBy`
|
|
678
|
-
| `findBy`
|
|
679
|
-
| `findUniqueBy`
|
|
680
|
-
| `findUniqueOrThrowBy`
|
|
681
|
-
| `findFirstBy`
|
|
682
|
-
| `findFirstOrThrowBy`
|
|
683
|
-
| `findFirst`
|
|
684
|
-
| `findFirstOrThrow`
|
|
685
|
-
| `findManyBy`
|
|
686
|
-
| `findMany`
|
|
687
|
-
| `findOneWhere`
|
|
688
|
-
| `
|
|
689
|
-
| `
|
|
690
|
-
| `
|
|
691
|
-
| `
|
|
692
|
-
| `
|
|
693
|
-
| `
|
|
694
|
-
| `
|
|
695
|
-
| `
|
|
696
|
-
| `
|
|
697
|
-
| `
|
|
698
|
-
| `
|
|
699
|
-
| `
|
|
700
|
-
| `
|
|
701
|
-
| `
|
|
702
|
-
| `
|
|
703
|
-
| `
|
|
704
|
-
| `
|
|
705
|
-
| `
|
|
706
|
-
| `
|
|
707
|
-
| `
|
|
708
|
-
| `groupBy` | `groupBy` | `Dinâmico[]` | Nome deve ser exato; recebe args nativos do Prisma; ignora `selectModels`, `pushWhere` e `requiredWhere` |
|
|
672
|
+
### Available prefixes
|
|
673
|
+
|
|
674
|
+
The method name's prefix determines which Prisma operation will be called and which arguments are expected.
|
|
675
|
+
|
|
676
|
+
| Prefix | Prisma operation | Return | Notes |
|
|
677
|
+
| ---------------------------- | -------------------------- | ------------------------ | ---------------------------------------------------------------------------|
|
|
678
|
+
| `findOneBy` | `findFirst` | `T \| null` | Single return. |
|
|
679
|
+
| `findBy` | `findMany` / `findFirst` | `T[]` or `T \| null` | Default is list; use `fbMode: "one"` for a single return (**deprecated**, use `findOneBy`) |
|
|
680
|
+
| `findUniqueBy` | `findUnique` | `T \| null` | |
|
|
681
|
+
| `findUniqueOrThrowBy` | `findUniqueOrThrow` | `T` | Throws an error if not found |
|
|
682
|
+
| `findFirstBy` | `findFirst` | `T \| null` | Accepts fields as filter |
|
|
683
|
+
| `findFirstOrThrowBy` | `findFirstOrThrow` | `T` | Accepts fields as filter; throws an error if not found |
|
|
684
|
+
| `findFirst` | `findFirst` | `T \| null` | No field filters; applies only `requiredWhere` and `pushWhere` |
|
|
685
|
+
| `findFirstOrThrow` | `findFirstOrThrow` | `T` | No field filters; applies only `requiredWhere` and `pushWhere`; throws an error if not found |
|
|
686
|
+
| `findManyBy` | `findMany` | `T[]` | Accepts fields as filter |
|
|
687
|
+
| `findMany` | `findMany` | `T[]` | No field filters; applies only `requiredWhere` and `pushWhere` |
|
|
688
|
+
| `findOneWhere` | `findFirst` | `T \| null` | Receives an explicit `where` object as argument |
|
|
689
|
+
| `findListWhere` | `findMany` | `T[]` | Receives an explicit `where` object as argument |
|
|
690
|
+
| `existsBy` | `findFirst` | `boolean` | Returns `true` if found, `false` otherwise |
|
|
691
|
+
| `existsWhere` | `findFirst` | `boolean` | Receives an explicit `where` object and returns whether it exists |
|
|
692
|
+
| `countBy` | `count` | `number` | Accepts fields as filter |
|
|
693
|
+
| `countWhere` | `count` | `number` | Receives an explicit `where` object as argument |
|
|
694
|
+
| `count` | `count` | `number` | No field filters; applies only `requiredWhere` and `pushWhere` |
|
|
695
|
+
| `create` | `create` | `T` | Receives `data` as argument |
|
|
696
|
+
| `createMany` | `createMany` | `{ count: number }` | Receives `data` as argument; supports `SkipDuplicates` |
|
|
697
|
+
| `createManyAndReturn` | `createManyAndReturn` | `T[]` | Receives `data` as argument; supports `SkipDuplicates` |
|
|
698
|
+
| `updateBy` | `update` | `T` | Receives `data` as argument |
|
|
699
|
+
| `updateManyBy` | `updateMany` | `{ count: number }` | Receives `data` as argument |
|
|
700
|
+
| `updateManyWhere` | `updateMany` | `{ count: number }` | Receives a `where` object and a `data` object as arguments |
|
|
701
|
+
| `updateManyAndReturnBy` | `updateManyAndReturn` | `T[]` | Receives `data` as argument |
|
|
702
|
+
| `updateManyAndReturnWhere` | `updateManyAndReturn` | `T[]` | Receives a `where` object and a `data` object as arguments |
|
|
703
|
+
| `upsertBy` | `upsert` | `T` | Receives `update` and `create` as arguments |
|
|
704
|
+
| `deleteBy` | `delete` | `T` | |
|
|
705
|
+
| `deleteManyBy` | `deleteMany` | `{ count: number }` | |
|
|
706
|
+
| `deleteManyWhere` | `deleteMany` | `{ count: number }` | Receives an explicit `where` object as argument |
|
|
707
|
+
| `aggregate` | `aggregate` | `Dynamic` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
|
|
708
|
+
| `groupBy` | `groupBy` | `Dynamic[]` | Name must be exact; receives native Prisma args; ignores `selectModels`, `pushWhere`, and `requiredWhere` |
|
|
709
709
|
|
|
710
710
|
---
|
|
711
711
|
|
|
712
|
-
###
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
|
717
|
-
|
|
|
718
|
-
| *(
|
|
719
|
-
| `Not`
|
|
720
|
-
| `In`
|
|
721
|
-
| `NotIn`
|
|
722
|
-
| `Contains`
|
|
723
|
-
| `NotContains`
|
|
724
|
-
| `StartsWith`
|
|
725
|
-
| `NotStartsWith`
|
|
726
|
-
| `EndsWith`
|
|
727
|
-
| `NotEndsWith`
|
|
728
|
-
| `GreaterThan`
|
|
729
|
-
| `GreaterThanEqual`
|
|
730
|
-
| `LessThan`
|
|
731
|
-
| `LessThanEqual`
|
|
732
|
-
| `Between`
|
|
733
|
-
| `NotBetween`
|
|
734
|
-
| `IsNull`
|
|
735
|
-
| `IsNotNull`
|
|
736
|
-
| `IsTrue`
|
|
737
|
-
| `IsFalse`
|
|
738
|
-
| `Insensitive`
|
|
739
|
-
|
|
740
|
-
`Insensitive`
|
|
712
|
+
### Field filters
|
|
713
|
+
|
|
714
|
+
Filters are suffixes applied to the field name inside the method. The field itself comes capitalized right after the prefix (or after `By`).
|
|
715
|
+
|
|
716
|
+
| Suffix | Prisma operator | Argument required |
|
|
717
|
+
| -------------------- | ---------------------- | ---------------------------|
|
|
718
|
+
| *(no suffix)* | equality (`=`) | yes |
|
|
719
|
+
| `Not` | `not` | yes |
|
|
720
|
+
| `In` | `in` | yes (array) |
|
|
721
|
+
| `NotIn` | `notIn` | yes (array) |
|
|
722
|
+
| `Contains` | `contains` | yes |
|
|
723
|
+
| `NotContains` | `not.contains` | yes |
|
|
724
|
+
| `StartsWith` | `startsWith` | yes |
|
|
725
|
+
| `NotStartsWith` | `not.startsWith` | yes |
|
|
726
|
+
| `EndsWith` | `endsWith` | yes |
|
|
727
|
+
| `NotEndsWith` | `not.endsWith` | yes |
|
|
728
|
+
| `GreaterThan` | `gt` | yes |
|
|
729
|
+
| `GreaterThanEqual` | `gte` | yes |
|
|
730
|
+
| `LessThan` | `lt` | yes |
|
|
731
|
+
| `LessThanEqual` | `lte` | yes |
|
|
732
|
+
| `Between` | `gte` + `lte` | yes (tuple `[min, max]`) |
|
|
733
|
+
| `NotBetween` | `not.gte` + `not.lte` | yes (tuple `[min, max]`) |
|
|
734
|
+
| `IsNull` | `null` | no |
|
|
735
|
+
| `IsNotNull` | `not: null` | no |
|
|
736
|
+
| `IsTrue` | `true` | no |
|
|
737
|
+
| `IsFalse` | `false` | no |
|
|
738
|
+
| `Insensitive` | `mode: 'insensitive'` | combinator |
|
|
739
|
+
|
|
740
|
+
`Insensitive` is a combinator and can be used together with another text filter:
|
|
741
741
|
|
|
742
742
|
```ts
|
|
743
|
-
|
|
744
|
-
findByEmailStartsWithInsensitive // { email: { startsWith:
|
|
745
|
-
|
|
743
|
+
findByNameContainsInsensitive // { name: { contains: value, mode: 'insensitive' } }
|
|
744
|
+
findByEmailStartsWithInsensitive // { email: { startsWith: value, mode: 'insensitive' } }
|
|
745
|
+
findByNameInsensitive // { name: { equals: value, mode: 'insensitive' } }
|
|
746
746
|
```
|
|
747
747
|
|
|
748
|
-
`Between`
|
|
748
|
+
`Between` and `NotBetween` receive a **tuple `[minValue, maxValue]`**:
|
|
749
749
|
|
|
750
750
|
```ts
|
|
751
751
|
methods: {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
752
|
+
findManyByAgeBetween: { map: true },
|
|
753
|
+
findManyBySalaryNotBetween: { map: true },
|
|
754
|
+
findManyByCreatedAtBetween: { map: true },
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
-
await
|
|
758
|
-
await
|
|
759
|
-
await
|
|
757
|
+
await userRepository.findManyByAgeBetween([18, 65]);
|
|
758
|
+
await userRepository.findManyBySalaryNotBetween([1000, 5000]);
|
|
759
|
+
await userRepository.findManyByCreatedAtBetween([new Date("2024-01-01"), new Date("2024-12-31")]);
|
|
760
760
|
```
|
|
761
761
|
|
|
762
|
-
|
|
762
|
+
The `Optional` suffix can be added to any field to make the argument optional:
|
|
763
763
|
|
|
764
764
|
```ts
|
|
765
|
-
|
|
765
|
+
findByNameOptionalAndEmail // name is optional, email is required
|
|
766
766
|
```
|
|
767
767
|
|
|
768
768
|
---
|
|
769
769
|
|
|
770
|
-
###
|
|
770
|
+
### Logical operators
|
|
771
771
|
|
|
772
|
-
|
|
|
773
|
-
| --------- |
|
|
774
|
-
| `And` |
|
|
775
|
-
| `Or` |
|
|
776
|
-
| `AND` |
|
|
772
|
+
| Operator | Usage in the name | Example |
|
|
773
|
+
| --------- | ------------------------------ | -----------------------------------|
|
|
774
|
+
| `And` | between two fields | `findOneByIdAndEmail` |
|
|
775
|
+
| `Or` | between two fields | `findByNameOrEmail` |
|
|
776
|
+
| `AND` | separates a final `AND` block | `findByEmailOrNameANDActiveStatus` |
|
|
777
777
|
|
|
778
|
-
`AND` (
|
|
778
|
+
`AND` (in caps) has a specific rule:
|
|
779
779
|
|
|
780
|
-
-
|
|
781
|
-
-
|
|
782
|
-
-
|
|
780
|
+
- Only **one** `AND` can exist per method.
|
|
781
|
+
- All fields after `AND` are injected inside `AND: []`.
|
|
782
|
+
- After an `AND`, there can't be an `Or`.
|
|
783
783
|
|
|
784
|
-
|
|
784
|
+
Example:
|
|
785
785
|
|
|
786
786
|
```ts
|
|
787
787
|
methods: {
|
|
788
788
|
findOneByIdAndEmail: { map: true },
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
789
|
+
findByNameOrEmail: { map: true },
|
|
790
|
+
findFirstByIdOrEmailAndName: { map: true },
|
|
791
|
+
findByEmailOrNameANDActiveStatusAndAgeGreaterThan: { map: true }
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
await
|
|
795
|
-
await
|
|
796
|
-
await
|
|
797
|
-
await
|
|
794
|
+
await userRepository.findOneByIdAndEmail(1, "john@email.com");
|
|
795
|
+
await userRepository.findByNameOrEmail("John", "john@email.com");
|
|
796
|
+
await userRepository.findFirstByIdOrEmailAndName(1, "john@email.com", "John");
|
|
797
|
+
await userRepository.findByEmailOrNameANDActiveStatusAndAgeGreaterThan("john@email.com", "John", true, 17)
|
|
798
798
|
```
|
|
799
799
|
|
|
800
|
-
|
|
800
|
+
Generates (`findOneByIdAndEmail`):
|
|
801
801
|
|
|
802
802
|
```ts
|
|
803
803
|
{
|
|
804
804
|
id: 1,
|
|
805
|
-
email: "
|
|
805
|
+
email: "john@email.com"
|
|
806
806
|
}
|
|
807
807
|
```
|
|
808
808
|
|
|
809
|
-
|
|
809
|
+
Generates (`findByNameOrEmail`):
|
|
810
810
|
|
|
811
811
|
```ts
|
|
812
812
|
{
|
|
813
813
|
OR: [
|
|
814
|
-
{
|
|
815
|
-
{ email: "
|
|
814
|
+
{ name: "John" },
|
|
815
|
+
{ email: "john@email.com" }
|
|
816
816
|
]
|
|
817
817
|
}
|
|
818
818
|
```
|
|
819
819
|
|
|
820
|
-
|
|
820
|
+
Generates (`findFirstByIdOrEmailAndName`):
|
|
821
821
|
|
|
822
822
|
```ts
|
|
823
823
|
{
|
|
824
824
|
OR: [
|
|
825
825
|
{ id: 1 },
|
|
826
826
|
{
|
|
827
|
-
email: "
|
|
828
|
-
|
|
827
|
+
email: "john@email.com",
|
|
828
|
+
name: "John"
|
|
829
829
|
}
|
|
830
830
|
]
|
|
831
831
|
}
|
|
832
832
|
```
|
|
833
833
|
|
|
834
|
-
|
|
834
|
+
Generates (`findByEmailOrNameANDActiveStatusAndAgeGreaterThan`):
|
|
835
835
|
|
|
836
836
|
```ts
|
|
837
837
|
{
|
|
838
838
|
OR: [
|
|
839
|
-
{ email: "
|
|
840
|
-
{ name: "
|
|
839
|
+
{ email: "john@email.com" },
|
|
840
|
+
{ name: "John" }
|
|
841
841
|
],
|
|
842
842
|
AND: [
|
|
843
843
|
{ activeStatus: true },
|
|
844
|
-
{
|
|
844
|
+
{ age: { gt: 17 } }
|
|
845
845
|
]
|
|
846
846
|
}
|
|
847
847
|
```
|
|
848
848
|
|
|
849
849
|
---
|
|
850
850
|
|
|
851
|
-
###
|
|
851
|
+
### Relation filters
|
|
852
852
|
|
|
853
|
-
|
|
853
|
+
Allow filtering by fields of related models.
|
|
854
854
|
|
|
855
855
|
> [!IMPORTANT]
|
|
856
|
-
> - **
|
|
857
|
-
> - **
|
|
858
|
-
> -
|
|
859
|
-
> -
|
|
860
|
-
|
|
861
|
-
|
|
|
862
|
-
|
|
|
863
|
-
| `Some`
|
|
864
|
-
| `SomeField`
|
|
865
|
-
| `EveryField`
|
|
866
|
-
| `None`
|
|
867
|
-
| `NoneField`
|
|
868
|
-
| `With`
|
|
869
|
-
| `WithField`
|
|
870
|
-
| `Without`
|
|
871
|
-
| `WithoutField`
|
|
872
|
-
|
|
873
|
-
|
|
856
|
+
> - **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 } }>`).
|
|
857
|
+
> - **Suffix compatibility**:
|
|
858
|
+
> - The `Some`, `Every`, and `None` suffixes only work for **to-many** relations (`many-to-many` and `one-to-many`).
|
|
859
|
+
> - The `With` and `Without` suffixes only work for **to-one** relations (`one-to-one` and `many-to-one`).
|
|
860
|
+
|
|
861
|
+
| Relation suffix | Prisma operator | Note |
|
|
862
|
+
| ------------------------ | ----------------- | -------------------------------------------------------|
|
|
863
|
+
| `Some` | `some: {}` | Relation has *some* record |
|
|
864
|
+
| `SomeField` | `some.field` | Filters within the relation's records |
|
|
865
|
+
| `EveryField` | `every.field` | Filters within the relation's records |
|
|
866
|
+
| `None` | `none: {}` | Relation has *no* records |
|
|
867
|
+
| `NoneField` | `none.field` | Filters within the relation's records |
|
|
868
|
+
| `With` | `is: {}` | Relation exists (not null) |
|
|
869
|
+
| `WithField` | `is.field` | Filters a field within the relation |
|
|
870
|
+
| `Without` | `isNot: {}` | Relation doesn't exist (is null) |
|
|
871
|
+
| `WithoutField` | `isNot.field` | Filters a field within the relation with negation |
|
|
872
|
+
|
|
873
|
+
Considering `user` with a to-one relation `profile` and a to-many relation `posts`:
|
|
874
874
|
|
|
875
875
|
```ts
|
|
876
876
|
methods: {
|
|
877
|
-
// to-many (
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
// to-one (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
877
|
+
// to-many (posts)
|
|
878
|
+
findByPostsSome: { map: true }, // has at least one post
|
|
879
|
+
findByPostsSomeTitle: { map: true }, // has at least one post with that title
|
|
880
|
+
findByPostsEveryPublishedIsTrue:{ map: true }, // all posts are published
|
|
881
|
+
findByPostsNone: { map: true }, // has no posts
|
|
882
|
+
findByPostsNoneTitle: { map: true }, // no post has that title
|
|
883
|
+
|
|
884
|
+
// to-one (profile)
|
|
885
|
+
findByProfileWith: { map: true }, // has a profile (not null)
|
|
886
|
+
findByProfileWithBio: { map: true }, // has a profile with that bio
|
|
887
|
+
findByProfileWithout: { map: true }, // has no profile (is null)
|
|
888
|
+
findByProfileWithoutBio: { map: true }, // has a profile, but with a different bio than the one provided
|
|
889
889
|
}
|
|
890
890
|
|
|
891
|
-
await
|
|
892
|
-
await
|
|
893
|
-
await
|
|
894
|
-
await
|
|
895
|
-
await
|
|
891
|
+
await userRepository.findByPostsSome();
|
|
892
|
+
await userRepository.findByPostsSomeTitle("My first post");
|
|
893
|
+
await userRepository.findByPostsEveryPublishedIsTrue();
|
|
894
|
+
await userRepository.findByPostsNone();
|
|
895
|
+
await userRepository.findByPostsNoneTitle("Draft");
|
|
896
896
|
|
|
897
|
-
await
|
|
898
|
-
await
|
|
899
|
-
await
|
|
900
|
-
await
|
|
897
|
+
await userRepository.findByProfileWith();
|
|
898
|
+
await userRepository.findByProfileWithBio("Hello, world!");
|
|
899
|
+
await userRepository.findByProfileWithout();
|
|
900
|
+
await userRepository.findByProfileWithoutBio("Old bio");
|
|
901
901
|
```
|
|
902
902
|
|
|
903
|
-
|
|
903
|
+
Generates (`findByPostsSomeTitle`):
|
|
904
904
|
|
|
905
905
|
```ts
|
|
906
906
|
{
|
|
907
|
-
|
|
908
|
-
some: {
|
|
907
|
+
posts: {
|
|
908
|
+
some: { title: "My first post" }
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
```
|
|
912
912
|
|
|
913
|
-
|
|
913
|
+
Generates (`findByPostsEveryPublishedIsTrue`):
|
|
914
914
|
|
|
915
915
|
```ts
|
|
916
916
|
{
|
|
917
|
-
|
|
918
|
-
every: {
|
|
917
|
+
posts: {
|
|
918
|
+
every: { published: true }
|
|
919
919
|
}
|
|
920
920
|
}
|
|
921
921
|
```
|
|
922
922
|
|
|
923
|
-
|
|
923
|
+
Generates (`findByProfileWithBio`):
|
|
924
924
|
|
|
925
925
|
```ts
|
|
926
926
|
{
|
|
927
|
-
|
|
928
|
-
is: { bio: "
|
|
927
|
+
profile: {
|
|
928
|
+
is: { bio: "Hello, world!" }
|
|
929
929
|
}
|
|
930
930
|
}
|
|
931
931
|
```
|
|
932
932
|
|
|
933
|
-
|
|
933
|
+
Generates (`findByProfileWithout`):
|
|
934
934
|
|
|
935
935
|
```ts
|
|
936
936
|
{
|
|
937
|
-
|
|
937
|
+
profile: {
|
|
938
938
|
isNot: {}
|
|
939
939
|
}
|
|
940
940
|
}
|
|
941
941
|
```
|
|
942
942
|
|
|
943
|
-
> `Some`, `None`, `With
|
|
943
|
+
> `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.
|
|
944
944
|
|
|
945
945
|
---
|
|
946
946
|
|
|
947
|
-
###
|
|
947
|
+
### Pagination and ordering suffixes
|
|
948
948
|
|
|
949
|
-
|
|
949
|
+
Applied at the **end** of the method name, they automatically inject the pagination and ordering arguments.
|
|
950
950
|
|
|
951
|
-
|
|
|
952
|
-
|
|
|
953
|
-
| `Paginated`
|
|
954
|
-
| `Ordered`
|
|
955
|
-
| `OrderedAndPaginated`
|
|
956
|
-
| `PaginatedAndOrdered`
|
|
951
|
+
| Suffix | Additional arguments |
|
|
952
|
+
| ------------------------ | -------------------------------|
|
|
953
|
+
| `Paginated` | `(pagination)` |
|
|
954
|
+
| `Ordered` | `(order)` |
|
|
955
|
+
| `OrderedAndPaginated` | `(order, pagination)` |
|
|
956
|
+
| `PaginatedAndOrdered` | `(pagination, order)` |
|
|
957
957
|
|
|
958
|
-
|
|
958
|
+
For `createMany` and `createManyAndReturn`, the `SkipDuplicates` suffix is available:
|
|
959
959
|
|
|
960
|
-
|
|
|
961
|
-
|
|
|
962
|
-
| `SkipDuplicates`
|
|
960
|
+
| Suffix | Effect |
|
|
961
|
+
| --------------------- | ---------------------------------------------|
|
|
962
|
+
| `SkipDuplicates` | Skips duplicate records during insertion |
|
|
963
963
|
|
|
964
964
|
---
|
|
965
965
|
|
|
966
|
-
###
|
|
966
|
+
### Distinct
|
|
967
967
|
|
|
968
|
-
|
|
968
|
+
The `Distinct` suffix lets you get only unique records based on one or more fields, equivalent to Prisma's `distinct` option.
|
|
969
969
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
970
|
+
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.
|
|
971
|
+
|
|
972
|
+
```ts
|
|
973
|
+
methods: {
|
|
974
|
+
// Returns unique users combining "age" and "role" (no field filter)
|
|
975
|
+
findManyDistinctAgeAndRole: { map: true },
|
|
976
|
+
|
|
977
|
+
// Distinct combined with the Paginated suffix
|
|
978
|
+
findManyDistinctNamePaginated: { map: true },
|
|
979
|
+
|
|
980
|
+
// Distinct combined with a field filter (name) — filters by name and then applies distinct on role
|
|
981
|
+
findManyByNameDistinctRole: { map: true },
|
|
982
|
+
},
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
```ts
|
|
986
|
+
// No arguments: the distinct fields are already fixed in the method name
|
|
987
|
+
await userRepository.findManyDistinctAgeAndRole();
|
|
988
|
+
|
|
989
|
+
// The pagination argument still works normally
|
|
990
|
+
await userRepository.findManyDistinctNamePaginated({ take: 10, skip: 0 });
|
|
991
|
+
|
|
992
|
+
// The "name" field filter is still passed normally as an argument
|
|
993
|
+
await userRepository.findManyByNameDistinctRole("John");
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
> The fields specified after `Distinct` are resolved from the method name at build time — they **don't** become runtime arguments, unlike regular field filters.
|
|
997
|
+
|
|
998
|
+
`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`.
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
### Method configuration
|
|
1003
|
+
|
|
1004
|
+
Each entry in `methods` accepts the following options:
|
|
1005
|
+
|
|
1006
|
+
| Option | Type | Default | Description |
|
|
1007
|
+
| --------------------- | --------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------|
|
|
1008
|
+
| `map` | `boolean` | — | **Required.** Defines whether the method will be exposed on the repository. |
|
|
1009
|
+
| `whereType` | `'extending'` \| `'overwrite'` | `extending` | `extending` combines with `requiredWhere`. `overwrite` ignores `requiredWhere`. |
|
|
1010
|
+
| `selectModel` | `keyof SelectModels \| false` | — | Overrides `defaultSelectModel` for this method. |
|
|
1011
|
+
| `fbMode` | `'one'` \| `'list'` | `'list'` | (**Deprecated. Use `findOneBy`**) Only for `findBy`. `'one'` returns `T \| null`; `'list'` returns `T[]`. |
|
|
1012
|
+
| `proxyTo` | `Valid method pattern` | — | Delegates the logic to another valid method pattern. |
|
|
1013
|
+
| `pushWhere` | `WhereModel<M>` | — | Extra `where` added to the query in addition to `requiredWhere`. |
|
|
1014
|
+
| `injectOrdenation` | `OrdenationModel<M>` | — | Fixed ordering automatically injected into the query. |
|
|
1015
|
+
| `injectPagination` | `PaginationModel<M>` | — | Fixed pagination automatically injected into the query. |
|
|
980
1016
|
|
|
981
1017
|
---
|
|
982
1018
|
|
|
983
|
-
### Aggregate
|
|
1019
|
+
### Aggregate and GroupBy
|
|
984
1020
|
|
|
985
1021
|
```ts
|
|
986
|
-
const
|
|
987
|
-
tableName: "
|
|
1022
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
1023
|
+
tableName: "user",
|
|
988
1024
|
pkName: "id",
|
|
989
1025
|
methods: {
|
|
990
1026
|
aggregate: { map: true },
|
|
@@ -994,33 +1030,33 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
|
|
|
994
1030
|
```
|
|
995
1031
|
|
|
996
1032
|
> [!NOTE]
|
|
997
|
-
>
|
|
998
|
-
>
|
|
1033
|
+
> These methods must have exactly these names (`aggregate` and `groupBy`).
|
|
1034
|
+
> Unlike the other dynamic methods, they receive native Prisma arguments and **ignore** the `selectModels`, `pushWhere`, and `requiredWhere` configurations.
|
|
999
1035
|
|
|
1000
1036
|
---
|
|
1001
1037
|
|
|
1002
|
-
##
|
|
1038
|
+
## Relations in save
|
|
1003
1039
|
|
|
1004
|
-
Configure
|
|
1040
|
+
Configure relations so that `save` and `patch` manage them automatically (`saveList` and `patchList` also manage relations automatically).
|
|
1005
1041
|
|
|
1006
1042
|
```ts
|
|
1007
1043
|
import type { Prisma } from "../../generated/prisma/client";
|
|
1008
1044
|
|
|
1009
|
-
type
|
|
1010
|
-
include: {
|
|
1045
|
+
type User = Prisma.userGetPayload<{
|
|
1046
|
+
include: { profile: true; posts: true };
|
|
1011
1047
|
}>;
|
|
1012
1048
|
|
|
1013
|
-
const
|
|
1014
|
-
tableName: "
|
|
1049
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
1050
|
+
tableName: "user",
|
|
1015
1051
|
pkName: "id",
|
|
1016
1052
|
|
|
1017
1053
|
relations: {
|
|
1018
|
-
|
|
1054
|
+
profile: {
|
|
1019
1055
|
pk: "id",
|
|
1020
1056
|
mode: "oto",
|
|
1021
1057
|
restriction: "set",
|
|
1022
1058
|
},
|
|
1023
|
-
|
|
1059
|
+
posts: {
|
|
1024
1060
|
pk: "id",
|
|
1025
1061
|
mode: "otm",
|
|
1026
1062
|
restriction: "add",
|
|
@@ -1029,80 +1065,80 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
|
|
|
1029
1065
|
}).build(prisma);
|
|
1030
1066
|
```
|
|
1031
1067
|
|
|
1032
|
-
**
|
|
1068
|
+
**Relation modes:**
|
|
1033
1069
|
|
|
1034
|
-
|
|
|
1035
|
-
| ----- |
|
|
1036
|
-
| `oto` | one-to-one
|
|
1037
|
-
| `otm` | one-to-many
|
|
1038
|
-
| `mto` | many-to-one
|
|
1039
|
-
| `mtm` | many-to-many
|
|
1070
|
+
| Mode | Relation |
|
|
1071
|
+
| ----- | -------------- |
|
|
1072
|
+
| `oto` | one-to-one |
|
|
1073
|
+
| `otm` | one-to-many |
|
|
1074
|
+
| `mto` | many-to-one |
|
|
1075
|
+
| `mtm` | many-to-many |
|
|
1040
1076
|
|
|
1041
|
-
**
|
|
1077
|
+
**Restrictions:**
|
|
1042
1078
|
|
|
1043
|
-
|
|
|
1044
|
-
|
|
|
1045
|
-
| `set`
|
|
1046
|
-
| `add`
|
|
1079
|
+
| Restriction | Behavior on update |
|
|
1080
|
+
| ------------ | ----------------------------------------------------------------|
|
|
1081
|
+
| `set` | Fully replaces (removes the ones that weren't sent) |
|
|
1082
|
+
| `add` | Adds/updates without removing existing ones |
|
|
1047
1083
|
|
|
1048
1084
|
> [!WARNING]
|
|
1049
|
-
> **`set`
|
|
1085
|
+
> **`set` means different things depending on the relation's `mode` — and this can cause data loss if you're not careful.**
|
|
1050
1086
|
>
|
|
1051
|
-
>
|
|
1087
|
+
> 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).
|
|
1052
1088
|
>
|
|
1053
|
-
> |
|
|
1054
|
-
> | ----- |
|
|
1055
|
-
> | `oto` |
|
|
1056
|
-
> | `otm` |
|
|
1057
|
-
> | `mto` |
|
|
1058
|
-
> | `mtm` |
|
|
1089
|
+
> | Mode | `restriction: "set"` when an item is omitted | Does the item continue to exist in the database? |
|
|
1090
|
+
> | ----- | ------------------------------------------------ | -----------------------------------------------------|
|
|
1091
|
+
> | `oto` | Passing `null` in the field → **deletes** the related record (`delete: true`) | No |
|
|
1092
|
+
> | `otm` | Items outside the sent list → **deleted** (`deleteMany` with `notIn`) | No |
|
|
1093
|
+
> | `mto` | Passing `null` in the field (with `nullable: true`) → **unlinks** (`disconnect: true`) | Yes |
|
|
1094
|
+
> | `mtm` | Items outside the sent list → **unlinked** from the join table (`set: []`) | Yes |
|
|
1059
1095
|
>
|
|
1060
|
-
>
|
|
1096
|
+
> 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.
|
|
1061
1097
|
|
|
1062
|
-
|
|
1098
|
+
**`mto` relation with nullable:**
|
|
1063
1099
|
|
|
1064
|
-
Use `nullable` (
|
|
1100
|
+
Use `nullable` (lowercase) to allow unlinking a many-to-one relation:
|
|
1065
1101
|
|
|
1066
1102
|
```ts
|
|
1067
1103
|
relations: {
|
|
1068
|
-
|
|
1104
|
+
category: {
|
|
1069
1105
|
pk: "id",
|
|
1070
1106
|
mode: "mto",
|
|
1071
1107
|
restriction: "set",
|
|
1072
|
-
nullable: true, //
|
|
1108
|
+
nullable: true, // allows passing null to unlink
|
|
1073
1109
|
},
|
|
1074
1110
|
}
|
|
1075
1111
|
```
|
|
1076
1112
|
|
|
1077
1113
|
---
|
|
1078
1114
|
|
|
1079
|
-
##
|
|
1115
|
+
## Transactions
|
|
1080
1116
|
|
|
1081
|
-
|
|
1117
|
+
All methods accept `options.db` to participate in a transaction:
|
|
1082
1118
|
|
|
1083
1119
|
```ts
|
|
1084
|
-
await
|
|
1085
|
-
const
|
|
1086
|
-
{
|
|
1120
|
+
await userRepository.prisma.$transaction(async (tx) => {
|
|
1121
|
+
const user = await userRepository.save(
|
|
1122
|
+
{ name: "Mary", email: "mary@email.com", password: "password" },
|
|
1087
1123
|
{ db: tx }
|
|
1088
1124
|
);
|
|
1089
1125
|
|
|
1090
|
-
await
|
|
1091
|
-
{
|
|
1126
|
+
await userLogsRepository.save(
|
|
1127
|
+
{ action: "User registration", data: { registeredUser: user.id } },
|
|
1092
1128
|
{ db: tx }
|
|
1093
1129
|
);
|
|
1094
1130
|
});
|
|
1095
1131
|
```
|
|
1096
1132
|
|
|
1097
|
-
|
|
1133
|
+
For `saveList` and `patchList`, the `db` field must be a `DbTransaction`:
|
|
1098
1134
|
|
|
1099
1135
|
```ts
|
|
1100
1136
|
await prisma.$transaction(async (tx) => {
|
|
1101
|
-
//
|
|
1102
|
-
const
|
|
1137
|
+
// CORRECT: tx is a DbTransaction
|
|
1138
|
+
const registeredUsers = await userRepository.saveList([{ name: "Mary" }, { name: "Lucas" }], { db: tx });
|
|
1103
1139
|
|
|
1104
|
-
await
|
|
1105
|
-
{
|
|
1140
|
+
await userLogsRepository.save(
|
|
1141
|
+
{ action: "User registration", data: { registeredUsers: registeredUsers.map(u => u.id) } },
|
|
1106
1142
|
{ db: tx }
|
|
1107
1143
|
);
|
|
1108
1144
|
});
|
|
@@ -1110,11 +1146,11 @@ await prisma.$transaction(async (tx) => {
|
|
|
1110
1146
|
|
|
1111
1147
|
---
|
|
1112
1148
|
|
|
1113
|
-
##
|
|
1149
|
+
## Extending a repository
|
|
1114
1150
|
|
|
1115
1151
|
```ts
|
|
1116
|
-
const
|
|
1117
|
-
tableName: "
|
|
1152
|
+
const userRepository = setupVSRepo<User, "user">()(({
|
|
1153
|
+
tableName: "user",
|
|
1118
1154
|
pkName: "id",
|
|
1119
1155
|
methods: {
|
|
1120
1156
|
findOneByEmailEndsWith: { map: true },
|
|
@@ -1122,54 +1158,54 @@ const usuarioRepository = setupVSRepo<Usuario, "usuario">()(({
|
|
|
1122
1158
|
})
|
|
1123
1159
|
.build(prisma)
|
|
1124
1160
|
.extend((repo) => ({
|
|
1125
|
-
|
|
1126
|
-
return repo.findOneByEmailEndsWith(`@${
|
|
1161
|
+
findActiveByDomain: async (domain: string) => {
|
|
1162
|
+
return repo.findOneByEmailEndsWith(`@${domain}`);
|
|
1127
1163
|
},
|
|
1128
1164
|
|
|
1129
|
-
|
|
1130
|
-
return repo.patchList(ids.map(id => [id, {
|
|
1165
|
+
activateMultiple: async (ids: string[]) => {
|
|
1166
|
+
return repo.patchList(ids.map(id => [id, { active: true }]));
|
|
1131
1167
|
},
|
|
1132
1168
|
}));
|
|
1133
1169
|
```
|
|
1134
1170
|
|
|
1135
1171
|
---
|
|
1136
1172
|
|
|
1137
|
-
##
|
|
1173
|
+
## Error handling
|
|
1138
1174
|
|
|
1139
|
-
|
|
1175
|
+
VSRepository throws `VSRepoError` and its subclasses in specific situations (Prisma errors are not overridden):
|
|
1140
1176
|
|
|
1141
1177
|
```ts
|
|
1142
1178
|
import { VSRepoError, VSRepoRuntimeError } from "../../generated/vsrepo";
|
|
1143
1179
|
|
|
1144
1180
|
try {
|
|
1145
|
-
const
|
|
1181
|
+
const user = await userRepository.getOrThrow("id-that-does-not-exist");
|
|
1146
1182
|
} catch (error) {
|
|
1147
1183
|
if (error instanceof VSRepoRuntimeError && error.code === "20727") {
|
|
1148
|
-
console.error("
|
|
1184
|
+
console.error("Record not found");
|
|
1149
1185
|
} else if (error instanceof VSRepoError) {
|
|
1150
|
-
console.error("
|
|
1186
|
+
console.error("Repository error:", error.message);
|
|
1151
1187
|
} else {
|
|
1152
|
-
console.error("
|
|
1188
|
+
console.error("Error:", error.message)
|
|
1153
1189
|
}
|
|
1154
1190
|
}
|
|
1155
1191
|
```
|
|
1156
1192
|
|
|
1157
|
-
**
|
|
1193
|
+
**Available subclasses:**
|
|
1158
1194
|
|
|
1159
|
-
|
|
|
1160
|
-
|
|
|
1161
|
-
| `VSRepoConfigError`
|
|
1162
|
-
| `VSRepoBuildError`
|
|
1163
|
-
| `VSRepoExtendError`
|
|
1164
|
-
| `VSRepoRuntimeError`
|
|
1195
|
+
| Class | When it's thrown |
|
|
1196
|
+
| ---------------------- | ------------------------------------------------------------------------|
|
|
1197
|
+
| `VSRepoConfigError` | Invalid configuration in `setupVSRepo` |
|
|
1198
|
+
| `VSRepoBuildError` | Invalid method name, field type, or configuration in `build` |
|
|
1199
|
+
| `VSRepoExtendError` | Invalid argument in `extend` |
|
|
1200
|
+
| `VSRepoRuntimeError` | Runtime error during an operation |
|
|
1165
1201
|
|
|
1166
|
-
`VSRepoRuntimeError`
|
|
1202
|
+
`VSRepoRuntimeError` has a `code` property for programmatic identification. Code `"20727"` is thrown by `getOrThrow` when the record is not found, for example.
|
|
1167
1203
|
|
|
1168
1204
|
---
|
|
1169
1205
|
|
|
1170
|
-
##
|
|
1206
|
+
## Utility types
|
|
1171
1207
|
|
|
1172
|
-
###
|
|
1208
|
+
### Client types
|
|
1173
1209
|
|
|
1174
1210
|
```ts
|
|
1175
1211
|
import type { DbClient, DbTransaction, ClientOrTransaction } from "../../generated/vsrepo";
|
|
@@ -1179,7 +1215,7 @@ type DbTransaction = Prisma.TransactionClient;
|
|
|
1179
1215
|
type ClientOrTransaction = DbClient | DbTransaction;
|
|
1180
1216
|
```
|
|
1181
1217
|
|
|
1182
|
-
###
|
|
1218
|
+
### Soft-delete visibility type
|
|
1183
1219
|
|
|
1184
1220
|
```ts
|
|
1185
1221
|
import type { SeeMode } from "../../generated/vsrepo";
|
|
@@ -1187,7 +1223,7 @@ import type { SeeMode } from "../../generated/vsrepo";
|
|
|
1187
1223
|
type SeeMode = "active" | "removed" | "all";
|
|
1188
1224
|
```
|
|
1189
1225
|
|
|
1190
|
-
###
|
|
1226
|
+
### Types derived from the Prisma model
|
|
1191
1227
|
|
|
1192
1228
|
```ts
|
|
1193
1229
|
import type {
|
|
@@ -1203,22 +1239,22 @@ import type {
|
|
|
1203
1239
|
} from "../../generated/vsrepo";
|
|
1204
1240
|
```
|
|
1205
1241
|
|
|
1206
|
-
###
|
|
1242
|
+
### Method options types
|
|
1207
1243
|
|
|
1208
1244
|
```ts
|
|
1209
1245
|
import type { MethodOptions, MethodOptionsModel } from "../../generated/vsrepo";
|
|
1210
1246
|
|
|
1211
|
-
// MethodOptions<S, IM> —
|
|
1212
|
-
type Opts = MethodOptions<"public" | "minimal", "
|
|
1247
|
+
// MethodOptions<S, IM> — options passed into the repository's methods
|
|
1248
|
+
type Opts = MethodOptions<"public" | "minimal", "withPosts">;
|
|
1213
1249
|
|
|
1214
|
-
// MethodOptionsModel<TRepo> —
|
|
1215
|
-
const
|
|
1216
|
-
type OptsModel = MethodOptionsModel<typeof
|
|
1250
|
+
// MethodOptionsModel<TRepo> — derived from a configured VSRepository instance
|
|
1251
|
+
const userVSRepo = setupVSRepo<User, "user">()(config);
|
|
1252
|
+
type OptsModel = MethodOptionsModel<typeof userVSRepo>;
|
|
1217
1253
|
```
|
|
1218
1254
|
|
|
1219
|
-
>
|
|
1255
|
+
> 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.
|
|
1220
1256
|
|
|
1221
|
-
###
|
|
1257
|
+
### Configuration types
|
|
1222
1258
|
|
|
1223
1259
|
```ts
|
|
1224
1260
|
import type {
|
|
@@ -1230,36 +1266,36 @@ import type {
|
|
|
1230
1266
|
} from "../../generated/vsrepo";
|
|
1231
1267
|
```
|
|
1232
1268
|
|
|
1233
|
-
###
|
|
1269
|
+
### Built repository type
|
|
1234
1270
|
|
|
1235
1271
|
```ts
|
|
1236
1272
|
import type { RepositoryOf } from "../../generated/vsrepo";
|
|
1237
1273
|
|
|
1238
|
-
const
|
|
1239
|
-
type
|
|
1274
|
+
const userVSRepo = setupVSRepo<User, "user">()({ ... });
|
|
1275
|
+
type UserRepository = RepositoryOf<typeof userVSRepo>;
|
|
1240
1276
|
```
|
|
1241
1277
|
|
|
1242
|
-
`RepositoryOf`
|
|
1278
|
+
`RepositoryOf` accepts three parameters:
|
|
1243
1279
|
|
|
1244
1280
|
```ts
|
|
1245
1281
|
type RepositoryOf<TRepo, C extends BuildConfig | undefined = undefined, E = unknown>
|
|
1246
1282
|
```
|
|
1247
1283
|
|
|
1248
|
-
###
|
|
1284
|
+
### `save` and `patch` payload types
|
|
1249
1285
|
|
|
1250
1286
|
```ts
|
|
1251
1287
|
import type { SaveObject, PatchObject } from "../../generated/vsrepo";
|
|
1252
1288
|
|
|
1253
|
-
const
|
|
1254
|
-
tableName: "
|
|
1289
|
+
const userVSRepo = setupVSRepo<User, "user">()(({
|
|
1290
|
+
tableName: "user",
|
|
1255
1291
|
pkName: "id",
|
|
1256
1292
|
relations: {
|
|
1257
|
-
|
|
1293
|
+
profile: { pk: "id", mode: "oto", restriction: "set" },
|
|
1258
1294
|
},
|
|
1259
1295
|
});
|
|
1260
1296
|
|
|
1261
|
-
type
|
|
1262
|
-
type
|
|
1297
|
+
type UserSavePayload = SaveObject<Prisma.UserCreateInput, typeof userVSRepo>;
|
|
1298
|
+
type UserPatchPayload = PatchObject<Prisma.UserUpdateInput, typeof userVSRepo>;
|
|
1263
1299
|
```
|
|
1264
1300
|
|
|
1265
1301
|
---
|
|
@@ -1270,16 +1306,16 @@ type UsuarioPatchPayload = PatchObject<Prisma.UsuarioUpdateInput, typeof usuario
|
|
|
1270
1306
|
|
|
1271
1307
|
```ts
|
|
1272
1308
|
setupVSRepo<TPayload, TTableName>()({
|
|
1273
|
-
tableName: Uncapitalize<M>; //
|
|
1274
|
-
pkName: keyof T; //
|
|
1275
|
-
softRemovekName?: keyof T & string; //
|
|
1276
|
-
selectModels?: SelectModels<M>; //
|
|
1277
|
-
defaultSelectModel?: keyof SM; // Select
|
|
1278
|
-
includeModels?: IncludeModels<M>; //
|
|
1279
|
-
requiredWhere?: WhereModel<M>; //
|
|
1280
|
-
defaultOrdenation?: OrdenationModel<M>; //
|
|
1281
|
-
relations?: RepositoryRelations<T>; //
|
|
1282
|
-
methods?: Record<string, MethodConfig<M, SM>>; //
|
|
1309
|
+
tableName: Uncapitalize<M>; // Table name in Prisma
|
|
1310
|
+
pkName: keyof T; // Primary key name
|
|
1311
|
+
softRemovekName?: keyof T & string; // DateTime field for soft-delete
|
|
1312
|
+
selectModels?: SelectModels<M>; // Named data projections (select)
|
|
1313
|
+
defaultSelectModel?: keyof SM; // Select applied by default
|
|
1314
|
+
includeModels?: IncludeModels<M>; // Named data projections (include) — no default, only in the call
|
|
1315
|
+
requiredWhere?: WhereModel<M>; // Always-applied filters
|
|
1316
|
+
defaultOrdenation?: OrdenationModel<M>; // Default ordering for queries without Ordered/injectOrdenation
|
|
1317
|
+
relations?: RepositoryRelations<T>; // Relation configuration
|
|
1318
|
+
methods?: Record<string, MethodConfig<M, SM>>; // Dynamic methods
|
|
1283
1319
|
});
|
|
1284
1320
|
```
|
|
1285
1321
|
|
|
@@ -1287,10 +1323,10 @@ setupVSRepo<TPayload, TTableName>()({
|
|
|
1287
1323
|
|
|
1288
1324
|
```ts
|
|
1289
1325
|
vsRepo.build(prisma, {
|
|
1290
|
-
showWorking?: boolean; //
|
|
1326
|
+
showWorking?: boolean; // Shows internal logs on the console (default = false)
|
|
1291
1327
|
|
|
1292
1328
|
baseMethods?: {
|
|
1293
|
-
//
|
|
1329
|
+
// Methods that can use a defaultSelect
|
|
1294
1330
|
get?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1295
1331
|
getOrThrow?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1296
1332
|
getList?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
@@ -1304,7 +1340,7 @@ vsRepo.build(prisma, {
|
|
|
1304
1340
|
softRemove?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1305
1341
|
restore?: { active?: boolean; defaultSelect?: string; ignoreRequiredWhere?: boolean };
|
|
1306
1342
|
|
|
1307
|
-
//
|
|
1343
|
+
// Methods that do NOT accept defaultSelect
|
|
1308
1344
|
removeList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1309
1345
|
softRemoveList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
1310
1346
|
restoreList?: { active?: boolean; ignoreRequiredWhere?: boolean };
|
|
@@ -1318,55 +1354,55 @@ vsRepo.build(prisma, {
|
|
|
1318
1354
|
|
|
1319
1355
|
```ts
|
|
1320
1356
|
repo.extend((repo) => ({
|
|
1321
|
-
|
|
1357
|
+
myMethod: () => { ... }
|
|
1322
1358
|
}));
|
|
1323
1359
|
```
|
|
1324
1360
|
|
|
1325
1361
|
---
|
|
1326
1362
|
|
|
1327
|
-
##
|
|
1363
|
+
## Practical examples
|
|
1328
1364
|
|
|
1329
|
-
|
|
1365
|
+
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.
|
|
1330
1366
|
|
|
1331
1367
|
```
|
|
1332
1368
|
examples/
|
|
1333
|
-
├── prisma.ts #
|
|
1334
|
-
├── repositories.ts #
|
|
1369
|
+
├── prisma.ts # PrismaClient instance used by the examples
|
|
1370
|
+
├── repositories.ts # Repository configuration (User, Address, Product) with setupVSRepo
|
|
1335
1371
|
└── tests/
|
|
1336
|
-
├── base-methods.test.ts #
|
|
1337
|
-
├── relations.test.ts #
|
|
1338
|
-
├── required-where.test.ts #
|
|
1339
|
-
├── dynamic-methods.test.ts #
|
|
1340
|
-
├── transactions.test.ts # Transactions
|
|
1341
|
-
├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList
|
|
1342
|
-
└── batch-methods.test.ts #
|
|
1372
|
+
├── base-methods.test.ts # Base methods: get, save, patch, remove, getAll, total, has...
|
|
1373
|
+
├── relations.test.ts # How to configure and use relations in save/patch and in filters
|
|
1374
|
+
├── required-where.test.ts # How requiredWhere is automatically applied to queries
|
|
1375
|
+
├── dynamic-methods.test.ts # Prefixes, field filters, logical operators, and pagination/ordering
|
|
1376
|
+
├── transactions.test.ts # Transactions with options.db and instance access via repository.prisma
|
|
1377
|
+
├── soft-delete.test.ts # Soft-delete: softRemove, softRemoveList, restore, restoreList and SeeMode
|
|
1378
|
+
└── batch-methods.test.ts # Batch operations: getList, saveList, patchList and merge
|
|
1343
1379
|
```
|
|
1344
1380
|
|
|
1345
|
-
|
|
1381
|
+
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.
|
|
1346
1382
|
|
|
1347
1383
|
---
|
|
1348
1384
|
|
|
1349
|
-
##
|
|
1385
|
+
## Contributing
|
|
1350
1386
|
|
|
1351
|
-
|
|
1387
|
+
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)**):
|
|
1352
1388
|
|
|
1353
|
-
1.
|
|
1354
|
-
2.
|
|
1355
|
-
3.
|
|
1356
|
-
4.
|
|
1389
|
+
1. **Fork** the project.
|
|
1390
|
+
2. Create a new branch with your change: `git checkout -b fixing-bug`.
|
|
1391
|
+
3. Push to your branch: `git push origin fixing-bug`.
|
|
1392
|
+
4. Open a **Pull Request**.
|
|
1357
1393
|
|
|
1358
|
-
|
|
1394
|
+
To report issues or suggest new features, open an **Issue**.
|
|
1359
1395
|
|
|
1360
1396
|
---
|
|
1361
1397
|
|
|
1362
|
-
##
|
|
1398
|
+
## Requirements
|
|
1363
1399
|
|
|
1364
1400
|
- Node.js 18+ (ESM)
|
|
1365
1401
|
- Prisma
|
|
1366
|
-
- TypeScript (
|
|
1367
|
-
- `"moduleResolution": "bundler"`
|
|
1402
|
+
- TypeScript (optional, but strongly recommended)
|
|
1403
|
+
- `"moduleResolution": "bundler"` or `"nodenext"` in tsconfig
|
|
1368
1404
|
|
|
1369
|
-
`tsconfig.json
|
|
1405
|
+
Recommended `tsconfig.json`:
|
|
1370
1406
|
|
|
1371
1407
|
```json
|
|
1372
1408
|
{
|
|
@@ -1385,20 +1421,22 @@ Para reportar problemas ou sugerir novas funcionalidades, abra uma **Issue**.
|
|
|
1385
1421
|
|
|
1386
1422
|
## Troubleshooting
|
|
1387
1423
|
|
|
1388
|
-
**
|
|
1424
|
+
**Generic types not inferred** — Check that `strict: true` and `moduleResolution: "bundler"` or `"nodenext"` are set in `tsconfig.json`.
|
|
1425
|
+
|
|
1426
|
+
**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.
|
|
1389
1427
|
|
|
1390
|
-
|
|
1428
|
+
**`proxyTo` required** — Names outside the standard patterns (e.g. `searchByEmail`) aren't parsed directly. Use `proxyTo: "findByEmail"` in these cases.
|
|
1391
1429
|
|
|
1392
|
-
|
|
1430
|
+
**Select model returns unexpected fields** — Check that the select model defines exactly the fields your TypeScript type expects.
|
|
1393
1431
|
|
|
1394
|
-
|
|
1432
|
+
**`selectModel` and `includeModel` together in the same call** — Not allowed. Choose one or the other: if `includeModel` is provided, the `select` (including `defaultSelectModel`) is ignored and only the `include` is sent to Prisma.
|
|
1395
1433
|
|
|
1396
|
-
**`
|
|
1434
|
+
**`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`.
|
|
1397
1435
|
|
|
1398
|
-
**`
|
|
1436
|
+
**`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.
|
|
1399
1437
|
|
|
1400
|
-
**`
|
|
1438
|
+
**`defaultOrdenation` isn't being applied** — Check whether the method uses the `Ordered`, `OrderedAndPaginated`, or `PaginatedAndOrdered` suffix, and whether it has `injectOrdenation` configured. Both take priority over the default ordering.
|
|
1401
1439
|
|
|
1402
|
-
**`
|
|
1440
|
+
**`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.
|
|
1403
1441
|
|
|
1404
|
-
**`saveList`/`patchList`
|
|
1442
|
+
**`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.
|