@bedrockio/model 0.2.17 → 0.2.18
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 +102 -15
- package/dist/cjs/delete-hooks.js +129 -115
- package/package.json +1 -1
- package/src/delete-hooks.js +124 -108
- package/types/delete-hooks.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -1019,12 +1019,15 @@ deletion. They are defined in the `onDelete` field of the model definition file:
|
|
|
1019
1019
|
}
|
|
1020
1020
|
},
|
|
1021
1021
|
"onDelete": {
|
|
1022
|
-
"clean":
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1022
|
+
"clean": [
|
|
1023
|
+
{
|
|
1024
|
+
"path": "profile"
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
"ref": "Shop",
|
|
1028
|
+
"path": "owner"
|
|
1026
1029
|
}
|
|
1027
|
-
|
|
1030
|
+
],
|
|
1028
1031
|
"errorOnReferenced": {
|
|
1029
1032
|
"except": ["AuditEntry"]
|
|
1030
1033
|
}
|
|
@@ -1035,12 +1038,13 @@ deletion. They are defined in the `onDelete` field of the model definition file:
|
|
|
1035
1038
|
#### Clean
|
|
1036
1039
|
|
|
1037
1040
|
`clean` determines other associated documents that will be deleted when the main
|
|
1038
|
-
document is deleted.
|
|
1041
|
+
document is deleted. It is defined as an array of operations that will be
|
|
1042
|
+
performed in order. Operations must contain either `path` or `paths`.
|
|
1039
1043
|
|
|
1040
1044
|
#### Local References
|
|
1041
1045
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1046
|
+
Operations that do not specify a `ref` are treated as local paths. In the above
|
|
1047
|
+
example:
|
|
1044
1048
|
|
|
1045
1049
|
```js
|
|
1046
1050
|
user.delete();
|
|
@@ -1050,26 +1054,110 @@ await user.populate('profile');
|
|
|
1050
1054
|
await user.profile.delete();
|
|
1051
1055
|
```
|
|
1052
1056
|
|
|
1053
|
-
#### Foreign
|
|
1057
|
+
#### Foreign References
|
|
1058
|
+
|
|
1059
|
+
Operations that specify a `ref` are treated as foreign references. In the above
|
|
1060
|
+
example:
|
|
1061
|
+
|
|
1062
|
+
```js
|
|
1063
|
+
user.delete();
|
|
1064
|
+
|
|
1065
|
+
// Will implicitly run:
|
|
1066
|
+
const shops = await Shop.find({
|
|
1067
|
+
owner: user,
|
|
1068
|
+
});
|
|
1069
|
+
for (let shop of shops) {
|
|
1070
|
+
await shop.delete();
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
#### Additional Filters
|
|
1075
|
+
|
|
1076
|
+
Operations may filter on additional fields with `query`:
|
|
1077
|
+
|
|
1078
|
+
```json
|
|
1079
|
+
// user.json
|
|
1080
|
+
{
|
|
1081
|
+
"onDelete": {
|
|
1082
|
+
"clean": [
|
|
1083
|
+
{
|
|
1084
|
+
"ref": "Shop",
|
|
1085
|
+
"path": "owner",
|
|
1086
|
+
"query": {
|
|
1087
|
+
"status": "active"
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
]
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1054
1094
|
|
|
1055
|
-
|
|
1056
|
-
that maps foreign `ref` names to their referencing field. In the above example:
|
|
1095
|
+
In this example:
|
|
1057
1096
|
|
|
1058
1097
|
```js
|
|
1059
1098
|
user.delete();
|
|
1060
1099
|
|
|
1061
1100
|
// Will implicitly run:
|
|
1062
|
-
const
|
|
1101
|
+
const shops = await Shop.find({
|
|
1102
|
+
status: 'active',
|
|
1063
1103
|
owner: user,
|
|
1064
1104
|
});
|
|
1065
|
-
|
|
1105
|
+
for (let shop of shops) {
|
|
1106
|
+
await shop.delete();
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
Any query that can be serliazed as JSON is valid, however top-level `$or`
|
|
1111
|
+
operators have special behavior with multiple paths (see note below).
|
|
1112
|
+
|
|
1113
|
+
#### Multiple Paths
|
|
1114
|
+
|
|
1115
|
+
An operation that specified an array of `paths` will implicitly run an `$or`
|
|
1116
|
+
query:
|
|
1117
|
+
|
|
1118
|
+
```json
|
|
1119
|
+
// user.json
|
|
1120
|
+
{
|
|
1121
|
+
"onDelete": {
|
|
1122
|
+
"clean": [
|
|
1123
|
+
{
|
|
1124
|
+
"ref": "Shop",
|
|
1125
|
+
"path": ["owner", "administrator"]
|
|
1126
|
+
}
|
|
1127
|
+
]
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
In this example:
|
|
1133
|
+
|
|
1134
|
+
```js
|
|
1135
|
+
user.delete();
|
|
1136
|
+
|
|
1137
|
+
// Will implicitly run:
|
|
1138
|
+
const shops = await Shop.find({
|
|
1139
|
+
$or: [
|
|
1140
|
+
{
|
|
1141
|
+
owner: user,
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
administrator: user,
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
});
|
|
1148
|
+
for (let shop of shops) {
|
|
1149
|
+
await shop.delete();
|
|
1150
|
+
}
|
|
1066
1151
|
```
|
|
1067
1152
|
|
|
1153
|
+
> [!WARNING] The ability to run an `$and` query with multiple paths is currently
|
|
1154
|
+
> not implemented.
|
|
1155
|
+
|
|
1068
1156
|
#### Erroring on Delete
|
|
1069
1157
|
|
|
1070
1158
|
The `errorOnReferenced` field helps to prevent orphaned references by defining
|
|
1071
1159
|
if and how the `delete` method will error if it is being referenced by another
|
|
1072
|
-
foreign document. In the
|
|
1160
|
+
foreign document. In the top example:
|
|
1073
1161
|
|
|
1074
1162
|
```js
|
|
1075
1163
|
user.delete();
|
|
@@ -1149,7 +1237,6 @@ const { createTestModel } = require('@bedrockio/model');
|
|
|
1149
1237
|
const User = createTestModel({
|
|
1150
1238
|
name: 'String',
|
|
1151
1239
|
});
|
|
1152
|
-
mk;
|
|
1153
1240
|
```
|
|
1154
1241
|
|
|
1155
1242
|
Note that a unique model name will be generated to prevent clashing with other
|
package/dist/cjs/delete-hooks.js
CHANGED
|
@@ -19,9 +19,8 @@ function applyDeleteHooks(schema, definition) {
|
|
|
19
19
|
if (!deleteHooks) {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
const cleanLocal = validateCleanLocal(deleteHooks, schema);
|
|
23
|
-
const cleanForeign = validateCleanForeign(deleteHooks);
|
|
24
22
|
const errorHook = validateError(deleteHooks);
|
|
23
|
+
const cleanHooks = validateCleanHooks(deleteHooks, schema);
|
|
25
24
|
let references;
|
|
26
25
|
const deleteFn = schema.methods.delete;
|
|
27
26
|
const restoreFn = schema.methods.restore;
|
|
@@ -30,23 +29,20 @@ function applyDeleteHooks(schema, definition) {
|
|
|
30
29
|
references ||= getAllReferences(this);
|
|
31
30
|
await errorOnForeignReferences(this, {
|
|
32
31
|
errorHook,
|
|
33
|
-
|
|
32
|
+
cleanHooks,
|
|
34
33
|
references
|
|
35
34
|
});
|
|
36
35
|
}
|
|
37
36
|
try {
|
|
38
|
-
await
|
|
39
|
-
await deleteForeignReferences(this, cleanForeign);
|
|
37
|
+
await deleteReferences(this, cleanHooks);
|
|
40
38
|
} catch (error) {
|
|
41
|
-
await
|
|
42
|
-
await restoreForeignReferences(this);
|
|
39
|
+
await restoreReferences(this, cleanHooks);
|
|
43
40
|
throw error;
|
|
44
41
|
}
|
|
45
42
|
await deleteFn.apply(this, arguments);
|
|
46
43
|
});
|
|
47
44
|
schema.method('restore', async function () {
|
|
48
|
-
await
|
|
49
|
-
await restoreForeignReferences(this);
|
|
45
|
+
await restoreReferences(this, cleanHooks);
|
|
50
46
|
await restoreFn.apply(this, arguments);
|
|
51
47
|
});
|
|
52
48
|
schema.add({
|
|
@@ -59,49 +55,58 @@ function applyDeleteHooks(schema, definition) {
|
|
|
59
55
|
|
|
60
56
|
// Clean Hook
|
|
61
57
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} = deleteHooks
|
|
66
|
-
if (!
|
|
67
|
-
return;
|
|
58
|
+
function validateCleanHooks(deleteHooks, schema) {
|
|
59
|
+
const {
|
|
60
|
+
clean
|
|
61
|
+
} = deleteHooks;
|
|
62
|
+
if (!clean) {
|
|
63
|
+
return [];
|
|
68
64
|
}
|
|
69
|
-
if (
|
|
70
|
-
throw new Error('
|
|
65
|
+
if (!Array.isArray(clean)) {
|
|
66
|
+
throw new Error('Delete clean hook must be an array.');
|
|
71
67
|
}
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
for (let hook of clean) {
|
|
69
|
+
const {
|
|
70
|
+
ref,
|
|
71
|
+
path,
|
|
72
|
+
paths
|
|
73
|
+
} = hook;
|
|
74
|
+
if (path && typeof path !== 'string') {
|
|
75
|
+
throw new Error('Clean hook path must be a string.');
|
|
76
|
+
} else if (paths && !Array.isArray(paths)) {
|
|
77
|
+
throw new Error('Clean hook paths must be an array.');
|
|
78
|
+
} else if (!path && !paths) {
|
|
79
|
+
throw new Error('Clean hook must define either "path" or "paths".');
|
|
80
|
+
} else if (path && paths) {
|
|
81
|
+
throw new Error('Clean hook may not define both "path" or "paths".');
|
|
82
|
+
} else if (ref && typeof ref !== 'string') {
|
|
83
|
+
throw new Error('Clean hook ref must be a string.');
|
|
84
|
+
} else if (!ref) {
|
|
85
|
+
validateLocalCleanHook(hook, schema);
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
return clean;
|
|
89
|
+
}
|
|
90
|
+
function validateLocalCleanHook(hook, schema) {
|
|
91
|
+
const paths = getHookPaths(hook);
|
|
92
|
+
for (let path of paths) {
|
|
93
|
+
if (schema.pathType(path) !== 'real') {
|
|
94
|
+
throw new Error(`Invalid reference in local delete hook: "${path}".`);
|
|
79
95
|
}
|
|
80
96
|
}
|
|
81
|
-
return local;
|
|
82
97
|
}
|
|
83
|
-
function
|
|
98
|
+
function getHookPaths(hook) {
|
|
84
99
|
const {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (typeof arg === 'object') {
|
|
95
|
-
const {
|
|
96
|
-
$and,
|
|
97
|
-
$or
|
|
98
|
-
} = arg;
|
|
99
|
-
if ($and && $or) {
|
|
100
|
-
throw new Error(`Cannot define both $or and $and in a delete hook for model ${modelName}.`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
100
|
+
path,
|
|
101
|
+
paths
|
|
102
|
+
} = hook;
|
|
103
|
+
if (path) {
|
|
104
|
+
return [path];
|
|
105
|
+
} else if (paths) {
|
|
106
|
+
return paths;
|
|
107
|
+
} else {
|
|
108
|
+
return [];
|
|
103
109
|
}
|
|
104
|
-
return foreign;
|
|
105
110
|
}
|
|
106
111
|
function validateError(deleteHooks) {
|
|
107
112
|
let {
|
|
@@ -173,15 +178,21 @@ async function errorOnForeignReferences(doc, options) {
|
|
|
173
178
|
}
|
|
174
179
|
function referenceIsAllowed(model, options) {
|
|
175
180
|
const {
|
|
176
|
-
|
|
181
|
+
modelName
|
|
182
|
+
} = model;
|
|
183
|
+
const {
|
|
184
|
+
cleanHooks
|
|
177
185
|
} = options;
|
|
186
|
+
const hasCleanHook = cleanHooks.some(hook => {
|
|
187
|
+
return hook.ref === modelName;
|
|
188
|
+
});
|
|
189
|
+
if (hasCleanHook) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
178
192
|
const {
|
|
179
193
|
only,
|
|
180
194
|
except
|
|
181
195
|
} = options?.errorHook || {};
|
|
182
|
-
if (model.modelName in cleanForeign) {
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
196
|
if (only) {
|
|
186
197
|
return !only.includes(model.modelName);
|
|
187
198
|
} else if (except) {
|
|
@@ -235,58 +246,61 @@ function getModelReferences(model, targetName) {
|
|
|
235
246
|
return paths;
|
|
236
247
|
}
|
|
237
248
|
|
|
238
|
-
//
|
|
249
|
+
// Delete
|
|
239
250
|
|
|
240
|
-
async function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const value = doc.get(name);
|
|
247
|
-
if (!value) {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
const arr = Array.isArray(value) ? value : [value];
|
|
251
|
-
for (let sub of arr) {
|
|
252
|
-
await sub.delete();
|
|
251
|
+
async function deleteReferences(doc, hooks) {
|
|
252
|
+
for (let hook of hooks) {
|
|
253
|
+
if (hook.ref) {
|
|
254
|
+
await deleteForeignReferences(doc, hook);
|
|
255
|
+
} else {
|
|
256
|
+
await deleteLocalReferences(doc, hook);
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
}
|
|
256
|
-
async function deleteForeignReferences(doc,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
async function deleteForeignReferences(doc, hook) {
|
|
261
|
+
const {
|
|
262
|
+
ref,
|
|
263
|
+
path,
|
|
264
|
+
paths,
|
|
265
|
+
query
|
|
266
|
+
} = hook;
|
|
260
267
|
const {
|
|
261
268
|
id
|
|
262
269
|
} = doc;
|
|
263
270
|
if (!id) {
|
|
264
271
|
throw new Error(`Refusing to apply delete hook to document without id.`);
|
|
265
272
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
273
|
+
const Model = _mongoose.default.models[ref];
|
|
274
|
+
if (!Model) {
|
|
275
|
+
throw new Error(`Unknown model: "${ref}".`);
|
|
276
|
+
}
|
|
277
|
+
if (path) {
|
|
278
|
+
await runDeletes(Model, doc, {
|
|
279
|
+
...query,
|
|
280
|
+
[path]: id
|
|
281
|
+
});
|
|
282
|
+
} else if (paths) {
|
|
283
|
+
await runDeletes(Model, doc, {
|
|
284
|
+
$or: paths.map(refName => {
|
|
285
|
+
return {
|
|
286
|
+
...query,
|
|
287
|
+
[refName]: id
|
|
288
|
+
};
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function deleteLocalReferences(doc, hook) {
|
|
294
|
+
const paths = getHookPaths(hook);
|
|
295
|
+
await doc.populate(paths);
|
|
296
|
+
for (let path of paths) {
|
|
297
|
+
const value = doc.get(path);
|
|
298
|
+
if (!value) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
302
|
+
for (let sub of arr) {
|
|
303
|
+
await sub.delete();
|
|
290
304
|
}
|
|
291
305
|
}
|
|
292
306
|
}
|
|
@@ -300,27 +314,25 @@ async function runDeletes(Model, refDoc, query) {
|
|
|
300
314
|
});
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
|
-
function mapArrayQuery(arr, id) {
|
|
304
|
-
return arr.map(refName => {
|
|
305
|
-
return {
|
|
306
|
-
[refName]: id
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
317
|
|
|
311
318
|
// Restore
|
|
312
319
|
|
|
313
|
-
async function
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
async function restoreReferences(doc, hooks) {
|
|
321
|
+
for (let hook of hooks) {
|
|
322
|
+
if (hook.ref) {
|
|
323
|
+
await restoreForeignReferences(doc);
|
|
324
|
+
} else {
|
|
325
|
+
await restoreLocalReferences(doc, hook);
|
|
326
|
+
}
|
|
316
327
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
328
|
+
}
|
|
329
|
+
async function restoreForeignReferences(refDoc) {
|
|
330
|
+
const grouped = (0, _lodash.groupBy)(refDoc.deletedRefs, 'ref');
|
|
331
|
+
for (let [modelName, refs] of Object.entries(grouped)) {
|
|
332
|
+
const ids = refs.map(ref => {
|
|
333
|
+
return ref._id;
|
|
334
|
+
});
|
|
335
|
+
const Model = _mongoose.default.models[modelName];
|
|
324
336
|
|
|
325
337
|
// @ts-ignore
|
|
326
338
|
const docs = await Model.findDeleted({
|
|
@@ -332,14 +344,17 @@ async function restoreLocalReferences(refDoc, arr) {
|
|
|
332
344
|
await doc.restore();
|
|
333
345
|
}
|
|
334
346
|
}
|
|
347
|
+
refDoc.deletedRefs = [];
|
|
335
348
|
}
|
|
336
|
-
async function
|
|
337
|
-
const
|
|
338
|
-
for (let
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
const
|
|
349
|
+
async function restoreLocalReferences(refDoc, hook) {
|
|
350
|
+
const paths = getHookPaths(hook);
|
|
351
|
+
for (let path of paths) {
|
|
352
|
+
const {
|
|
353
|
+
ref
|
|
354
|
+
} = (0, _utils.getInnerField)(refDoc.constructor.schema.obj, path);
|
|
355
|
+
const value = refDoc.get(path);
|
|
356
|
+
const ids = Array.isArray(value) ? value : [value];
|
|
357
|
+
const Model = _mongoose.default.models[ref];
|
|
343
358
|
|
|
344
359
|
// @ts-ignore
|
|
345
360
|
const docs = await Model.findDeleted({
|
|
@@ -351,5 +366,4 @@ async function restoreForeignReferences(refDoc) {
|
|
|
351
366
|
await doc.restore();
|
|
352
367
|
}
|
|
353
368
|
}
|
|
354
|
-
refDoc.deletedRefs = [];
|
|
355
369
|
}
|
package/package.json
CHANGED
package/src/delete-hooks.js
CHANGED
|
@@ -13,9 +13,8 @@ export function applyDeleteHooks(schema, definition) {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const cleanLocal = validateCleanLocal(deleteHooks, schema);
|
|
17
|
-
const cleanForeign = validateCleanForeign(deleteHooks);
|
|
18
16
|
const errorHook = validateError(deleteHooks);
|
|
17
|
+
const cleanHooks = validateCleanHooks(deleteHooks, schema);
|
|
19
18
|
|
|
20
19
|
let references;
|
|
21
20
|
|
|
@@ -27,24 +26,21 @@ export function applyDeleteHooks(schema, definition) {
|
|
|
27
26
|
references ||= getAllReferences(this);
|
|
28
27
|
await errorOnForeignReferences(this, {
|
|
29
28
|
errorHook,
|
|
30
|
-
|
|
29
|
+
cleanHooks,
|
|
31
30
|
references,
|
|
32
31
|
});
|
|
33
32
|
}
|
|
34
33
|
try {
|
|
35
|
-
await
|
|
36
|
-
await deleteForeignReferences(this, cleanForeign);
|
|
34
|
+
await deleteReferences(this, cleanHooks);
|
|
37
35
|
} catch (error) {
|
|
38
|
-
await
|
|
39
|
-
await restoreForeignReferences(this);
|
|
36
|
+
await restoreReferences(this, cleanHooks);
|
|
40
37
|
throw error;
|
|
41
38
|
}
|
|
42
39
|
await deleteFn.apply(this, arguments);
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
schema.method('restore', async function () {
|
|
46
|
-
await
|
|
47
|
-
await restoreForeignReferences(this);
|
|
43
|
+
await restoreReferences(this, cleanHooks);
|
|
48
44
|
await restoreFn.apply(this, arguments);
|
|
49
45
|
});
|
|
50
46
|
|
|
@@ -60,45 +56,53 @@ export function applyDeleteHooks(schema, definition) {
|
|
|
60
56
|
|
|
61
57
|
// Clean Hook
|
|
62
58
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
if (!
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (typeof local !== 'string' && !Array.isArray(local)) {
|
|
69
|
-
throw new Error('Local delete hook must be an array.');
|
|
59
|
+
function validateCleanHooks(deleteHooks, schema) {
|
|
60
|
+
const { clean } = deleteHooks;
|
|
61
|
+
if (!clean) {
|
|
62
|
+
return [];
|
|
70
63
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
64
|
+
if (!Array.isArray(clean)) {
|
|
65
|
+
throw new Error('Delete clean hook must be an array.');
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
|
|
68
|
+
for (let hook of clean) {
|
|
69
|
+
const { ref, path, paths } = hook;
|
|
70
|
+
if (path && typeof path !== 'string') {
|
|
71
|
+
throw new Error('Clean hook path must be a string.');
|
|
72
|
+
} else if (paths && !Array.isArray(paths)) {
|
|
73
|
+
throw new Error('Clean hook paths must be an array.');
|
|
74
|
+
} else if (!path && !paths) {
|
|
75
|
+
throw new Error('Clean hook must define either "path" or "paths".');
|
|
76
|
+
} else if (path && paths) {
|
|
77
|
+
throw new Error('Clean hook may not define both "path" or "paths".');
|
|
78
|
+
} else if (ref && typeof ref !== 'string') {
|
|
79
|
+
throw new Error('Clean hook ref must be a string.');
|
|
80
|
+
} else if (!ref) {
|
|
81
|
+
validateLocalCleanHook(hook, schema);
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
|
|
85
|
+
return clean;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
function
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (typeof foreign !== 'object') {
|
|
89
|
-
throw new Error('Foreign delete hook must be an object.');
|
|
90
|
-
}
|
|
91
|
-
for (let [modelName, arg] of Object.entries(foreign)) {
|
|
92
|
-
if (typeof arg === 'object') {
|
|
93
|
-
const { $and, $or } = arg;
|
|
94
|
-
if ($and && $or) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Cannot define both $or and $and in a delete hook for model ${modelName}.`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
88
|
+
function validateLocalCleanHook(hook, schema) {
|
|
89
|
+
const paths = getHookPaths(hook);
|
|
90
|
+
for (let path of paths) {
|
|
91
|
+
if (schema.pathType(path) !== 'real') {
|
|
92
|
+
throw new Error(`Invalid reference in local delete hook: "${path}".`);
|
|
99
93
|
}
|
|
100
94
|
}
|
|
101
|
-
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getHookPaths(hook) {
|
|
98
|
+
const { path, paths } = hook;
|
|
99
|
+
if (path) {
|
|
100
|
+
return [path];
|
|
101
|
+
} else if (paths) {
|
|
102
|
+
return paths;
|
|
103
|
+
} else {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
function validateError(deleteHooks) {
|
|
@@ -170,11 +174,17 @@ async function errorOnForeignReferences(doc, options) {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
function referenceIsAllowed(model, options) {
|
|
173
|
-
const {
|
|
174
|
-
const {
|
|
175
|
-
|
|
177
|
+
const { modelName } = model;
|
|
178
|
+
const { cleanHooks } = options;
|
|
179
|
+
|
|
180
|
+
const hasCleanHook = cleanHooks.some((hook) => {
|
|
181
|
+
return hook.ref === modelName;
|
|
182
|
+
});
|
|
183
|
+
if (hasCleanHook) {
|
|
176
184
|
return true;
|
|
177
185
|
}
|
|
186
|
+
|
|
187
|
+
const { only, except } = options?.errorHook || {};
|
|
178
188
|
if (only) {
|
|
179
189
|
return !only.includes(model.modelName);
|
|
180
190
|
} else if (except) {
|
|
@@ -227,16 +237,54 @@ function getModelReferences(model, targetName) {
|
|
|
227
237
|
return paths;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
//
|
|
240
|
+
// Delete
|
|
231
241
|
|
|
232
|
-
async function
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
async function deleteReferences(doc, hooks) {
|
|
243
|
+
for (let hook of hooks) {
|
|
244
|
+
if (hook.ref) {
|
|
245
|
+
await deleteForeignReferences(doc, hook);
|
|
246
|
+
} else {
|
|
247
|
+
await deleteLocalReferences(doc, hook);
|
|
248
|
+
}
|
|
235
249
|
}
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function deleteForeignReferences(doc, hook) {
|
|
253
|
+
const { ref, path, paths, query } = hook;
|
|
254
|
+
|
|
255
|
+
const { id } = doc;
|
|
256
|
+
if (!id) {
|
|
257
|
+
throw new Error(`Refusing to apply delete hook to document without id.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const Model = mongoose.models[ref];
|
|
261
|
+
if (!Model) {
|
|
262
|
+
throw new Error(`Unknown model: "${ref}".`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (path) {
|
|
266
|
+
await runDeletes(Model, doc, {
|
|
267
|
+
...query,
|
|
268
|
+
[path]: id,
|
|
269
|
+
});
|
|
270
|
+
} else if (paths) {
|
|
271
|
+
await runDeletes(Model, doc, {
|
|
272
|
+
$or: paths.map((refName) => {
|
|
273
|
+
return {
|
|
274
|
+
...query,
|
|
275
|
+
[refName]: id,
|
|
276
|
+
};
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function deleteLocalReferences(doc, hook) {
|
|
283
|
+
const paths = getHookPaths(hook);
|
|
284
|
+
await doc.populate(paths);
|
|
285
|
+
for (let path of paths) {
|
|
286
|
+
const value = doc.get(path);
|
|
238
287
|
|
|
239
|
-
const value = doc.get(name);
|
|
240
288
|
if (!value) {
|
|
241
289
|
continue;
|
|
242
290
|
}
|
|
@@ -248,39 +296,6 @@ async function deleteLocalReferences(doc, arr) {
|
|
|
248
296
|
}
|
|
249
297
|
}
|
|
250
298
|
|
|
251
|
-
async function deleteForeignReferences(doc, refs) {
|
|
252
|
-
if (!refs) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
const { id } = doc;
|
|
256
|
-
if (!id) {
|
|
257
|
-
throw new Error(`Refusing to apply delete hook to document without id.`);
|
|
258
|
-
}
|
|
259
|
-
for (let [modelName, arg] of Object.entries(refs)) {
|
|
260
|
-
const Model = mongoose.models[modelName];
|
|
261
|
-
if (typeof arg === 'string') {
|
|
262
|
-
await runDeletes(Model, doc, {
|
|
263
|
-
[arg]: id,
|
|
264
|
-
});
|
|
265
|
-
} else if (Array.isArray(arg)) {
|
|
266
|
-
await runDeletes(Model, doc, {
|
|
267
|
-
$or: mapArrayQuery(arg, id),
|
|
268
|
-
});
|
|
269
|
-
} else {
|
|
270
|
-
const { $and, $or } = arg;
|
|
271
|
-
if ($and) {
|
|
272
|
-
await runDeletes(Model, doc, {
|
|
273
|
-
$and: mapArrayQuery($and, id),
|
|
274
|
-
});
|
|
275
|
-
} else if ($or) {
|
|
276
|
-
await runDeletes(Model, doc, {
|
|
277
|
-
$or: mapArrayQuery($or, id),
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
299
|
async function runDeletes(Model, refDoc, query) {
|
|
285
300
|
const docs = await Model.find(query);
|
|
286
301
|
for (let doc of docs) {
|
|
@@ -292,33 +307,14 @@ async function runDeletes(Model, refDoc, query) {
|
|
|
292
307
|
}
|
|
293
308
|
}
|
|
294
309
|
|
|
295
|
-
function mapArrayQuery(arr, id) {
|
|
296
|
-
return arr.map((refName) => {
|
|
297
|
-
return {
|
|
298
|
-
[refName]: id,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
310
|
// Restore
|
|
304
311
|
|
|
305
|
-
async function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const value = refDoc.get(name);
|
|
312
|
-
const ids = Array.isArray(value) ? value : [value];
|
|
313
|
-
const Model = mongoose.models[ref];
|
|
314
|
-
|
|
315
|
-
// @ts-ignore
|
|
316
|
-
const docs = await Model.findDeleted({
|
|
317
|
-
_id: { $in: ids },
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
for (let doc of docs) {
|
|
321
|
-
await doc.restore();
|
|
312
|
+
async function restoreReferences(doc, hooks) {
|
|
313
|
+
for (let hook of hooks) {
|
|
314
|
+
if (hook.ref) {
|
|
315
|
+
await restoreForeignReferences(doc);
|
|
316
|
+
} else {
|
|
317
|
+
await restoreLocalReferences(doc, hook);
|
|
322
318
|
}
|
|
323
319
|
}
|
|
324
320
|
}
|
|
@@ -344,3 +340,23 @@ async function restoreForeignReferences(refDoc) {
|
|
|
344
340
|
|
|
345
341
|
refDoc.deletedRefs = [];
|
|
346
342
|
}
|
|
343
|
+
|
|
344
|
+
async function restoreLocalReferences(refDoc, hook) {
|
|
345
|
+
const paths = getHookPaths(hook);
|
|
346
|
+
|
|
347
|
+
for (let path of paths) {
|
|
348
|
+
const { ref } = getInnerField(refDoc.constructor.schema.obj, path);
|
|
349
|
+
const value = refDoc.get(path);
|
|
350
|
+
const ids = Array.isArray(value) ? value : [value];
|
|
351
|
+
const Model = mongoose.models[ref];
|
|
352
|
+
|
|
353
|
+
// @ts-ignore
|
|
354
|
+
const docs = await Model.findDeleted({
|
|
355
|
+
_id: { $in: ids },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
for (let doc of docs) {
|
|
359
|
+
await doc.restore();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,
|
|
1
|
+
{"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,qEA8CC"}
|