vsrepo 1.3.0 → 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 +714 -480
- package/dist/VSRepository.d.ts +36 -19
- 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
|
@@ -1,66 +1,70 @@
|
|
|
1
1
|
# VSRepository
|
|
2
2
|
|
|
3
3
|

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