@bedrockio/model 0.1.33 → 0.2.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/dist/cjs/const.js CHANGED
@@ -4,13 +4,11 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.SEARCH_DEFAULTS = exports.POPULATE_MAX_DEPTH = void 0;
7
- const SEARCH_DEFAULTS = {
7
+ const SEARCH_DEFAULTS = exports.SEARCH_DEFAULTS = {
8
8
  limit: 50,
9
9
  sort: {
10
10
  field: 'createdAt',
11
11
  order: 'desc'
12
12
  }
13
13
  };
14
- exports.SEARCH_DEFAULTS = SEARCH_DEFAULTS;
15
- const POPULATE_MAX_DEPTH = 5;
16
- exports.POPULATE_MAX_DEPTH = POPULATE_MAX_DEPTH;
14
+ const POPULATE_MAX_DEPTH = exports.POPULATE_MAX_DEPTH = 5;
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.applyDeleteHooks = applyDeleteHooks;
7
+ var _mongoose = _interopRequireDefault(require("mongoose"));
8
+ var _lodash = require("lodash");
9
+ var _errors = require("./errors");
10
+ var _utils = require("./utils");
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ const {
13
+ ObjectId: SchemaObjectId
14
+ } = _mongoose.default.Schema.Types;
15
+ function applyDeleteHooks(schema, definition) {
16
+ let {
17
+ onDelete: deleteHooks
18
+ } = definition;
19
+ if (!deleteHooks) {
20
+ return;
21
+ }
22
+ const cleanLocal = validateCleanLocal(deleteHooks, schema);
23
+ const cleanForeign = validateCleanForeign(deleteHooks);
24
+ const errorHook = validateError(deleteHooks);
25
+ let references;
26
+ const deleteFn = schema.methods.delete;
27
+ const restoreFn = schema.methods.restore;
28
+ schema.method('delete', async function () {
29
+ if (errorHook) {
30
+ references ||= getAllReferences(this);
31
+ await errorOnForeignReferences(this, {
32
+ errorHook,
33
+ cleanForeign,
34
+ references
35
+ });
36
+ }
37
+ try {
38
+ await deleteLocalReferences(this, cleanLocal);
39
+ await deleteForeignReferences(this, cleanForeign);
40
+ } catch (error) {
41
+ await restoreLocalReferences(this, cleanLocal);
42
+ await restoreForeignReferences(this);
43
+ throw error;
44
+ }
45
+ await deleteFn.apply(this, arguments);
46
+ });
47
+ schema.method('restore', async function () {
48
+ await restoreLocalReferences(this, cleanLocal);
49
+ await restoreForeignReferences(this);
50
+ await restoreFn.apply(this, arguments);
51
+ });
52
+ schema.add({
53
+ deletedRefs: [{
54
+ _id: 'ObjectId',
55
+ ref: 'String'
56
+ }]
57
+ });
58
+ }
59
+
60
+ // Clean Hook
61
+
62
+ function validateCleanLocal(deleteHooks, schema) {
63
+ let {
64
+ local
65
+ } = deleteHooks.clean || {};
66
+ if (!local) {
67
+ return;
68
+ }
69
+ if (typeof local !== 'string' && !Array.isArray(local)) {
70
+ throw new Error('Local delete hook must be an array.');
71
+ }
72
+ if (typeof local === 'string') {
73
+ local = [local];
74
+ }
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}".`);
79
+ }
80
+ }
81
+ return local;
82
+ }
83
+ function validateCleanForeign(deleteHooks) {
84
+ 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
+ }
103
+ }
104
+ return foreign;
105
+ }
106
+ function validateError(deleteHooks) {
107
+ let {
108
+ errorOnReferenced
109
+ } = deleteHooks;
110
+ if (!errorOnReferenced) {
111
+ return;
112
+ }
113
+ if (errorOnReferenced === true) {
114
+ errorOnReferenced = {};
115
+ }
116
+ return errorOnReferenced;
117
+ }
118
+
119
+ // Error on references
120
+
121
+ async function errorOnForeignReferences(doc, options) {
122
+ const {
123
+ errorHook,
124
+ cleanForeign,
125
+ references
126
+ } = options;
127
+ if (!errorHook) {
128
+ return;
129
+ }
130
+ const {
131
+ only,
132
+ except
133
+ } = errorHook;
134
+ assertModelNames(only);
135
+ assertModelNames(except);
136
+ const results = [];
137
+ for (let {
138
+ model,
139
+ paths
140
+ } of references) {
141
+ if (referenceIsAllowed(errorHook, model)) {
142
+ continue;
143
+ }
144
+ const $or = paths.filter(path => {
145
+ if (cleanForeign) {
146
+ return cleanForeign[model.modelName] !== path;
147
+ }
148
+ return true;
149
+ }).map(path => {
150
+ return {
151
+ [path]: doc.id
152
+ };
153
+ });
154
+ if (!$or.length) {
155
+ continue;
156
+ }
157
+ const docs = await model.find({
158
+ $or
159
+ }, {
160
+ _id: 1
161
+ }).lean();
162
+ if (docs.length > 0) {
163
+ const ids = docs.map(doc => {
164
+ return String(doc._id);
165
+ });
166
+ results.push({
167
+ ids,
168
+ model,
169
+ count: ids.length
170
+ });
171
+ }
172
+ }
173
+ if (results.length) {
174
+ const {
175
+ modelName
176
+ } = doc.constructor;
177
+ const refNames = results.map(reference => {
178
+ return reference.model.modelName;
179
+ });
180
+ throw new _errors.ReferenceError(`Refusing to delete ${modelName} referenced by ${refNames}.`, results);
181
+ }
182
+ }
183
+ function referenceIsAllowed(errorHook, model) {
184
+ const {
185
+ only,
186
+ except
187
+ } = errorHook;
188
+ if (only) {
189
+ return !only.includes(model.modelName);
190
+ } else if (except) {
191
+ return except.includes(model.modelName);
192
+ } else {
193
+ return false;
194
+ }
195
+ }
196
+ function assertModelNames(arr = []) {
197
+ for (let val of arr) {
198
+ if (!_mongoose.default.models[val]) {
199
+ throw new Error(`Unknown model "${val}".`);
200
+ }
201
+ }
202
+ }
203
+ function getAllReferences(doc) {
204
+ const targetName = doc.constructor.modelName;
205
+ return Object.values(_mongoose.default.models).map(model => {
206
+ const paths = getModelReferences(model, targetName);
207
+ return {
208
+ model,
209
+ paths
210
+ };
211
+ }).filter(({
212
+ paths
213
+ }) => {
214
+ return paths.length > 0;
215
+ });
216
+ }
217
+ function getModelReferences(model, targetName) {
218
+ const paths = [];
219
+ model.schema.eachPath((schemaPath, schemaType) => {
220
+ if (schemaType instanceof SchemaObjectId && schemaPath[0] !== '_') {
221
+ const {
222
+ ref,
223
+ refPath
224
+ } = schemaType.options;
225
+ let refs;
226
+ if (ref) {
227
+ refs = [ref];
228
+ } else if (refPath) {
229
+ refs = model.schema.path(refPath).options.enum;
230
+ } else {
231
+ throw new Error(`Cannot derive refs for ${model.modelName}#${schemaPath}.`);
232
+ }
233
+ if (refs.includes(targetName)) {
234
+ paths.push(schemaPath);
235
+ }
236
+ }
237
+ });
238
+ return paths;
239
+ }
240
+
241
+ // Deletion
242
+
243
+ async function deleteLocalReferences(doc, arr) {
244
+ if (!arr) {
245
+ return;
246
+ }
247
+ for (let name of arr) {
248
+ await doc.populate(name);
249
+ const value = doc.get(name);
250
+ const arr = Array.isArray(value) ? value : [value];
251
+ for (let sub of arr) {
252
+ await sub.delete();
253
+ }
254
+ }
255
+ }
256
+ async function deleteForeignReferences(doc, refs) {
257
+ if (!refs) {
258
+ return;
259
+ }
260
+ const {
261
+ id
262
+ } = doc;
263
+ if (!id) {
264
+ throw new Error(`Refusing to apply delete hook to document without id.`);
265
+ }
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 {
273
+ const {
274
+ $and,
275
+ $or
276
+ } = arg;
277
+ if ($and) {
278
+ await runDeletes(Model, doc, {
279
+ $and: mapArrayQuery($and, id)
280
+ });
281
+ } else if ($or) {
282
+ await runDeletes(Model, doc, {
283
+ $or: mapArrayQuery($or, id)
284
+ });
285
+ }
286
+ }
287
+ }
288
+ }
289
+ async function runDeletes(Model, refDoc, query) {
290
+ const docs = await Model.find(query);
291
+ for (let doc of docs) {
292
+ await doc.delete();
293
+ refDoc.deletedRefs.push({
294
+ _id: doc.id,
295
+ ref: doc.constructor.modelName
296
+ });
297
+ }
298
+ }
299
+ function mapArrayQuery(arr, id) {
300
+ return arr.map(refName => {
301
+ return {
302
+ [refName]: id
303
+ };
304
+ });
305
+ }
306
+
307
+ // Restore
308
+
309
+ async function restoreLocalReferences(refDoc, arr) {
310
+ if (!arr) {
311
+ return;
312
+ }
313
+ for (let name of arr) {
314
+ const {
315
+ ref
316
+ } = (0, _utils.getInnerField)(refDoc.constructor.schema.obj, name);
317
+ const value = refDoc.get(name);
318
+ const ids = Array.isArray(value) ? value : [value];
319
+ const Model = _mongoose.default.models[ref];
320
+
321
+ // @ts-ignore
322
+ const docs = await Model.findDeleted({
323
+ _id: {
324
+ $in: ids
325
+ }
326
+ });
327
+ for (let doc of docs) {
328
+ await doc.restore();
329
+ }
330
+ }
331
+ }
332
+ async function restoreForeignReferences(refDoc) {
333
+ const grouped = (0, _lodash.groupBy)(refDoc.deletedRefs, 'ref');
334
+ for (let [modelName, refs] of Object.entries(grouped)) {
335
+ const ids = refs.map(ref => {
336
+ return ref._id;
337
+ });
338
+ const Model = _mongoose.default.models[modelName];
339
+
340
+ // @ts-ignore
341
+ const docs = await Model.findDeleted({
342
+ _id: {
343
+ $in: ids
344
+ }
345
+ });
346
+ for (let doc of docs) {
347
+ await doc.restore();
348
+ }
349
+ }
350
+ refDoc.deletedRefs = [];
351
+ }
@@ -7,24 +7,10 @@ exports.applyDisallowed = applyDisallowed;
7
7
  var _warn = _interopRequireDefault(require("./warn"));
8
8
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
9
9
  function applyDisallowed(schema) {
10
- schema.method('remove', function () {
11
- (0, _warn.default)('The "remove" method on documents is disallowed due to ambiguity.', 'To permanently delete a document use "destroy", otherwise "delete".');
12
- throw new Error('Method not allowed.');
13
- }, {
14
- suppressWarning: true
15
- });
16
- schema.method('update', function () {
17
- (0, _warn.default)('The "update" method on documents is deprecated. Use "updateOne" instead.');
18
- throw new Error('Method not allowed.');
19
- });
20
10
  schema.method('deleteOne', function () {
21
11
  (0, _warn.default)('The "deleteOne" method on documents is disallowed due to ambiguity', 'Use either "delete" or "deleteOne" on the model.');
22
12
  throw new Error('Method not allowed.');
23
13
  });
24
- schema.static('remove', function () {
25
- (0, _warn.default)('The "remove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "destroyMany", otherwise "deleteMany".');
26
- throw new Error('Method not allowed.');
27
- });
28
14
  schema.static('findOneAndRemove', function () {
29
15
  (0, _warn.default)('The "findOneAndRemove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".');
30
16
  throw new Error('Method not allowed.');
package/dist/cjs/env.js CHANGED
@@ -4,5 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.debug = void 0;
7
- const debug = !!process.env.DEBUG;
8
- exports.debug = debug;
7
+ const debug = exports.debug = !!process.env.DEBUG;
@@ -24,10 +24,9 @@ _mongoose.default.Query.prototype.include = function include(arg) {
24
24
  return this;
25
25
  };
26
26
  const DESCRIPTION = 'Field to be selected or populated.';
27
- const INCLUDE_FIELD_SCHEMA = _yada.default.object({
27
+ const INCLUDE_FIELD_SCHEMA = exports.INCLUDE_FIELD_SCHEMA = _yada.default.object({
28
28
  include: _yada.default.allow(_yada.default.string().description(DESCRIPTION), _yada.default.array(_yada.default.string().description(DESCRIPTION)))
29
29
  });
30
- exports.INCLUDE_FIELD_SCHEMA = INCLUDE_FIELD_SCHEMA;
31
30
  function applyInclude(schema) {
32
31
  // Query Includes
33
32
 
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.wrapQuery = wrapQuery;
7
+ // Wrapper that allows a mongoose query object to be returned
8
+ // to access chained methods while still resolving with a
9
+ // transformed value. Allows methods to behave like other find
10
+ // methods and importantly allow custom population with the same API.
11
+ function wrapQuery(query, fn) {
12
+ const runQuery = query.then.bind(query);
13
+ query.then = async (resolve, reject) => {
14
+ try {
15
+ resolve(await fn(runQuery()));
16
+ } catch (err) {
17
+ reject(err);
18
+ }
19
+ };
20
+ return query;
21
+ }
@@ -13,8 +13,8 @@ var _slug = require("./slug");
13
13
  var _search = require("./search");
14
14
  var _assign = require("./assign");
15
15
  var _include = require("./include");
16
- var _references = require("./references");
17
16
  var _softDelete = require("./soft-delete");
17
+ var _deleteHooks = require("./delete-hooks");
18
18
  var _disallowed = require("./disallowed");
19
19
  var _validation = require("./validation");
20
20
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -45,10 +45,13 @@ function createSchema(definition, options = {}) {
45
45
  toObject: _serialization.serializeOptions,
46
46
  ...options
47
47
  });
48
+
49
+ // Soft Delete needs to be applied
50
+ // first for hooks to work correctly.
51
+ (0, _softDelete.applySoftDelete)(schema);
48
52
  (0, _validation.applyValidation)(schema, definition);
53
+ (0, _deleteHooks.applyDeleteHooks)(schema, definition);
49
54
  (0, _search.applySearch)(schema, definition);
50
- (0, _softDelete.applySoftDelete)(schema);
51
- (0, _references.applyReferences)(schema);
52
55
  (0, _disallowed.applyDisallowed)(schema);
53
56
  (0, _include.applyInclude)(schema);
54
57
  (0, _assign.applyAssign)(schema);
@@ -139,9 +142,6 @@ function attributesToMongoose(attributes) {
139
142
  return definition;
140
143
  }
141
144
  function assertSchemaType(type, path) {
142
- if (type === 'Mixed') {
143
- throw new Error('Type "Mixed" is not allowed. Use "Object" instead.');
144
- }
145
145
  if (typeof type === 'string') {
146
146
  if (!isMongooseType(type)) {
147
147
  const p = path.join('.');
@@ -13,6 +13,7 @@ var _utils = require("./utils");
13
13
  var _const = require("./const");
14
14
  var _validation = require("./validation");
15
15
  var _env = require("./env");
16
+ var _query = require("./query");
16
17
  var _warn = _interopRequireDefault(require("./warn"));
17
18
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
19
  const {
@@ -49,29 +50,17 @@ function applySearch(schema, definition) {
49
50
  _logger.default.info(`Search query for ${this.modelName}:\n`, JSON.stringify(query, null, 2));
50
51
  }
51
52
  const mQuery = this.find(query).sort(resolveSort(sort, schema)).skip(skip).limit(limit);
52
-
53
- // The following construct is awkward but it allows the mongoose query
54
- // object to be returned while still ultimately resolving with metadata
55
- // so that this method can behave like other find methods and importantly
56
- // allow custom population with the same API.
57
-
58
- const runQuery = mQuery.then.bind(mQuery);
59
- mQuery.then = async (resolve, reject) => {
60
- try {
61
- const [data, total] = await Promise.all([runQuery(), this.countDocuments(query)]);
62
- resolve({
63
- data,
64
- meta: {
65
- total,
66
- skip,
67
- limit
68
- }
69
- });
70
- } catch (err) {
71
- reject(err);
72
- }
73
- };
74
- return mQuery;
53
+ return (0, _query.wrapQuery)(mQuery, async promise => {
54
+ const [data, total] = await Promise.all([promise, this.countDocuments(query)]);
55
+ return {
56
+ data,
57
+ meta: {
58
+ total,
59
+ skip,
60
+ limit
61
+ }
62
+ };
63
+ });
75
64
  });
76
65
  }
77
66
  function searchValidation(options = {}) {
@@ -8,7 +8,8 @@ var _lodash = require("lodash");
8
8
  var _include = require("./include");
9
9
  var _access = require("./access");
10
10
  var _utils = require("./utils");
11
- const serializeOptions = {
11
+ const DISALLOWED_FIELDS = ['deleted', 'deletedRefs'];
12
+ const serializeOptions = exports.serializeOptions = {
12
13
  getters: true,
13
14
  versionKey: false,
14
15
  transform: (doc, ret, options) => {
@@ -17,11 +18,12 @@ const serializeOptions = {
17
18
  transformField(ret, doc.schema.obj, options);
18
19
  }
19
20
  };
20
- exports.serializeOptions = serializeOptions;
21
21
  function transformField(obj, field, options) {
22
22
  if (Array.isArray(obj)) {
23
23
  for (let el of obj) {
24
24
  transformField(el, field, options);
25
+ // Delete ids in array elements.
26
+ delete el.id;
25
27
  }
26
28
  } else if ((0, _lodash.isPlainObject)(obj)) {
27
29
  for (let [key, val] of Object.entries(obj)) {
@@ -37,7 +39,7 @@ function isAllowedField(key, field, options) {
37
39
  if (key[0] === '_') {
38
40
  // Strip internal keys like _id and __v
39
41
  return false;
40
- } else if (key === 'deleted') {
42
+ } else if (DISALLOWED_FIELDS.includes(key)) {
41
43
  // Strip "deleted" field which defaults
42
44
  // to false and should not be exposed.
43
45
  return false;