@bedrockio/model 0.2.16 → 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 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
- "local": "profile",
1024
- "foreign": {
1025
- "Shop": "owner"
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
- `clean.local` specifies local refs to delete. It may be a string or array of
1043
- strings. In the above example:
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 Reference Cleanup
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
- `clean.foreign` specifies foreign refs to delete. It is defined as an object
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 shop = await Shop.find({
1101
+ const shops = await Shop.find({
1102
+ status: 'active',
1063
1103
  owner: user,
1064
1104
  });
1065
- await shop.delete();
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 above example:
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
@@ -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
- cleanForeign,
32
+ cleanHooks,
34
33
  references
35
34
  });
36
35
  }
37
36
  try {
38
- await deleteLocalReferences(this, cleanLocal);
39
- await deleteForeignReferences(this, cleanForeign);
37
+ await deleteReferences(this, cleanHooks);
40
38
  } catch (error) {
41
- await restoreLocalReferences(this, cleanLocal);
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 restoreLocalReferences(this, cleanLocal);
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 validateCleanLocal(deleteHooks, schema) {
63
- let {
64
- local
65
- } = deleteHooks.clean || {};
66
- if (!local) {
67
- return;
58
+ function validateCleanHooks(deleteHooks, schema) {
59
+ const {
60
+ clean
61
+ } = deleteHooks;
62
+ if (!clean) {
63
+ return [];
68
64
  }
69
- if (typeof local !== 'string' && !Array.isArray(local)) {
70
- throw new Error('Local delete hook must be an array.');
65
+ if (!Array.isArray(clean)) {
66
+ throw new Error('Delete clean hook must be an array.');
71
67
  }
72
- if (typeof local === 'string') {
73
- local = [local];
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
- for (let name of local) {
76
- const pathType = schema.pathType(name);
77
- if (pathType !== 'real') {
78
- throw new Error(`Delete hook has invalid local reference "${name}".`);
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 validateCleanForeign(deleteHooks) {
98
+ function getHookPaths(hook) {
84
99
  const {
85
- foreign
86
- } = deleteHooks.clean || {};
87
- if (!foreign) {
88
- return;
89
- }
90
- if (typeof foreign !== 'object') {
91
- throw new Error('Foreign delete hook must be an object.');
92
- }
93
- for (let [modelName, arg] of Object.entries(foreign)) {
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
- cleanForeign = {}
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
- // Deletion
249
+ // Delete
239
250
 
240
- async function deleteLocalReferences(doc, arr) {
241
- if (!arr) {
242
- return;
243
- }
244
- for (let name of arr) {
245
- await doc.populate(name);
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, refs) {
257
- if (!refs) {
258
- return;
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
- for (let [modelName, arg] of Object.entries(refs)) {
267
- const Model = _mongoose.default.models[modelName];
268
- if (typeof arg === 'string') {
269
- await runDeletes(Model, doc, {
270
- [arg]: id
271
- });
272
- } else if (Array.isArray(arg)) {
273
- await runDeletes(Model, doc, {
274
- $or: mapArrayQuery(arg, id)
275
- });
276
- } else {
277
- const {
278
- $and,
279
- $or
280
- } = arg;
281
- if ($and) {
282
- await runDeletes(Model, doc, {
283
- $and: mapArrayQuery($and, id)
284
- });
285
- } else if ($or) {
286
- await runDeletes(Model, doc, {
287
- $or: mapArrayQuery($or, id)
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 restoreLocalReferences(refDoc, arr) {
314
- if (!arr) {
315
- return;
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
- for (let name of arr) {
318
- const {
319
- ref
320
- } = (0, _utils.getInnerField)(refDoc.constructor.schema.obj, name);
321
- const value = refDoc.get(name);
322
- const ids = Array.isArray(value) ? value : [value];
323
- const Model = _mongoose.default.models[ref];
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 restoreForeignReferences(refDoc) {
337
- const grouped = (0, _lodash.groupBy)(refDoc.deletedRefs, 'ref');
338
- for (let [modelName, refs] of Object.entries(grouped)) {
339
- const ids = refs.map(ref => {
340
- return ref._id;
341
- });
342
- const Model = _mongoose.default.models[modelName];
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
  }
@@ -26,6 +26,23 @@ function transformField(obj, field, options) {
26
26
  } else if ((0, _lodash.isPlainObject)(obj)) {
27
27
  for (let [key, val] of Object.entries(obj)) {
28
28
  if (!isAllowedField(key, field, options)) {
29
+ // Although the "id" field is automatically added for most
30
+ // documents, this doesn't appear to be the case for mongoose
31
+ // schemas with a "type" field that is an array. For example:
32
+ //
33
+ // "type": [
34
+ // {
35
+ // "name": "String"
36
+ // }
37
+ // ]
38
+ //
39
+ // This may be a mongoose bug.
40
+ // The "type": "Array" extended syntax wraps this behavior, so
41
+ // to keep consistency with other array field declaration types,
42
+ // force the "id" field to be set here.
43
+ if (key === '_id') {
44
+ obj.id = val.toString();
45
+ }
29
46
  delete obj[key];
30
47
  } else {
31
48
  transformField(val, (0, _utils.getInnerField)(field, key), options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/model",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Bedrock utilities for model creation.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
- cleanForeign,
29
+ cleanHooks,
31
30
  references,
32
31
  });
33
32
  }
34
33
  try {
35
- await deleteLocalReferences(this, cleanLocal);
36
- await deleteForeignReferences(this, cleanForeign);
34
+ await deleteReferences(this, cleanHooks);
37
35
  } catch (error) {
38
- await restoreLocalReferences(this, cleanLocal);
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 restoreLocalReferences(this, cleanLocal);
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 validateCleanLocal(deleteHooks, schema) {
64
- let { local } = deleteHooks.clean || {};
65
- if (!local) {
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 (typeof local === 'string') {
72
- local = [local];
64
+ if (!Array.isArray(clean)) {
65
+ throw new Error('Delete clean hook must be an array.');
73
66
  }
74
- for (let name of local) {
75
- const pathType = schema.pathType(name);
76
- if (pathType !== 'real') {
77
- throw new Error(`Delete hook has invalid local reference "${name}".`);
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
- return local;
84
+
85
+ return clean;
81
86
  }
82
87
 
83
- function validateCleanForeign(deleteHooks) {
84
- const { foreign } = deleteHooks.clean || {};
85
- if (!foreign) {
86
- return;
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
- return foreign;
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 { cleanForeign = {} } = options;
174
- const { only, except } = options?.errorHook || {};
175
- if (model.modelName in cleanForeign) {
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
- // Deletion
240
+ // Delete
231
241
 
232
- async function deleteLocalReferences(doc, arr) {
233
- if (!arr) {
234
- return;
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
- for (let name of arr) {
237
- await doc.populate(name);
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 restoreLocalReferences(refDoc, arr) {
306
- if (!arr) {
307
- return;
308
- }
309
- for (let name of arr) {
310
- const { ref } = getInnerField(refDoc.constructor.schema.obj, name);
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
+ }
@@ -24,6 +24,23 @@ function transformField(obj, field, options) {
24
24
  } else if (isPlainObject(obj)) {
25
25
  for (let [key, val] of Object.entries(obj)) {
26
26
  if (!isAllowedField(key, field, options)) {
27
+ // Although the "id" field is automatically added for most
28
+ // documents, this doesn't appear to be the case for mongoose
29
+ // schemas with a "type" field that is an array. For example:
30
+ //
31
+ // "type": [
32
+ // {
33
+ // "name": "String"
34
+ // }
35
+ // ]
36
+ //
37
+ // This may be a mongoose bug.
38
+ // The "type": "Array" extended syntax wraps this behavior, so
39
+ // to keep consistency with other array field declaration types,
40
+ // force the "id" field to be set here.
41
+ if (key === '_id') {
42
+ obj.id = val.toString();
43
+ }
27
44
  delete obj[key];
28
45
  } else {
29
46
  transformField(val, getInnerField(field, key), options);
@@ -1 +1 @@
1
- {"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,qEAkDC"}
1
+ {"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,qEA8CC"}