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