@vsrepo/prisma7-adapter 1.3.2 → 2.0.0-dev.1
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/CHANGELOG.md +535 -0
- package/LICENSE +20 -20
- package/README.md +394 -299
- package/README.pt-BR.md +394 -299
- package/dist/constants/provider-capabilities.constant.d.ts +17 -0
- package/dist/constants/provider-capabilities.constant.js +27 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.js +1 -7
- package/dist/parsers/data.parser.d.ts +3 -33
- package/dist/parsers/data.parser.js +70 -69
- package/dist/parsers/include.parser.d.ts +1 -1
- package/dist/parsers/include.parser.js +3 -7
- package/dist/parsers/order-by.parser.d.ts +1 -10
- package/dist/parsers/order-by.parser.js +3 -16
- package/dist/parsers/select.parser.d.ts +1 -1
- package/dist/parsers/select.parser.js +3 -7
- package/dist/parsers/where.parser.d.ts +4 -29
- package/dist/parsers/where.parser.js +49 -148
- package/dist/prisma7.adapter.d.ts +116 -81
- package/dist/prisma7.adapter.js +345 -362
- package/dist/resolvers/map-prisma-error.resolver.d.ts +0 -11
- package/dist/resolvers/map-prisma-error.resolver.js +48 -72
- package/dist/resolvers/merge-entities.resolver.d.ts +11 -15
- package/dist/resolvers/merge-entities.resolver.js +29 -38
- package/dist/resolvers/resolve-provider.resolver.d.ts +2 -22
- package/dist/resolvers/resolve-provider.resolver.js +10 -33
- package/dist/types/adapter-config.type.d.ts +21 -10
- package/dist/types/adapter-config.type.js +1 -3
- package/dist/types/adapter-relations.type.d.ts +14 -8
- package/dist/types/adapter-relations.type.js +1 -3
- package/dist/types/plain-object.type.js +1 -3
- package/dist/types/prisma-arg-like.type.js +1 -3
- package/dist/types/prisma-client-models.type.d.ts +4 -4
- package/dist/types/prisma-client-models.type.js +1 -3
- package/dist/types/prisma-provider.type.d.ts +1 -1
- package/dist/types/prisma-provider.type.js +1 -3
- package/dist/types/prisma-repository-like.type.d.ts +1 -1
- package/dist/types/prisma-repository-like.type.js +1 -3
- package/dist/types/prisma7-client-like.type.d.ts +1 -8
- package/dist/types/prisma7-client-like.type.js +1 -3
- package/dist/types/prisma7-global-config.type.d.ts +17 -0
- package/dist/types/prisma7-global-config.type.js +1 -0
- package/dist/types/prisma7-orm-types.type.d.ts +2 -2
- package/dist/types/prisma7-orm-types.type.js +1 -3
- package/dist/types/relation.type.d.ts +119 -25
- package/dist/types/relation.type.js +1 -3
- package/dist/validators/is-decimal.validator.d.ts +2 -5
- package/dist/validators/is-decimal.validator.js +3 -6
- package/dist/validators/is-plain-object.validator.d.ts +1 -1
- package/dist/validators/is-plain-object.validator.js +3 -7
- package/dist/validators/validate-prisma-client.validator.d.ts +1 -14
- package/dist/validators/validate-prisma-client.validator.js +7 -24
- package/package.json +108 -91
- package/dist/index.js.map +0 -1
- package/dist/parsers/data.parser.js.map +0 -1
- package/dist/parsers/include.parser.js.map +0 -1
- package/dist/parsers/order-by.parser.js.map +0 -1
- package/dist/parsers/select.parser.js.map +0 -1
- package/dist/parsers/where.parser.js.map +0 -1
- package/dist/prisma7.adapter.js.map +0 -1
- package/dist/resolvers/map-prisma-error.resolver.js.map +0 -1
- package/dist/resolvers/merge-entities.resolver.js.map +0 -1
- package/dist/resolvers/resolve-provider.resolver.js.map +0 -1
- package/dist/types/adapter-config.type.js.map +0 -1
- package/dist/types/adapter-relations.type.js.map +0 -1
- package/dist/types/plain-object.type.js.map +0 -1
- package/dist/types/prisma-arg-like.type.js.map +0 -1
- package/dist/types/prisma-client-models.type.js.map +0 -1
- package/dist/types/prisma-provider.type.js.map +0 -1
- package/dist/types/prisma-repository-like.type.js.map +0 -1
- package/dist/types/prisma7-client-like.type.js.map +0 -1
- package/dist/types/prisma7-orm-types.type.js.map +0 -1
- package/dist/types/relation.type.js.map +0 -1
- package/dist/validators/is-decimal.validator.js.map +0 -1
- package/dist/validators/is-plain-object.validator.js.map +0 -1
- package/dist/validators/validate-adapter-config.validator.d.ts +0 -8
- package/dist/validators/validate-adapter-config.validator.js +0 -78
- package/dist/validators/validate-adapter-config.validator.js.map +0 -1
- package/dist/validators/validate-prisma-client.validator.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,299 +1,394 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
|
|
3
|
-
|
|
4
|
-
<p style="margin-top: 12px;">
|
|
5
|
-
<img src="https://img.shields.io/npm/v/@vsrepo/prisma7-adapter?style=flat-square" alt="npm version"/>
|
|
6
|
-
<img src="https://img.shields.io/npm/l/@vsrepo/prisma7-adapter?style=flat-square" alt="npm license"/>
|
|
7
|
-
<img src="https://img.shields.io/npm/dt/@vsrepo/prisma7-adapter?style=flat-square" alt="npm downloads"/>
|
|
8
|
-
<img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
|
|
9
|
-
</p>
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
# VSRepoPrisma7Adapter
|
|
13
|
-
|
|
14
|
-
🇧🇷 [Ler em português](./README.pt-BR.md)
|
|
15
|
-
|
|
16
|
-
>
|
|
17
|
-
|
|
18
|
-
`VSRepoAdapter` implementation for [VSRepository
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Table of contents
|
|
23
|
-
|
|
24
|
-
- [Installation](#installation)
|
|
25
|
-
- [Basic usage](#basic-usage)
|
|
26
|
-
- [Constructor config](#constructor-config)
|
|
27
|
-
- [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- [`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- [`
|
|
36
|
-
- [
|
|
37
|
-
- [`
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| `mode` |
|
|
138
|
-
|
|
|
139
|
-
| `
|
|
140
|
-
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- `
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://res.cloudinary.com/ddbfifdxd/image/upload/w_200,q_auto,f_auto/v1786386427/VS_logo_TextoAbaixo_yev4tq.png" alt="VSRepository Logo" width="200"/>
|
|
3
|
+
|
|
4
|
+
<p style="margin-top: 12px;">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/@vsrepo/prisma7-adapter?style=flat-square" alt="npm version"/>
|
|
6
|
+
<img src="https://img.shields.io/npm/l/@vsrepo/prisma7-adapter?style=flat-square" alt="npm license"/>
|
|
7
|
+
<img src="https://img.shields.io/npm/dt/@vsrepo/prisma7-adapter?style=flat-square" alt="npm downloads"/>
|
|
8
|
+
<img src="https://img.shields.io/badge/inspired%20by-JpaRepository-E73121?style=flat-square" alt="inspired by JpaRepository"/>
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
# VSRepoPrisma7Adapter
|
|
13
|
+
|
|
14
|
+
🇧🇷 [Ler em português](./README.pt-BR.md)
|
|
15
|
+
|
|
16
|
+
> 🚧 **In development.** This is the `v2` of `@vsrepo/prisma7-adapter`, targeting [VSRepository v3](https://github.com/VSRepository/VSRepository). It's a development version, not a stable release yet, so its API may still change. For VSRepository v2, use the 1.x line (latest: `1.3.2`). It's currently the only officially supported adapter — adapters for other ORMs are planned but not published yet (see [Adapter status](https://github.com/VSRepository/VSRepository#adapter-status) in the VSRepository README).
|
|
17
|
+
|
|
18
|
+
`VSRepoAdapter` implementation for [VSRepository v3](https://github.com/VSRepository/VSRepository) backed by [Prisma 7](https://www.prisma.io/). It translates every `VSRepository` operation into Prisma Client calls, resolving `VSRepoWhere`, `Ordering`, `select`/`relations` through dedicated parsers, and — when a `relations` config is provided — resolving relation fields on `create`/`update`/`upsert`/`save`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Table of contents
|
|
23
|
+
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Basic usage](#basic-usage)
|
|
26
|
+
- [Constructor config](#constructor-config)
|
|
27
|
+
- [Global config](#global-config)
|
|
28
|
+
- [Relations](#relations)
|
|
29
|
+
- [The two `relations`](#the-two-relations)
|
|
30
|
+
- [`relations` in the constructor (write)](#relations-in-the-constructor-write)
|
|
31
|
+
- [`mode`](#mode)
|
|
32
|
+
- [`pk`](#pk)
|
|
33
|
+
- [`onUpdate`](#onupdate)
|
|
34
|
+
- [`onDisconnect`](#ondisconnect)
|
|
35
|
+
- [Ignoring a relation (`"ignore"`)](#ignoring-a-relation-ignore)
|
|
36
|
+
- [`fkHere` and `fkThere`](#fkhere-and-fkthere)
|
|
37
|
+
- [`allowOmitPk`](#allowomitpk)
|
|
38
|
+
- [How each write method resolves relations](#how-each-write-method-resolves-relations)
|
|
39
|
+
- [`relations` in method options (read)](#relations-in-method-options-read)
|
|
40
|
+
- [Atomic and aggregation methods](#atomic-and-aggregation-methods)
|
|
41
|
+
- [`createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes](#createmanycreatemanyreturningupdatemanyupdatemanyreturning-dont-support-nested-writes)
|
|
42
|
+
- [Provider-specific limitations](#provider-specific-limitations)
|
|
43
|
+
- [Transactions](#transactions)
|
|
44
|
+
- [Logging](#logging)
|
|
45
|
+
- [Requirements](#requirements)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install vsrepo@dev @prisma/client @vsrepo/prisma7-adapter@dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
While `2.0.0` is in development, install both `vsrepo` and `@vsrepo/prisma7-adapter` with the `dev` tag.
|
|
56
|
+
|
|
57
|
+
## Basic usage
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { VSRepository, VSLogLevel } from "vsrepo";
|
|
61
|
+
import { Prisma7Adapter, Prisma7OrmTypes } from "@vsrepo/prisma7-adapter";
|
|
62
|
+
import { Prisma, PrismaClient } from "./generated/prisma/client";
|
|
63
|
+
import { prisma } from "./prisma";
|
|
64
|
+
|
|
65
|
+
type User = Prisma.UserGetPayload<{ include: { posts: true } }>;
|
|
66
|
+
type MyOrmTypes = Prisma7OrmTypes<PrismaClient>;
|
|
67
|
+
|
|
68
|
+
class UserRepository extends VSRepository<User, string, MyOrmTypes> {
|
|
69
|
+
constructor() {
|
|
70
|
+
super({
|
|
71
|
+
adapter: new Prisma7Adapter(prisma, {
|
|
72
|
+
modelName: "user",
|
|
73
|
+
pkName: "id",
|
|
74
|
+
relations: {
|
|
75
|
+
posts: { mode: "otm", pk: "id" },
|
|
76
|
+
},
|
|
77
|
+
logLevel: VSLogLevel.WARN,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userRepository = new UserRepository();
|
|
84
|
+
|
|
85
|
+
const user = await userRepository.get("...", { relations: { posts: true } }); // by primary key
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Here the `relations` you pass in the method `options` — shaped like `{ field: true }` — is transformed into a Prisma `include` by the adapter (see [`relations` in method options (read)](#relations-in-method-options-read)). If you supply `select`, the `relations`/`include` is ignored. Don't confuse it with the constructor-config `relations`, which describes how relation fields are resolved in write payloads — the difference is explained in [The two `relations`](#the-two-relations).
|
|
89
|
+
|
|
90
|
+
`Prisma7OrmTypes<DB>` ties `VSRepository`'s `getDbClient()`/`transaction()` return types to your real, generated Prisma types — see [Transactions](#transactions).
|
|
91
|
+
|
|
92
|
+
## Constructor config
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
new Prisma7Adapter(prisma, {
|
|
96
|
+
modelName: "user", // required — the Prisma Client model/delegate name, as in `prisma.user`
|
|
97
|
+
pkName: "id", // required — the entity's primary key field name
|
|
98
|
+
relations: { ... }, // optional — see "relations in the constructor (write)" below
|
|
99
|
+
provider: "postgresql", // optional — detected from the Prisma Client when omitted
|
|
100
|
+
logLevel: VSLogLevel.WARN, // optional — default: VSLogLevel.WARN
|
|
101
|
+
allowOmitPk: false, // optional — default: false. Read "`allowOmitPk`" before turning it on
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
At construction, the adapter checks the Prisma Client and the model, and resolves the datasource provider:
|
|
106
|
+
|
|
107
|
+
- a `null`/`undefined` client throws a `VSRepoAdapterError` (code `MISSING_DB_CLIENT`);
|
|
108
|
+
- a `modelName` with no matching delegate on the client (`prisma.<modelName>`) throws `MODEL_NOT_FOUND`;
|
|
109
|
+
- the provider comes from `provider` when set; otherwise it's detected from the generated client (`_engineConfig.activeProvider`, falling back to the driver adapter's `provider`). If it can't be detected, construction throws `NOT_SUPPORTED` — set `provider` explicitly in that case (one of `postgresql`, `cockroachdb`, `mysql`, `sqlite`, `sqlserver`).
|
|
110
|
+
|
|
111
|
+
The other fields (`pkName`, `relations`, `logLevel`, `allowOmitPk`) are checked by TypeScript only, not at runtime.
|
|
112
|
+
|
|
113
|
+
## Global config
|
|
114
|
+
|
|
115
|
+
`provider`, `logLevel` and `allowOmitPk` can be set once for every adapter, through the static `setGlobalConfig`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { VSLogLevel } from "vsrepo";
|
|
119
|
+
import { Prisma7Adapter } from "@vsrepo/prisma7-adapter";
|
|
120
|
+
|
|
121
|
+
Prisma7Adapter.setGlobalConfig({ logLevel: VSLogLevel.WARN });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- It only affects adapters created **after** the call, so call it before instantiating your repositories.
|
|
125
|
+
- Each call **replaces** the previous global config — it doesn't merge with it.
|
|
126
|
+
- A value set in an adapter's own config always wins over the global one.
|
|
127
|
+
|
|
128
|
+
## Relations
|
|
129
|
+
|
|
130
|
+
### The two `relations`
|
|
131
|
+
|
|
132
|
+
The name `relations` appears in **two different places** in the API, with **different shapes and purposes** — easy to confuse. In a nutshell:
|
|
133
|
+
|
|
134
|
+
| | `relations` in the **constructor** | `relations` in **method options** |
|
|
135
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| Where you define it | `new Prisma7Adapter(prisma, { relations: ... })` | `repository.get(where, { relations: ... })` — and other methods |
|
|
137
|
+
| Shape | One **config object** per field (`{ mode, pk, onUpdate?, onDisconnect?, fkHere?, fkThere? }`), or `"ignore"` | One **`true`/sub-object** per field: `{ posts: true }` |
|
|
138
|
+
| Purpose | **Write** — when a `create`/`update`/`upsert`/`save` payload contains a relation field, tells the adapter how to turn it into a Prisma nested write (`create`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) | **Read** — eager loading: which relations to fetch alongside the result (becomes a Prisma `include`) |
|
|
139
|
+
| Consumed by | `parsePrismaWriteData` (write resolver) | `parsePrismaInclude` (via `resolveReadArg`) |
|
|
140
|
+
| Depends on the other? | No — it only affects writes | No — it works even with no `relations` in the constructor |
|
|
141
|
+
|
|
142
|
+
Neither depends on the other: the method-options `relations` does eager loading even when the constructor has no `relations`, and the constructor `relations` governs write behavior even if you never pass `relations` in options. The two subsections below cover each one.
|
|
143
|
+
|
|
144
|
+
### `relations` in the constructor (write)
|
|
145
|
+
|
|
146
|
+
The `relations` config on the constructor describes, for each relation field of your entity, **how the adapter should resolve that field when it shows up in write payloads** (`create`/`update`/`upsert`/`save`).
|
|
147
|
+
|
|
148
|
+
Each relation is configured by its field name:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
relations: {
|
|
152
|
+
posts: { mode: "otm", pk: "id", onUpdate: "set" },
|
|
153
|
+
address: { mode: "oto", pk: "id", onUpdate: "upsert", onDisconnect: "delete" },
|
|
154
|
+
author: { mode: "mto", pk: "id" }, // every option has a default
|
|
155
|
+
audit: "ignore", // never written through this repository
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Without `relations`, every field — including relation fields — is passed straight through to Prisma's `where`/`data`/`create`/`update`, as-is. That's fine for scalar fields, but Prisma expects a very specific nested-write shape (`create`/`connectOrCreate`/`upsert`/`disconnect`/`deleteMany`/`set`) for relation fields, so if your entity has relations you'll usually want to configure them.
|
|
160
|
+
|
|
161
|
+
#### `mode`
|
|
162
|
+
|
|
163
|
+
Cardinality of the relation, from the point of view of the entity that owns the field:
|
|
164
|
+
|
|
165
|
+
| `mode` | Meaning | Field shape you send | Options |
|
|
166
|
+
| ------ | ------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
|
|
167
|
+
| `oto` | one-to-one | a single object, or `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
|
|
168
|
+
| `mto` | many-to-one | a single object, or `null` | `pk`, `onUpdate` (to-one), `onDisconnect` (`"disconnect"`/`"delete"`), `fkHere`, `fkThere` |
|
|
169
|
+
| `otm` | one-to-many | an array of objects | `pk`, `onUpdate` (to-many), `fkThere` |
|
|
170
|
+
| `mtm` | many-to-many | an array of objects | `pk`, `onUpdate` (to-many), `fkThere` |
|
|
171
|
+
|
|
172
|
+
#### `pk`
|
|
173
|
+
|
|
174
|
+
The field used to identify an existing related record. An item **with** `pk` in the payload is connected to (or upserted into) the existing record; an item **without** it is created.
|
|
175
|
+
|
|
176
|
+
#### `onUpdate`
|
|
177
|
+
|
|
178
|
+
How `save`/`update`/`upsert` write the relation on update. The values accepted depend on the `mode`.
|
|
179
|
+
|
|
180
|
+
**To-one (`oto`/`mto`)**, when the related object has its `pk`:
|
|
181
|
+
|
|
182
|
+
- **`"connect"`** never creates nor overwrites the related record: it only points the relation to it.
|
|
183
|
+
- **`"connectOrCreate"`** creates the related record when it is missing.
|
|
184
|
+
- **`"upsert"`** is the one that **overwrites** the related record with the fields you sent.
|
|
185
|
+
- When the owner record itself is being created (`create`, or the `create` branch of `save`), a related object with `pk` always becomes a `connectOrCreate`.
|
|
186
|
+
|
|
187
|
+
**To-many (`otm`/`mtm`)**, for the items that already exist (matched by `pk`) and for the ones that were **not** sent:
|
|
188
|
+
|
|
189
|
+
- **`"add"`** (default) — only creates/connects/upserts the items you send. Existing items that aren't in the payload are left untouched.
|
|
190
|
+
- **`"set"`** — same as `"add"`, but also removes what wasn't sent: `otm` runs a `deleteMany` on the items missing from the array, and `mtm` resets the join (`set: []`) before reconnecting.
|
|
191
|
+
|
|
192
|
+
#### `onDisconnect`
|
|
193
|
+
|
|
194
|
+
What sending `null` to a to-one relation does on update:
|
|
195
|
+
|
|
196
|
+
- **`"disconnect"`** (default) — detaches the related record and keeps it. Prisma rejects it when the relation is required.
|
|
197
|
+
- **`"delete"`** — deletes the related record. Available for `oto` and `mto`, and always opt-in. Be careful on an `mto`: the related record is shared with other rows, so deleting it affects all of them, and Prisma rejects the write while other rows still reference it (unless the FK has a referential action such as `onDelete: Cascade`).
|
|
198
|
+
|
|
199
|
+
#### Ignoring a relation (`"ignore"`)
|
|
200
|
+
|
|
201
|
+
Setting a relation to `"ignore"` leaves that field out of every write (`create`, `update`, `save`...): the object (or array) is simply dropped. It is useful for a relation you **load** but don't want to **persist** through this entity.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// AddressRepository
|
|
205
|
+
relations: {
|
|
206
|
+
user: "ignore";
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Since the relation object is dropped, a scalar FK in the payload (`userId`) goes through as is, together with the primary key. That makes it the simplest way around the [`allowOmitPk` problem](#allowomitpk) when you only want to keep the FK: `address.save(addressLoadedWithItsUser)` updates the address and never touches the user.
|
|
211
|
+
|
|
212
|
+
#### `fkHere` and `fkThere`
|
|
213
|
+
|
|
214
|
+
In a one-to-one or many-to-one relation, only one side keeps the foreign key column. For `Address.user`, that's `Address.userId`. Tell the adapter which side each entity is on:
|
|
215
|
+
|
|
216
|
+
| Entity | Relation field | Holds the FK? | Config |
|
|
217
|
+
| ---------------------- | -------------- | --------------------------------------------------- | ------------------- |
|
|
218
|
+
| `Address` (owner side) | `user` | **yes** — `userId` is a column of `Address` | `fkHere: "userId"` |
|
|
219
|
+
| `User` (inverse side) | `address` | no — the FK lives in the related entity (`Address`) | `fkThere: "userId"` |
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// UserRepository
|
|
223
|
+
relations: { address: { mode: "oto", pk: "id", fkThere: "userId" } }
|
|
224
|
+
|
|
225
|
+
// AddressRepository
|
|
226
|
+
relations: { user: { mode: "oto", pk: "id", fkHere: "userId" } }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- **`fkThere`** — name of the FK that lives in the **related** entity. A related object loaded with `get`/`find` usually carries it (`address: { id, street, userId }`), but the relation already sets it, and Prisma doesn't accept it inside the nested write. The adapter strips it from the related payload(s) before building the write.
|
|
230
|
+
- **`fkHere`** — name of the FK that lives in **this** entity. Prisma doesn't accept the scalar FK together with the relation object, so when the relation field is in the payload the adapter drops this field from `create` and `update`. A payload that carries only the scalar FK (and no relation object) is left as it is.
|
|
231
|
+
|
|
232
|
+
`fkHere` has one more consequence, for the primary key — read the next section.
|
|
233
|
+
|
|
234
|
+
#### `allowOmitPk`
|
|
235
|
+
|
|
236
|
+
`allowOmitPk` is an adapter option (it can also be set in the [global config](#global-config)), but it only matters for relations configured with `fkHere`.
|
|
237
|
+
|
|
238
|
+
**The problem.** Take an `Address` loaded together with its `user` (`relations: { user: true }`) and sent back to `save`. The payload has the primary key (`id`) **and** a relation that holds its FK in the entity itself. Once the relation object is there, Prisma won't take the scalar FK nor the primary key (`Unknown argument 'id'`), so the adapter can't send everything.
|
|
239
|
+
|
|
240
|
+
**What the adapter does:**
|
|
241
|
+
|
|
242
|
+
- **`allowOmitPk: false`** (default) — throws a `VSRepoAdapterError` (`INVALID_DATA`) **before** touching the database, explaining what to do. Payloads without a primary key (e.g. a plain `create`), or without the relation object, are not affected.
|
|
243
|
+
- **`allowOmitPk: true`** — drops the primary key from `create` and `update`. Records that already exist are still found through `where: { id }`, so **updates work normally**. The catch is the `create` branch of `save`: if the record doesn't exist yet, it is created with a **database-generated primary key, not the one you sent** — and nothing warns you.
|
|
244
|
+
|
|
245
|
+
**When to turn it on.** Only if you really need it, and only if it's not your application that manages the ids — i.e. the database generates them (`autoincrement`, `uuid()`/`cuid()` defaults, sequences...). If your application assigns the ids (client-generated UUIDs, ids imported from another system, ids that other records already reference), keep it **off**: a record that doesn't exist would silently be stored under a different id.
|
|
246
|
+
|
|
247
|
+
**Ways around it, without `allowOmitPk`:**
|
|
248
|
+
|
|
249
|
+
1. **Ignore the relation** when you only want to keep the FK: `relations: { user: "ignore" }` (see [Ignoring a relation](#ignoring-a-relation-ignore)). The payload keeps `id` and `userId`, and the related entity is not written.
|
|
250
|
+
2. **Send the scalar FK instead of the relation object.** `address.save({ id, city, userId })` keeps both `id` and `userId`, since there's no relation object. Persist the related entity on its own (through its repository) when it also changed — ideally in one `transaction`.
|
|
251
|
+
3. **Send only what changed.** `address.update({ id }, { city })` carries neither the primary key nor the relation.
|
|
252
|
+
4. **Save from the other side.** `user.save({ ...user, address })` keeps the primary keys (configure `fkThere` there): the FK lives in `Address`, so `User` has no FK of its own to conflict with.
|
|
253
|
+
5. **Let the database generate the key for new records.** Don't send `id` when creating: `address.create({ city, user })`.
|
|
254
|
+
|
|
255
|
+
#### How each write method resolves relations
|
|
256
|
+
|
|
257
|
+
| Method | Relations |
|
|
258
|
+
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
259
|
+
| `create` | `create`/`connectOrCreate` only (no upsert — there's nothing to update yet) |
|
|
260
|
+
| `update` / `upsert` (`update` half) / `save` (upsert branch) | Full resolution: `create`/`connect`/`connectOrCreate`/`upsert`/`disconnect`/`delete`/`deleteMany`/`set`, per `mode`/`onUpdate`/`onDisconnect` |
|
|
261
|
+
| `createMany` / `createManyReturning` / `updateMany` / `updateManyReturning` | Not supported — throws a `VSRepoAdapterError` naming the offending field if the payload contains a configured relation |
|
|
262
|
+
|
|
263
|
+
### `relations` in method options (read)
|
|
264
|
+
|
|
265
|
+
This is the `relations` you pass in the `options` of a `VSRepository` method (`get`, `find`, `findOne`, etc.). The shape is much simpler: an object where each relation field accepts:
|
|
266
|
+
|
|
267
|
+
- `true` — load the relation as-is;
|
|
268
|
+
- or another `relations` object — for nested eager loading (relations of the relation).
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const user = await userRepository.get(
|
|
272
|
+
"...", // primary key
|
|
273
|
+
{
|
|
274
|
+
relations: {
|
|
275
|
+
posts: true, // fetch posts along
|
|
276
|
+
author: { profile: true }, // and, inside author, its profile (nested eager loading)
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This object is turned into a Prisma `include` by the adapter (`parsePrismaInclude`). It does **not** use the constructor's `relations` config: it's purely a read-side option and works even with no `relations` in the config.
|
|
283
|
+
|
|
284
|
+
The `select` and `relations` you pass in the options are turned into a Prisma `select` (`parsePrismaSelect`) and a Prisma `include` (`parsePrismaInclude`), respectively. When `select` is provided, the `include` derived from `relations` is discarded (`undefined`), because Prisma doesn't allow combining `include` and `select` in a single query. In other words, if you're already using `select`, the `relations` object becomes redundant — so it's optional. Here's the resolved logic (from `resolveReadArg`):
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const prismaSelect = options.select && parsePrismaSelect(options.select);
|
|
288
|
+
const prismaInclude = prismaSelect ? undefined : options.relations && parsePrismaInclude(options.relations);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Atomic and aggregation methods
|
|
292
|
+
|
|
293
|
+
The adapter implements the 12 abstract methods `VSRepository`'s `increment`/`decrement`/`multiply`/`divide`, their `*Many` counterparts, and `sum`/`average`/`min`/`max` delegate to: `incrementOne`, `decrementOne`, `multiplyOne`, `divideOne`, `incrementMany`, `decrementMany`, `multiplyMany`, `divideMany`, `sum`, `average`, `min`, `max`.
|
|
294
|
+
|
|
295
|
+
- `incrementOne`/`decrementOne`/`multiplyOne`/`divideOne` translate directly into Prisma's native single-field atomic write — `data: { [field]: { increment: value } }` (and `decrement`/`multiply`/`divide`) — so the operation is evaluated **server-side** against the row's _current_ value (`UPDATE ... SET field = field + value`), not as a fetch-then-save round trip on the client. Prisma's `update()` already returns the row reflecting the state _after_ the write, so the adapter never issues a follow-up read.
|
|
296
|
+
- `incrementMany`/`decrementMany`/`multiplyMany`/`divideMany` apply that same native write to **every** record matching `where`, through Prisma's `updateMany`, also evaluated server-side. They return a `{ count }` (`CountResult`) instead of the records.
|
|
297
|
+
- `sum`/`average`/`min`/`max` translate into Prisma's `aggregate()` with `_sum`/`_avg`/`_min`/`_max: { [field]: true }`. The raw result (`number`, `bigint`, a `Decimal` instance, or `null`) is normalized to `number | null` — `null` is returned as-is (mirroring SQL's `SUM()`/`AVG()`/`MIN()`/`MAX()`, which return `NULL`, not `0`, over an empty set), and a `Decimal`/`DecimalLike` value is converted via its `.toNumber()`.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Atomic — evaluated server-side, no read-modify-write:
|
|
301
|
+
await productRepository.increment("stock", 10, { id: productId });
|
|
302
|
+
await accountRepository.decrement("balance", 50, { id: accountId });
|
|
303
|
+
await productRepository.multiply("price", 1.1, { id: productId }); // e.g. a 10% price bump
|
|
304
|
+
await productRepository.divide("price", 2, { id: productId });
|
|
305
|
+
|
|
306
|
+
// Same, for every record matching `where` — returns `{ count }`:
|
|
307
|
+
await productRepository.incrementMany("stock", 10, { category: "books" });
|
|
308
|
+
|
|
309
|
+
// Aggregation — across every record matching an optional `where` (all if omitted):
|
|
310
|
+
const total = await productRepository.sum("price"); // number | null
|
|
311
|
+
const avgPrice = await productRepository.average("price", { active: true });
|
|
312
|
+
const cheapest = await productRepository.min("price");
|
|
313
|
+
const mostExpensive = await productRepository.max("price");
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Decimal fields** (`Prisma.Decimal`) are fully supported as the `field` for all of these methods — `value` for the atomic methods is passed straight through to Prisma (which accepts a `number`, `string`, or `Decimal`/`DecimalJsLike`), and the aggregate methods always normalize the result to a `number`, never a `Decimal` instance.
|
|
317
|
+
|
|
318
|
+
## `createMany`/`createManyReturning`/`updateMany`/`updateManyReturning` don't support nested writes
|
|
319
|
+
|
|
320
|
+
`createMany`, `createManyReturning`, `updateMany` and `updateManyReturning` only accept scalar fields in their `data`. If your payload includes a field configured in `relations` (regardless of its value), the adapter throws a `VSRepoAdapterError` naming the offending field. For a full nested write, use `create`/`update`/`save` one record at a time, or wrap several `save` calls in a `saveMany`/`transaction`.
|
|
321
|
+
|
|
322
|
+
> Note on return order: `createManyReturning` and `updateManyReturning` don't guarantee the returned records follow the order of the input payload (`objs`). Without `select`/`relations` they are a single `createManyAndReturn`/`updateManyAndReturn` call; with them, the result comes from a second `findMany` (re-querying the inserted/updated rows by primary key, inside a transaction). Either way, don't rely on the order matching the input.
|
|
323
|
+
|
|
324
|
+
## Provider-specific limitations
|
|
325
|
+
|
|
326
|
+
Some features this adapter relies on aren't available on every database provider. The adapter checks every feature below upfront and throws a `VSRepoAdapterError` (code `NOT_SUPPORTED`) before touching the database.
|
|
327
|
+
|
|
328
|
+
| Feature | Supported providers | Not supported |
|
|
329
|
+
| ------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
330
|
+
| `ignoreCase` (translated to Prisma's `mode: "insensitive"`; used by `findByXIgnoreCase` dynamic methods) | PostgreSQL, CockroachDB | MySQL, SQLite, SQL Server — MySQL and SQL Server are case-insensitive by default (depending on the collation), so no `mode` is needed there; SQLite is only case-insensitive for ASCII characters |
|
|
331
|
+
| `createManyReturning` / `updateManyReturning` (built on Prisma's `createManyAndReturn`/`updateManyAndReturn`) | PostgreSQL, CockroachDB, SQLite | MySQL, SQL Server |
|
|
332
|
+
| `ignoreConflicts: true` on `createMany` / `createManyReturning` (Prisma's `skipDuplicates`) | PostgreSQL, MySQL, CockroachDB | SQL Server, SQLite |
|
|
333
|
+
|
|
334
|
+
The `like` operator isn't supported on any provider — Prisma has no equivalent, so the adapter throws `NOT_SUPPORTED`. Use `contains`, `startsWith` or `endsWith` instead.
|
|
335
|
+
|
|
336
|
+
> These limitations come from Prisma itself, not from this adapter — see [Prisma's case sensitivity docs](https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#mode) and [CRUD reference](https://www.prisma.io/docs/orm/v7/prisma-client/queries/crud) for details. The adapter is covered by integration tests on PostgreSQL, MySQL, SQLite, SQL Server and CockroachDB.
|
|
337
|
+
|
|
338
|
+
## Transactions
|
|
339
|
+
|
|
340
|
+
**Every** method accepts `options.db` and runs its operation on the client/transaction you pass — the difference is in **how** each one treats it:
|
|
341
|
+
|
|
342
|
+
- Most methods (single, one-call Prisma operations) simply run directly on `options?.db`: hand them a transaction client and the call participates in that transaction, starting nothing new.
|
|
343
|
+
- `saveMany` and `deleteManyReturning` — and `updateManyReturning`/`createManyReturning` when you pass `select` or `relations` — need to run **more than one** Prisma operation atomically (`saveMany` saves each record individually; `updateManyReturning`/`createManyReturning` do an `updateManyAndReturn`/`createManyAndReturn` + `findMany`; `deleteManyReturning` does a `findMany` + `deleteMany`), so they go through `runTransactional`. Without `select`/`relations`, `updateManyReturning`/`createManyReturning` are a single Prisma call. If `options.db` is already an active transaction client, it's reused — no nested transaction is started; otherwise, a new transaction is created (on `options.db` if provided, or on the root client otherwise).
|
|
344
|
+
|
|
345
|
+
A transaction client is told apart from the root `PrismaClient` by the `$on` method: Prisma's root client exposes it (for event listeners), the interactive transaction client doesn't. This is what `Prisma7ClientLike`/`Prisma7OrmTypes` encode in their types.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
await userRepository.transaction(async tx => {
|
|
349
|
+
// Every repository call made with `{ db: tx }` inside this callback
|
|
350
|
+
// shares the same transaction — including calls to `saveMany`/
|
|
351
|
+
// `updateManyReturning`/`createManyReturning`/`deleteManyReturning`, which
|
|
352
|
+
// will detect `tx` is already a transaction and run directly on it instead
|
|
353
|
+
// of nesting a new one.
|
|
354
|
+
await userRepository.save(user, { db: tx });
|
|
355
|
+
await userRepository.save(otherUsers, { db: tx }); // an array saves several records
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Concurrency in `deleteManyReturning`
|
|
360
|
+
|
|
361
|
+
`deleteManyReturning` runs a `findMany` on the given `where` (to capture the records it will return) and then deletes **those same records by primary key** with a `deleteMany`, all inside one transaction. Prisma has no native `deleteManyAndReturn`, which is why it takes these two steps. If your `select` leaves the primary key out, the adapter adds it to the query internally and strips it from the returned records.
|
|
362
|
+
|
|
363
|
+
Since the delete is driven by the primary keys that were actually fetched, a row **inserted after** the `findMany` is never deleted by mistake. A concurrent change between the two steps can still make them diverge, though:
|
|
364
|
+
|
|
365
|
+
- a returned row that was **deleted by someone else** in the meantime is still returned, even though this call didn't delete it;
|
|
366
|
+
- a returned row that **stops matching** the `where` before the `deleteMany` runs is still deleted, since the `where` isn't re-applied.
|
|
367
|
+
|
|
368
|
+
If you need to guarantee there are no concurrency issues, run this method inside a `repository.transaction()` at a higher isolation level — e.g. `SERIALIZABLE`:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { VSRepoIsolationLevel } from "vsrepo";
|
|
372
|
+
|
|
373
|
+
await userRepository.transaction(async tx => {
|
|
374
|
+
// Atomic: no concurrent insert/update can slip between the findMany
|
|
375
|
+
// and the deleteMany, since SERIALIZABLE isolates this transaction.
|
|
376
|
+
// Always pass `{ db: tx }` so the call runs inside this transaction.
|
|
377
|
+
const deleted = await userRepository.deleteManyReturning({ id: { in: [...] } }, { db: tx });
|
|
378
|
+
}, {
|
|
379
|
+
isolationLevel: VSRepoIsolationLevel.SERIALIZABLE,
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> The isolation level applies only when the repository call actually runs **inside** that transaction (i.e. with `{ db: tx }`). When `deleteManyReturning` starts its own internal transaction (no `options.db` passed), it falls back to the default isolation level.
|
|
384
|
+
|
|
385
|
+
## Logging
|
|
386
|
+
|
|
387
|
+
The adapter uses `VSLogger` (from `vsrepo`) internally: every method logs a `DEBUG` line with the resolved Prisma arg, and start/end performance logs. Set `logLevel` in the constructor config to control verbosity; it defaults to `VSLogLevel.WARN`.
|
|
388
|
+
|
|
389
|
+
## Requirements
|
|
390
|
+
|
|
391
|
+
- `vsrepo` ^3.0.0-dev.3
|
|
392
|
+
- `@prisma/client` ^7.10.1
|
|
393
|
+
- Node.js `>=22`
|
|
394
|
+
- A PostgreSQL, CockroachDB, MySQL, SQLite or SQL Server database (all covered by the integration tests)
|