@roundtreasury/prisma-extension-soft-delete 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +993 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/lib/createSoftDeleteExtension.d.ts +2 -0
- package/dist/esm/lib/createSoftDeleteExtension.js +102 -0
- package/dist/esm/lib/helpers/createParams.d.ts +26 -0
- package/dist/esm/lib/helpers/createParams.js +374 -0
- package/dist/esm/lib/helpers/modifyResult.d.ts +4 -0
- package/dist/esm/lib/helpers/modifyResult.js +12 -0
- package/dist/esm/lib/types.d.ts +11 -0
- package/dist/esm/lib/types.js +1 -0
- package/dist/esm/lib/utils/nestedReads.d.ts +5 -0
- package/dist/esm/lib/utils/nestedReads.js +26 -0
- package/dist/esm/lib/utils/resultFiltering.d.ts +5 -0
- package/dist/esm/lib/utils/resultFiltering.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +20 -0
- package/dist/lib/createSoftDeleteExtension.d.ts +2 -0
- package/dist/lib/createSoftDeleteExtension.js +106 -0
- package/dist/lib/helpers/createParams.d.ts +26 -0
- package/dist/lib/helpers/createParams.js +393 -0
- package/dist/lib/helpers/modifyResult.d.ts +4 -0
- package/dist/lib/helpers/modifyResult.js +16 -0
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils/nestedReads.d.ts +5 -0
- package/dist/lib/utils/nestedReads.js +31 -0
- package/dist/lib/utils/resultFiltering.d.ts +5 -0
- package/dist/lib/utils/resultFiltering.js +22 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Prisma Extension Soft Delete</h1>
|
|
3
|
+
|
|
4
|
+
<p>Prisma extension for soft deleting records.</p>
|
|
5
|
+
|
|
6
|
+
<p>
|
|
7
|
+
Soft deleting records is a common pattern in many applications. This library provides an extension for Prisma that
|
|
8
|
+
allows you to soft delete records and exclude them from queries. It handles deleting records through relations and
|
|
9
|
+
excluding soft deleted records when including relations or referencing them in where objects. It does this by using
|
|
10
|
+
the <a href="https://github.com/olivierwilkinson/prisma-nested-middleware">prisma-extension-nested-operations</a>
|
|
11
|
+
library to handle nested relations.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<hr />
|
|
17
|
+
|
|
18
|
+
[![Build Status][build-badge]][build]
|
|
19
|
+
[![version][version-badge]][package]
|
|
20
|
+
[![MIT License][license-badge]][license]
|
|
21
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
22
|
+
[![PRs Welcome][prs-badge]][prs]
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
27
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
28
|
+
|
|
29
|
+
- [Installation](#installation)
|
|
30
|
+
- [Usage](#usage)
|
|
31
|
+
- [Extension Setup](#extension-setup)
|
|
32
|
+
- [Prisma Schema Setup](#prisma-schema-setup)
|
|
33
|
+
- [Behaviour](#behaviour)
|
|
34
|
+
- [Deleting Records](#deleting-records)
|
|
35
|
+
- [Deleting a Single Record](#deleting-a-single-record)
|
|
36
|
+
- [Deleting Multiple Records](#deleting-multiple-records)
|
|
37
|
+
- [Deleting Through a relationship](#deleting-through-a-relationship)
|
|
38
|
+
- [Hard Deletes](#hard-deletes)
|
|
39
|
+
- [Excluding Soft Deleted Records](#excluding-soft-deleted-records)
|
|
40
|
+
- [Excluding Soft Deleted Records in a `findFirst` Operation](#excluding-soft-deleted-records-in-a-findfirst-operation)
|
|
41
|
+
- [Excluding Soft Deleted Records in a `findMany` Operation](#excluding-soft-deleted-records-in-a-findmany-operation)
|
|
42
|
+
- [Excluding Soft Deleted Records in a `findUnique` Operation](#excluding-soft-deleted-records-in-a-findunique-operation)
|
|
43
|
+
- [Updating Records](#updating-records)
|
|
44
|
+
- [Explicitly Updating Many Soft Deleted Records](#explicitly-updating-many-soft-deleted-records)
|
|
45
|
+
- [Where objects](#where-objects)
|
|
46
|
+
- [Explicitly Querying Soft Deleted Records](#explicitly-querying-soft-deleted-records)
|
|
47
|
+
- [Including or Selecting Soft Deleted Records](#including-or-selecting-soft-deleted-records)
|
|
48
|
+
- [Including or Selecting toMany Relations](#including-or-selecting-tomany-relations)
|
|
49
|
+
- [Including or Selecting toOne Relations](#including-or-selecting-toone-relations)
|
|
50
|
+
- [Explicitly Including Soft Deleted Records in toMany Relations](#explicitly-including-soft-deleted-records-in-tomany-relations)
|
|
51
|
+
- [LICENSE](#license)
|
|
52
|
+
|
|
53
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
This module is distributed via [npm][npm] and should be installed as one of your
|
|
58
|
+
project's dependencies:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
npm install prisma-extension-soft-delete
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`@prisma/client` is a peer dependency of this library, so you will need to
|
|
65
|
+
install it if you haven't already:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
npm install @prisma/client
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### Extension Setup
|
|
74
|
+
|
|
75
|
+
To add soft delete functionality to your Prisma client create the extension using the `createSoftDeleteExtension`
|
|
76
|
+
function and pass it to `client.$extends`.
|
|
77
|
+
|
|
78
|
+
The `createSoftDeleteExtension` function takes a config object where you can define the models you want to use soft
|
|
79
|
+
delete with.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { PrismaClient } from "@prisma/client";
|
|
83
|
+
|
|
84
|
+
const client = new PrismaClient();
|
|
85
|
+
|
|
86
|
+
const extendedClient = client.$extends(
|
|
87
|
+
createSoftDeleteExtension({
|
|
88
|
+
models: {
|
|
89
|
+
Comment: true,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
By default the extension will use a `deleted` field of type `Boolean` on the model. If you want to use a custom field
|
|
96
|
+
name or value you can pass a config object for the model. For example to use a `deletedAt` field where the value is null
|
|
97
|
+
by default and a `DateTime` when the record is deleted you would pass the following:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const extendedClient = client.$extends(
|
|
101
|
+
createSoftDeleteExtension({
|
|
102
|
+
models: {
|
|
103
|
+
Comment: {
|
|
104
|
+
field: "deletedAt",
|
|
105
|
+
createValue: (deleted) => {
|
|
106
|
+
if (deleted) return new Date();
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `field` property is the name of the field to use for soft delete, and the `createValue` property is a function that
|
|
116
|
+
takes a deleted argument and returns the value for whether the record is soft deleted or not. The `createValue` method
|
|
117
|
+
must return a falsy value if the record is not deleted and a truthy value if it is deleted.
|
|
118
|
+
|
|
119
|
+
It is possible to setup soft delete for multiple models at once by passing a config for each model in the `models`
|
|
120
|
+
object:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const extendedClient = client.$extends(
|
|
124
|
+
createSoftDeleteExtension({
|
|
125
|
+
models: {
|
|
126
|
+
Comment: true,
|
|
127
|
+
Post: true,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
To modify the default field and type for all models you can pass a `defaultConfig`:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const extendedClient = client.$extends(
|
|
137
|
+
createSoftDeleteExtension({
|
|
138
|
+
models: {
|
|
139
|
+
Comment: true,
|
|
140
|
+
Post: true,
|
|
141
|
+
},
|
|
142
|
+
defaultConfig: {
|
|
143
|
+
field: "deletedAt",
|
|
144
|
+
createValue: (deleted) => {
|
|
145
|
+
if (deleted) return new Date();
|
|
146
|
+
return null;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
When using the default config you can also override the default config for a specific model by passing a config object
|
|
154
|
+
for that model:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const extendedClient = client.$extends(
|
|
158
|
+
createSoftDeleteExtension({
|
|
159
|
+
models: {
|
|
160
|
+
Comment: true,
|
|
161
|
+
Post: {
|
|
162
|
+
field: "deleted",
|
|
163
|
+
createValue: Boolean,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
defaultConfig: {
|
|
167
|
+
field: "deletedAt",
|
|
168
|
+
createValue: (deleted) => {
|
|
169
|
+
if (deleted) return new Date();
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The config object also has a `allowToOneUpdates` option that can be used to allow updates to toOne relationships through
|
|
178
|
+
nested updates. By default this is set to `false` and will throw an error if you try to update a toOne relationship
|
|
179
|
+
through a nested update. If you want to allow this you can set `allowToOneUpdates` to `true`:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const extendedClient = client.$extends(
|
|
183
|
+
createSoftDeleteExtension({
|
|
184
|
+
models: {
|
|
185
|
+
Comment: {
|
|
186
|
+
field: "deleted",
|
|
187
|
+
createValue: Boolean,
|
|
188
|
+
allowToOneUpdates: true,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
For more information for why updating through toOne relationship is disabled by default see the
|
|
196
|
+
[Updating Records](#updating-records) section.
|
|
197
|
+
|
|
198
|
+
Similarly to `allowToOneUpdates` there is an `allowCompoundUniqueIndexWhere` option that can be used to allow using
|
|
199
|
+
where objects with compound unique index fields when using `findUnique` queries. By default this is set to `false` and
|
|
200
|
+
will throw an error if you try to use a where with compound unique index fields. If you want to allow this you can set
|
|
201
|
+
`allowCompoundUniqueIndexWhere` to `true`:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const extendedClient = client.$extends(
|
|
205
|
+
createSoftDeleteExtension({
|
|
206
|
+
models: {
|
|
207
|
+
Comment: {
|
|
208
|
+
field: "deleted",
|
|
209
|
+
createValue: Boolean,
|
|
210
|
+
allowCompoundUniqueIndexWhere: true,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
For more information for why updating through toOne relationship is disabled by default see the
|
|
218
|
+
[Excluding Soft Deleted Records in a `findUnique` Operation](#excluding-soft-deleted-records-in-a-findunique-operation) section.
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
To allow to one updates or compound unique index fields globally you can use the `defaultConfig` to do so:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const extendedClient = client.$extends(
|
|
225
|
+
createSoftDeleteExtension({
|
|
226
|
+
models: {
|
|
227
|
+
User: true,
|
|
228
|
+
Comment: true,
|
|
229
|
+
},
|
|
230
|
+
defaultConfig: {
|
|
231
|
+
field: "deleted",
|
|
232
|
+
createValue: Boolean,
|
|
233
|
+
allowToOneUpdates: true,
|
|
234
|
+
allowCompoundUniqueIndexWhere: true,
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Prisma Schema Setup
|
|
241
|
+
|
|
242
|
+
The Prisma schema must be updated to include the soft delete field for each model you want to use soft delete with.
|
|
243
|
+
|
|
244
|
+
For models configured to use the default field and type you must add the `deleted` field to your Prisma schema manually.
|
|
245
|
+
Using the Comment model configured in [Extension Setup](#extension-setup) you would need add the following to the
|
|
246
|
+
Prisma schema:
|
|
247
|
+
|
|
248
|
+
```prisma
|
|
249
|
+
model Comment {
|
|
250
|
+
deleted Boolean @default(false)
|
|
251
|
+
[other fields]
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
If the Comment model was configured to use a `deletedAt` field where the value is null by default and a `DateTime` when
|
|
256
|
+
the record is deleted you would need to add the following to your Prisma schema:
|
|
257
|
+
|
|
258
|
+
```prisma
|
|
259
|
+
model Comment {
|
|
260
|
+
deletedAt DateTime?
|
|
261
|
+
[other fields]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Models configured to use soft delete that are related to other models through a toOne relationship must have this
|
|
266
|
+
relationship defined as optional. This is because the extension will exclude soft deleted records when the relationship
|
|
267
|
+
is included or selected. If the relationship is not optional the types for the relation will be incorrect and you may
|
|
268
|
+
get runtime errors.
|
|
269
|
+
|
|
270
|
+
For example if you have an `author` relationship on the Comment model and the User model is configured to use soft
|
|
271
|
+
delete you would need to change the relationship to be optional:
|
|
272
|
+
|
|
273
|
+
```prisma
|
|
274
|
+
model Comment {
|
|
275
|
+
authorId Int?
|
|
276
|
+
author User? @relation(fields: [authorId], references: [id])
|
|
277
|
+
[other fields]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`@unique` fields on models that are configured to use soft deletes may cause problems due to the records not actually
|
|
282
|
+
being deleted. If a record is soft deleted and then a new record is created with the same value for the unique field,
|
|
283
|
+
the new record will not be created.
|
|
284
|
+
|
|
285
|
+
## Behaviour
|
|
286
|
+
|
|
287
|
+
The main behaviour of the extension is to replace delete operations with update operations that set the soft delete
|
|
288
|
+
field to the deleted value.
|
|
289
|
+
|
|
290
|
+
The extension also prevents accidentally fetching or updating soft deleted records by excluding soft deleted records
|
|
291
|
+
from find queries, includes, selects and bulk updates. The extension does allow explicit queries for soft deleted
|
|
292
|
+
records and allows updates through unique fields such is it's id. The reason it allows updates through unique fields is
|
|
293
|
+
because soft deleted records can only be fetched explicitly so updates through a unique fields should be intentional.
|
|
294
|
+
|
|
295
|
+
### Deleting Records
|
|
296
|
+
|
|
297
|
+
When deleting a record using the `delete` or `deleteMany` operations the extension will change the operation to an
|
|
298
|
+
`update` operation and set the soft delete field to be the deleted value defined in the config for that model.
|
|
299
|
+
|
|
300
|
+
For example if the Comment model was configured to use the default `deleted` field of type `Boolean` the extension
|
|
301
|
+
would change the `delete` operation to an `update` operation and set the `deleted` field to `true`.
|
|
302
|
+
|
|
303
|
+
#### Deleting a Single Record
|
|
304
|
+
|
|
305
|
+
When deleting a single record using the `delete` operation:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
await client.comment.delete({
|
|
309
|
+
where: {
|
|
310
|
+
id: 1,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The extension would change the operation to:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
await client.comment.update({
|
|
319
|
+
where: {
|
|
320
|
+
id: 1,
|
|
321
|
+
},
|
|
322
|
+
data: {
|
|
323
|
+
deleted: true,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Deleting Multiple Records
|
|
329
|
+
|
|
330
|
+
When deleting multiple records using the `deleteMany` operation:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
await client.comment.deleteMany({
|
|
334
|
+
where: {
|
|
335
|
+
id: {
|
|
336
|
+
in: [1, 2, 3],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The extension would change the operation to:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
await client.comment.updateMany({
|
|
346
|
+
where: {
|
|
347
|
+
id: {
|
|
348
|
+
in: [1, 2, 3],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
data: {
|
|
352
|
+
deleted: true,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Deleting Through a relationship
|
|
358
|
+
|
|
359
|
+
When using a nested delete through a relationship the extension will change the nested delete operation to an update
|
|
360
|
+
operation:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
await client.post.update({
|
|
364
|
+
where: {
|
|
365
|
+
id: 1,
|
|
366
|
+
},
|
|
367
|
+
data: {
|
|
368
|
+
comments: {
|
|
369
|
+
delete: {
|
|
370
|
+
where: {
|
|
371
|
+
id: 2,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
author: {
|
|
376
|
+
delete: true,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The extension would change the operation to:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
await client.post.update({
|
|
386
|
+
where: {
|
|
387
|
+
id: 1,
|
|
388
|
+
},
|
|
389
|
+
data: {
|
|
390
|
+
comments: {
|
|
391
|
+
update: {
|
|
392
|
+
where: {
|
|
393
|
+
id: 2,
|
|
394
|
+
},
|
|
395
|
+
data: {
|
|
396
|
+
deleted: true,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
author: {
|
|
401
|
+
update: {
|
|
402
|
+
deleted: true,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The same behaviour applies when using a nested `deleteMany` with a toMany relationship.
|
|
410
|
+
|
|
411
|
+
#### Hard Deletes
|
|
412
|
+
|
|
413
|
+
Hard deletes are not currently supported by this extension, when the `extendedWhereUnique` feature is supported
|
|
414
|
+
it will be possible to explicitly hard delete a soft deleted record. In the meantime you can use the `executeRaw`
|
|
415
|
+
operation to perform hard deletes.
|
|
416
|
+
|
|
417
|
+
### Excluding Soft Deleted Records
|
|
418
|
+
|
|
419
|
+
When using the `findUnique`, `findFirst` and `findMany` operations the extension will modify the `where` object passed
|
|
420
|
+
to exclude soft deleted records. It does this by adding an additional condition to the `where` object that excludes
|
|
421
|
+
records where the soft delete field is set to the deleted value defined in the config for that model.
|
|
422
|
+
|
|
423
|
+
#### Excluding Soft Deleted Records in a `findFirst` Operation
|
|
424
|
+
|
|
425
|
+
When using a `findFirst` operation the extension will modify the `where` object to exclude soft deleted records, so for:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
await client.comment.findFirst({
|
|
429
|
+
where: {
|
|
430
|
+
id: 1,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The extension would change the operation to:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
await client.comment.findFirst({
|
|
439
|
+
where: {
|
|
440
|
+
id: 1,
|
|
441
|
+
deleted: false,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### Excluding Soft Deleted Records in a `findMany` Operation
|
|
447
|
+
|
|
448
|
+
When using a `findMany` operation the extension will modify the `where` object to exclude soft deleted records, so for:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
await client.comment.findMany({
|
|
452
|
+
where: {
|
|
453
|
+
id: 1,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
The extension would change the operation to:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
await client.comment.findMany({
|
|
462
|
+
where: {
|
|
463
|
+
id: 1,
|
|
464
|
+
deleted: false,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
#### Excluding Soft Deleted Records in a `findUnique` Operation
|
|
470
|
+
|
|
471
|
+
When using a `findUnique` operation the extension will change the query to use `findFirst` so that it can modify the
|
|
472
|
+
`where` object to exclude soft deleted records, so for:
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
await client.comment.findUnique({
|
|
476
|
+
where: {
|
|
477
|
+
id: 1,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The extension would change the operation to:
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
await client.comment.findFirst({
|
|
486
|
+
where: {
|
|
487
|
+
id: 1,
|
|
488
|
+
deleted: false,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
When querying using a compound unique index in the where object the extension will throw an error by default. This
|
|
494
|
+
is because it is not possible to use these types of where object with `findFirst` and it is not possible to exclude
|
|
495
|
+
soft-deleted records when using `findUnique`. For example take the following query:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
await client.user.findUnique({
|
|
499
|
+
where: {
|
|
500
|
+
name_email: {
|
|
501
|
+
name: "foo",
|
|
502
|
+
email: "bar",
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Since the compound unique index `@@unique([name, email])` is being queried through the `name_email` field of the where
|
|
509
|
+
object the extension will throw to avoid accidentally returning a soft deleted record.
|
|
510
|
+
|
|
511
|
+
It is possible to override this behaviour by setting `allowCompoundUniqueIndexWhere` to `true` in the model config.
|
|
512
|
+
|
|
513
|
+
### Updating Records
|
|
514
|
+
|
|
515
|
+
Updating records is split into three categories, updating a single record using a root operation, updating a single
|
|
516
|
+
record through a relation and updating multiple records either through a root operation or a relation.
|
|
517
|
+
|
|
518
|
+
When updating a single record using a root operation such as `update` or `upsert` the extension will not modify the
|
|
519
|
+
operation. This is because unless explicitly queried for soft deleted records should not be returned from queries,
|
|
520
|
+
so if these operations are updating a soft deleted record it should be intentional.
|
|
521
|
+
|
|
522
|
+
When updating a single record through a relation the extension will throw an error by default. This is because it is
|
|
523
|
+
not possible to filter out soft deleted records for nested toOne relations. For example take the following query:
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
await client.post.update({
|
|
527
|
+
where: {
|
|
528
|
+
id: 1,
|
|
529
|
+
},
|
|
530
|
+
data: {
|
|
531
|
+
author: {
|
|
532
|
+
update: {
|
|
533
|
+
name: "foo",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Since the `author` field is a toOne relation it does not support a where object. This means that if the `author` field
|
|
541
|
+
is a soft deleted record it will be updated accidentally.
|
|
542
|
+
|
|
543
|
+
It is possible to override this behaviour by setting `allowToOneUpdates` to `true` in the extension config.
|
|
544
|
+
|
|
545
|
+
When updating multiple records using `updateMany` the extension will modify the `where` object passed to exclude soft
|
|
546
|
+
deleted records. For example take the following query:
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
await client.comment.updateMany({
|
|
550
|
+
where: {
|
|
551
|
+
id: 1,
|
|
552
|
+
},
|
|
553
|
+
data: {
|
|
554
|
+
content: "foo",
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The extension would change the operation to:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
await client.comment.updateMany({
|
|
563
|
+
where: {
|
|
564
|
+
id: 1,
|
|
565
|
+
deleted: false,
|
|
566
|
+
},
|
|
567
|
+
data: {
|
|
568
|
+
content: "foo",
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
This also works when a toMany relation is updated:
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
await client.post.update({
|
|
577
|
+
where: {
|
|
578
|
+
id: 1,
|
|
579
|
+
},
|
|
580
|
+
data: {
|
|
581
|
+
comments: {
|
|
582
|
+
updateMany: {
|
|
583
|
+
where: {
|
|
584
|
+
id: 1,
|
|
585
|
+
},
|
|
586
|
+
data: {
|
|
587
|
+
content: "foo",
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
The extension would change the operation to:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
await client.post.update({
|
|
599
|
+
where: {
|
|
600
|
+
id: 1,
|
|
601
|
+
},
|
|
602
|
+
data: {
|
|
603
|
+
comments: {
|
|
604
|
+
updateMany: {
|
|
605
|
+
where: {
|
|
606
|
+
id: 1,
|
|
607
|
+
deleted: false,
|
|
608
|
+
},
|
|
609
|
+
data: {
|
|
610
|
+
content: "foo",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### Explicitly Updating Many Soft Deleted Records
|
|
619
|
+
|
|
620
|
+
When using the `updateMany` operation it is possible to explicitly update many soft deleted records by setting the
|
|
621
|
+
deleted field to the deleted value defined in the config for that model. An example that would update soft deleted
|
|
622
|
+
records would be:
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
await client.comment.updateMany({
|
|
626
|
+
where: {
|
|
627
|
+
content: "foo",
|
|
628
|
+
deleted: true,
|
|
629
|
+
},
|
|
630
|
+
data: {
|
|
631
|
+
content: "bar",
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Where objects
|
|
637
|
+
|
|
638
|
+
When using a `where` query it is possible to reference models configured to use soft deletes. In this case the
|
|
639
|
+
extension will modify the `where` object to exclude soft deleted records from the query, so for:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
await client.post.findMany({
|
|
643
|
+
where: {
|
|
644
|
+
id: 1,
|
|
645
|
+
comments: {
|
|
646
|
+
some: {
|
|
647
|
+
content: "foo",
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
The extension would change the operation to:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
await client.post.findMany({
|
|
658
|
+
where: {
|
|
659
|
+
id: 1,
|
|
660
|
+
comments: {
|
|
661
|
+
some: {
|
|
662
|
+
content: "foo",
|
|
663
|
+
deleted: false,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
This also works when the where object includes logical operators:
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
await client.post.findMany({
|
|
674
|
+
where: {
|
|
675
|
+
id: 1,
|
|
676
|
+
OR: [
|
|
677
|
+
{
|
|
678
|
+
comments: {
|
|
679
|
+
some: {
|
|
680
|
+
author: {
|
|
681
|
+
name: "Jack",
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
comments: {
|
|
688
|
+
none: {
|
|
689
|
+
author: {
|
|
690
|
+
name: "Jill",
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
The extension would change the operation to:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
await client.post.findMany({
|
|
704
|
+
where: {
|
|
705
|
+
id: 1,
|
|
706
|
+
OR: [
|
|
707
|
+
{
|
|
708
|
+
comments: {
|
|
709
|
+
some: {
|
|
710
|
+
deleted: false,
|
|
711
|
+
author: {
|
|
712
|
+
name: "Jack",
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
comments: {
|
|
719
|
+
none: {
|
|
720
|
+
deleted: false,
|
|
721
|
+
author: {
|
|
722
|
+
name: "Jill",
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
When using the `every` modifier the extension will modify the `where` object to exclude soft deleted records from the
|
|
733
|
+
query in a different way, so for:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
await client.post.findMany({
|
|
737
|
+
where: {
|
|
738
|
+
id: 1,
|
|
739
|
+
comments: {
|
|
740
|
+
every: {
|
|
741
|
+
content: "foo",
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
The extension would change the operation to:
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
await client.post.findMany({
|
|
752
|
+
where: {
|
|
753
|
+
id: 1,
|
|
754
|
+
comments: {
|
|
755
|
+
every: {
|
|
756
|
+
OR: [{ deleted: { not: false } }, { content: "foo" }],
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
This is because if the same logic that is used for `some` and `none` were to be used with `every` then the query would
|
|
764
|
+
fail for cases where there are deleted models.
|
|
765
|
+
|
|
766
|
+
The deleted case uses the `not` operator to ensure that the query works for custom fields and types. For example if the
|
|
767
|
+
field was configured to be `deletedAt` where the type is `DateTime` when deleted and `null` when not deleted then the
|
|
768
|
+
query would be:
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
await client.post.findMany({
|
|
772
|
+
where: {
|
|
773
|
+
id: 1,
|
|
774
|
+
comments: {
|
|
775
|
+
every: {
|
|
776
|
+
OR: [{ deletedAt: { not: null } }, { content: "foo" }],
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
#### Explicitly Querying Soft Deleted Records
|
|
784
|
+
|
|
785
|
+
It is possible to explicitly query soft deleted records by setting the configured field in the `where` object. For
|
|
786
|
+
example the following will include deleted records in the results:
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
await client.comment.findMany({
|
|
790
|
+
where: {
|
|
791
|
+
deleted: true,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
It is also possible to explicitly query soft deleted records through relationships in the `where` object. For example
|
|
797
|
+
the following will also not be modified:
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
await client.post.findMany({
|
|
801
|
+
where: {
|
|
802
|
+
comments: {
|
|
803
|
+
some: {
|
|
804
|
+
deleted: true,
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Including or Selecting Soft Deleted Records
|
|
812
|
+
|
|
813
|
+
When using `include` or `select` the extension will modify the `include` and `select` objects passed to exclude soft
|
|
814
|
+
deleted records.
|
|
815
|
+
|
|
816
|
+
#### Including or Selecting toMany Relations
|
|
817
|
+
|
|
818
|
+
When using `include` or `select` on a toMany relationship the extension will modify the where object to exclude soft
|
|
819
|
+
deleted records from the query, so for:
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
await client.post.findMany({
|
|
823
|
+
where: {
|
|
824
|
+
id: 1,
|
|
825
|
+
},
|
|
826
|
+
include: {
|
|
827
|
+
comments: true,
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
If the Comment model was configured to be soft deleted the extension would modify the `include` action where object to
|
|
833
|
+
exclude soft deleted records, so the query would be:
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
await client.post.findMany({
|
|
837
|
+
where: {
|
|
838
|
+
id: 1,
|
|
839
|
+
},
|
|
840
|
+
include: {
|
|
841
|
+
comments: {
|
|
842
|
+
where: {
|
|
843
|
+
deleted: false,
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
The same applies for `select`:
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
await client.post.findMany({
|
|
854
|
+
where: {
|
|
855
|
+
id: 1,
|
|
856
|
+
},
|
|
857
|
+
select: {
|
|
858
|
+
comments: true,
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
This also works for nested includes and selects:
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
await client.user.findMany({
|
|
867
|
+
where: {
|
|
868
|
+
id: 1,
|
|
869
|
+
},
|
|
870
|
+
include: {
|
|
871
|
+
posts: {
|
|
872
|
+
select: {
|
|
873
|
+
comments: {
|
|
874
|
+
where: {
|
|
875
|
+
content: "foo",
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
The extension would modify the query to:
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
await client.user.findMany({
|
|
888
|
+
where: {
|
|
889
|
+
id: 1,
|
|
890
|
+
},
|
|
891
|
+
include: {
|
|
892
|
+
posts: {
|
|
893
|
+
select: {
|
|
894
|
+
comments: {
|
|
895
|
+
where: {
|
|
896
|
+
deleted: false,
|
|
897
|
+
content: "foo",
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
#### Including or Selecting toOne Relations
|
|
907
|
+
|
|
908
|
+
Records included through a toOne relation are also excluded, however there is no way to explicitly include them. For
|
|
909
|
+
example the following query:
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
await client.post.findFirst({
|
|
913
|
+
where: {
|
|
914
|
+
id: 1,
|
|
915
|
+
},
|
|
916
|
+
include: {
|
|
917
|
+
author: true,
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
The extension would not modify the query since toOne relations do not support where clauses. Instead the extension
|
|
923
|
+
will manually filter results based on the configured deleted field.
|
|
924
|
+
|
|
925
|
+
So if the author of the Post was soft deleted the extension would filter the results and remove the author from the
|
|
926
|
+
results:
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
{
|
|
930
|
+
id: 1,
|
|
931
|
+
title: "foo",
|
|
932
|
+
author: null
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
When selecting specific fields on a toOne relation the extension will manually add the configured deleted field to the
|
|
937
|
+
select object, filter the results and finally strip the deleted field from the results before returning them.
|
|
938
|
+
|
|
939
|
+
For example the following query would behave that way:
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
await client.post.findMany({
|
|
943
|
+
where: {
|
|
944
|
+
id: 1,
|
|
945
|
+
},
|
|
946
|
+
select: {
|
|
947
|
+
author: {
|
|
948
|
+
select: {
|
|
949
|
+
name: true,
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
#### Explicitly Including Soft Deleted Records in toMany Relations
|
|
957
|
+
|
|
958
|
+
It is possible to explicitly include soft deleted records in toMany relations by adding the configured deleted field to
|
|
959
|
+
the `where` object. For example the following will include deleted records in the results:
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
await client.post.findMany({
|
|
963
|
+
where: {
|
|
964
|
+
id: 1,
|
|
965
|
+
},
|
|
966
|
+
include: {
|
|
967
|
+
comments: {
|
|
968
|
+
where: {
|
|
969
|
+
deleted: true,
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
## LICENSE
|
|
977
|
+
|
|
978
|
+
Apache 2.0
|
|
979
|
+
|
|
980
|
+
[npm]: https://www.npmjs.com/
|
|
981
|
+
[node]: https://nodejs.org
|
|
982
|
+
[build-badge]: https://github.com/olivierwilkinson/prisma-extension-soft-delete/workflows/prisma-extension-soft-delete/badge.svg
|
|
983
|
+
[build]: https://github.com/olivierwilkinson/prisma-extension-soft-delete/actions?query=branch%main+workflow%3Aprisma-extension-soft-delete
|
|
984
|
+
[version-badge]: https://img.shields.io/npm/v/prisma-extension-soft-delete.svg?style=flat-square
|
|
985
|
+
[package]: https://www.npmjs.com/package/prisma-extension-soft-delete
|
|
986
|
+
[downloads-badge]: https://img.shields.io/npm/dm/prisma-extension-soft-delete.svg?style=flat-square
|
|
987
|
+
[npmtrends]: http://www.npmtrends.com/prisma-extension-soft-delete
|
|
988
|
+
[license-badge]: https://img.shields.io/npm/l/prisma-extension-soft-delete.svg?style=flat-square
|
|
989
|
+
[license]: https://github.com/olivierwilkinson/prisma-extension-soft-delete/blob/main/LICENSE
|
|
990
|
+
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
|
991
|
+
[prs]: http://makeapullrequest.com
|
|
992
|
+
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
|
|
993
|
+
[coc]: https://github.com/olivierwilkinson/prisma-extension-soft-delete/blob/main/other/CODE_OF_CONDUCT.md
|