@bedrockio/model 0.1.0 → 0.1.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.
Files changed (73) hide show
  1. package/README.md +24 -0
  2. package/dist/cjs/access.js +4 -0
  3. package/dist/cjs/assign.js +2 -3
  4. package/dist/cjs/disallowed.js +40 -0
  5. package/dist/cjs/include.js +3 -2
  6. package/dist/cjs/load.js +15 -3
  7. package/dist/cjs/schema.js +29 -12
  8. package/dist/cjs/search.js +16 -16
  9. package/dist/cjs/serialization.js +2 -3
  10. package/dist/cjs/soft-delete.js +234 -55
  11. package/dist/cjs/testing.js +8 -0
  12. package/dist/cjs/validation.js +109 -43
  13. package/package.json +9 -7
  14. package/src/access.js +3 -0
  15. package/src/disallowed.js +63 -0
  16. package/src/include.js +1 -0
  17. package/src/load.js +12 -1
  18. package/src/schema.js +25 -5
  19. package/src/search.js +21 -10
  20. package/src/soft-delete.js +238 -85
  21. package/src/testing.js +7 -0
  22. package/src/validation.js +134 -43
  23. package/types/access.d.ts +7 -0
  24. package/types/access.d.ts.map +1 -0
  25. package/types/assign.d.ts +2 -0
  26. package/types/assign.d.ts.map +1 -0
  27. package/types/const.d.ts +9 -0
  28. package/types/const.d.ts.map +1 -0
  29. package/types/disallowed.d.ts +2 -0
  30. package/types/disallowed.d.ts.map +1 -0
  31. package/types/errors.d.ts +9 -0
  32. package/types/errors.d.ts.map +1 -0
  33. package/types/include.d.ts +4 -0
  34. package/types/include.d.ts.map +1 -0
  35. package/types/index.d.ts +6 -0
  36. package/types/index.d.ts.map +1 -0
  37. package/types/load.d.ts +15 -0
  38. package/types/load.d.ts.map +1 -0
  39. package/types/references.d.ts +2 -0
  40. package/types/references.d.ts.map +1 -0
  41. package/types/schema.d.ts +71 -0
  42. package/types/schema.d.ts.map +1 -0
  43. package/types/search.d.ts +303 -0
  44. package/types/search.d.ts.map +1 -0
  45. package/types/serialization.d.ts +6 -0
  46. package/types/serialization.d.ts.map +1 -0
  47. package/types/slug.d.ts +2 -0
  48. package/types/slug.d.ts.map +1 -0
  49. package/types/soft-delete.d.ts +4 -0
  50. package/types/soft-delete.d.ts.map +1 -0
  51. package/types/testing.d.ts +11 -0
  52. package/types/testing.d.ts.map +1 -0
  53. package/types/utils.d.ts +8 -0
  54. package/types/utils.d.ts.map +1 -0
  55. package/types/validation.d.ts +13 -0
  56. package/types/validation.d.ts.map +1 -0
  57. package/types/warn.d.ts +2 -0
  58. package/types/warn.d.ts.map +1 -0
  59. package/babel.config.cjs +0 -41
  60. package/jest.config.js +0 -8
  61. package/test/assign.test.js +0 -225
  62. package/test/definitions/custom-model.json +0 -9
  63. package/test/definitions/special-category.json +0 -18
  64. package/test/include.test.js +0 -896
  65. package/test/load.test.js +0 -47
  66. package/test/references.test.js +0 -71
  67. package/test/schema.test.js +0 -919
  68. package/test/search.test.js +0 -652
  69. package/test/serialization.test.js +0 -748
  70. package/test/setup.js +0 -27
  71. package/test/slug.test.js +0 -112
  72. package/test/soft-delete.test.js +0 -333
  73. package/test/validation.test.js +0 -1925
@@ -4,9 +4,16 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.applySoftDelete = applySoftDelete;
7
- var _warn = _interopRequireDefault(require("./warn"));
8
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
7
+ exports.assertUnique = assertUnique;
8
+ exports.hasUniqueConstraints = hasUniqueConstraints;
9
9
  function applySoftDelete(schema) {
10
+ applyQueries(schema);
11
+ applyUniqueConstraints(schema);
12
+ }
13
+
14
+ // Soft Delete Querying
15
+
16
+ function applyQueries(schema) {
10
17
  // Implementation
11
18
 
12
19
  schema.pre(/^find|count|exists/, function (next) {
@@ -23,11 +30,13 @@ function applySoftDelete(schema) {
23
30
  schema.method('delete', function () {
24
31
  this.deleted = true;
25
32
  this.deletedAt = new Date();
33
+ // @ts-ignore
26
34
  return this.save();
27
35
  });
28
36
  schema.method('restore', function restore() {
29
37
  this.deleted = false;
30
38
  this.deletedAt = undefined;
39
+ // @ts-ignore
31
40
  return this.save();
32
41
  });
33
42
  schema.method('destroy', function destroy(...args) {
@@ -88,85 +97,75 @@ function applySoftDelete(schema) {
88
97
  destroyedCount: res.deletedCount
89
98
  };
90
99
  });
91
- schema.static('findDeleted', function findDeleted(filter) {
92
- return this.find({
100
+ schema.static('findDeleted', function findDeleted(filter, ...rest) {
101
+ filter = {
93
102
  ...filter,
94
103
  deleted: true
95
- });
104
+ };
105
+ return this.find(filter, ...rest);
96
106
  });
97
- schema.static('findOneDeleted', function findOneDeleted(filter) {
98
- return this.findOne({
107
+ schema.static('findOneDeleted', function findOneDeleted(filter, ...rest) {
108
+ filter = {
99
109
  ...filter,
100
110
  deleted: true
101
- });
111
+ };
112
+ return this.findOne(filter, ...rest);
102
113
  });
103
- schema.static('findByIdDeleted', function findByIdDeleted(id) {
104
- return this.findOne({
114
+ schema.static('findByIdDeleted', function findByIdDeleted(id, ...rest) {
115
+ const filter = {
105
116
  _id: id,
106
117
  deleted: true
107
- });
118
+ };
119
+ return this.findOne(filter, ...rest);
108
120
  });
109
- schema.static('existsDeleted', function existsDeleted() {
110
- return this.exists({
121
+ schema.static('existsDeleted', function existsDeleted(filter, ...rest) {
122
+ filter = {
123
+ ...filter,
111
124
  deleted: true
112
- });
125
+ };
126
+ return this.exists(filter, ...rest);
113
127
  });
114
- schema.static('countDocumentsDeleted', function countDocumentsDeleted(filter) {
115
- return this.countDocuments({
128
+ schema.static('countDocumentsDeleted', function countDocumentsDeleted(filter, ...rest) {
129
+ filter = {
116
130
  ...filter,
117
131
  deleted: true
118
- });
132
+ };
133
+ return this.countDocuments(filter, ...rest);
119
134
  });
120
- schema.static('findWithDeleted', function findWithDeleted(filter) {
121
- return this.find({
135
+ schema.static('findWithDeleted', function findWithDeleted(filter, ...rest) {
136
+ filter = {
122
137
  ...filter,
123
138
  ...getWithDeletedQuery()
124
- });
139
+ };
140
+ return this.find(filter, ...rest);
125
141
  });
126
- schema.static('findOneWithDeleted', function findOneWithDeleted(filter) {
127
- return this.findOne({
142
+ schema.static('findOneWithDeleted', function findOneWithDeleted(filter, ...rest) {
143
+ filter = {
128
144
  ...filter,
129
145
  ...getWithDeletedQuery()
130
- });
146
+ };
147
+ return this.findOne(filter, ...rest);
131
148
  });
132
- schema.static('findByIdWithDeleted', function findByIdWithDeleted(id) {
133
- return this.findOne({
149
+ schema.static('findByIdWithDeleted', function findByIdWithDeleted(id, ...rest) {
150
+ const filter = {
134
151
  _id: id,
135
152
  ...getWithDeletedQuery()
136
- });
153
+ };
154
+ return this.findOne(filter, ...rest);
137
155
  });
138
- schema.static('existsWithDeleted', function existsWithDeleted() {
139
- return this.exists({
156
+ schema.static('existsWithDeleted', function existsWithDeleted(filter, ...rest) {
157
+ filter = {
158
+ ...filter,
140
159
  ...getWithDeletedQuery()
141
- });
160
+ };
161
+ return this.exists(filter, ...rest);
142
162
  });
143
- schema.static('countDocumentsWithDeleted', function countDocumentsWithDeleted(filter) {
144
- return this.countDocuments({
163
+ schema.static('countDocumentsWithDeleted', function countDocumentsWithDeleted(filter, ...rest) {
164
+ filter = {
145
165
  ...filter,
146
166
  ...getWithDeletedQuery()
147
- });
148
- });
149
- schema.method('remove', function () {
150
- (0, _warn.default)('The "remove" method on documents is disallowed due to ambiguity.', 'To permanently delete a document use "destroy", otherwise "delete".');
151
- throw new Error('Method not allowed.');
152
- }, {
153
- suppressWarning: true
154
- });
155
- schema.method('deleteOne', function () {
156
- (0, _warn.default)('The "deleteOne" method on documents is disallowed due to ambiguity', 'Use either "delete" or "deleteOne" on the model.');
157
- throw new Error('Method not allowed.');
158
- });
159
- schema.static('remove', function () {
160
- (0, _warn.default)('The "remove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "destroyMany", otherwise "deleteMany".');
161
- throw new Error('Method not allowed.');
162
- });
163
- schema.static('findOneAndRemove', function () {
164
- (0, _warn.default)('The "findOneAndRemove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".');
165
- throw new Error('Method not allowed.');
166
- });
167
- schema.static('findByIdAndRemove', function () {
168
- (0, _warn.default)('The "findByIdAndRemove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "findByIdAndDestroy", otherwise "findByIdAndDelete".');
169
- throw new Error('Method not allowed.');
167
+ };
168
+ return this.countDocuments(filter, ...rest);
170
169
  });
171
170
  }
172
171
  function getDelete() {
@@ -189,4 +188,184 @@ function getWithDeletedQuery() {
189
188
  $in: [true, false]
190
189
  }
191
190
  };
192
- }
191
+ }
192
+
193
+ // Unique Constraints
194
+
195
+ function applyUniqueConstraints(schema) {
196
+ const hasUnique = hasUniqueConstraints(schema);
197
+ if (!hasUnique) {
198
+ return;
199
+ }
200
+ schema.pre('save', async function () {
201
+ await assertUnique(this.toObject(), {
202
+ operation: this.isNew ? 'create' : 'update',
203
+ model: this.constructor,
204
+ schema
205
+ });
206
+ });
207
+ schema.pre(/^(update|replace)/, async function () {
208
+ await assertUniqueForQuery(this, {
209
+ schema
210
+ });
211
+ });
212
+ schema.pre('insertMany', async function (next, obj) {
213
+ // Note that in order to access the objects to be inserted
214
+ // we must supply the hook with at least 2 arguments, the
215
+ // first of which is the next hook. This typically appears
216
+ // as the last argument, however as we are passing an async
217
+ // function it appears to not stop the middleware if we
218
+ // don't call it directly.
219
+ await assertUnique(obj, {
220
+ operation: 'create',
221
+ model: this,
222
+ schema
223
+ });
224
+ });
225
+ }
226
+ async function assertUnique(obj, options) {
227
+ const {
228
+ operation,
229
+ model,
230
+ schema
231
+ } = options;
232
+ const id = getId(obj);
233
+ const objFields = resolveUnique(schema, obj);
234
+ if (Object.keys(objFields).length === 0) {
235
+ return;
236
+ }
237
+ const query = {
238
+ $or: getUniqueQueries(objFields),
239
+ ...(id && {
240
+ _id: {
241
+ $ne: id
242
+ }
243
+ })
244
+ };
245
+ const found = await model.findOne(query, {}, {
246
+ lean: true
247
+ });
248
+ if (found) {
249
+ const {
250
+ modelName
251
+ } = model;
252
+ const foundFields = resolveUnique(schema, found);
253
+ const collisions = getCollisions(objFields, foundFields).join(', ');
254
+ throw new Error(`Cannot ${operation} ${modelName}. Duplicate fields exist: ${collisions}.`);
255
+ }
256
+ }
257
+ function getId(arg) {
258
+ const id = arg.id || arg._id;
259
+ return id ? String(id) : null;
260
+ }
261
+
262
+ // Asserts than an update or insert query will not
263
+ // result in duplicate unique fields being present
264
+ // within non-deleted documents.
265
+ async function assertUniqueForQuery(query, options) {
266
+ let update = query.getUpdate();
267
+ const operation = getOperationForQuery(update);
268
+ // Note: No need to check unique constraints
269
+ // if the operation is a delete.
270
+ if (operation === 'restore' || operation === 'update') {
271
+ const {
272
+ model
273
+ } = query;
274
+ const filter = query.getFilter();
275
+ if (operation === 'restore') {
276
+ // A restore operation is functionally identical to a new
277
+ // insert so we need to fetch the deleted documents with
278
+ // all fields available to check against.
279
+ const docs = await model.findWithDeleted(filter, {}, {
280
+ lean: true
281
+ });
282
+ update = docs.map(doc => {
283
+ return {
284
+ ...doc,
285
+ ...update
286
+ };
287
+ });
288
+ }
289
+ await assertUnique(update, {
290
+ ...options,
291
+ operation,
292
+ model
293
+ });
294
+ }
295
+ }
296
+ function getOperationForQuery(update) {
297
+ if (update?.deleted === false) {
298
+ return 'restore';
299
+ } else if (update?.deleted === true) {
300
+ return 'delete';
301
+ } else {
302
+ return 'update';
303
+ }
304
+ }
305
+ function hasUniqueConstraints(schema) {
306
+ const paths = [...Object.keys(schema.paths), ...Object.keys(schema.subpaths)];
307
+ return paths.some(key => {
308
+ return isUniquePath(schema, key);
309
+ });
310
+ }
311
+ function isUniquePath(schema, key) {
312
+ return schema.path(key)?.options?.softUnique === true;
313
+ }
314
+
315
+ // Returns a flattened map of key -> [...values]
316
+ // consisting of only paths defined as unique on the schema.
317
+ function resolveUnique(schema, obj, map = {}, path = []) {
318
+ if (Array.isArray(obj)) {
319
+ for (let el of obj) {
320
+ resolveUnique(schema, el, map, path);
321
+ }
322
+ } else if (obj && typeof obj === 'object') {
323
+ for (let [key, val] of Object.entries(obj)) {
324
+ resolveUnique(schema, val, map, [...path, key]);
325
+ }
326
+ } else if (obj) {
327
+ const key = path.join('.');
328
+ if (isUniquePath(schema, key)) {
329
+ map[key] ||= [];
330
+ map[key].push(obj);
331
+ }
332
+ }
333
+ return map;
334
+ }
335
+
336
+ // Argument is guaranteed to be flattened.
337
+ function getUniqueQueries(obj) {
338
+ return Object.entries(obj).map(([key, val]) => {
339
+ if (val.length > 1) {
340
+ return {
341
+ [key]: {
342
+ $in: val
343
+ }
344
+ };
345
+ } else {
346
+ return {
347
+ [key]: val[0]
348
+ };
349
+ }
350
+ });
351
+ }
352
+
353
+ // Both arguments here are guaranteed to be flattened
354
+ // maps of key -> [values] of unique fields only.
355
+ function getCollisions(obj1, obj2) {
356
+ const collisions = [];
357
+ for (let [key, arr1] of Object.entries(obj1)) {
358
+ const arr2 = obj2[key];
359
+ if (arr2) {
360
+ const hasCollision = arr1.some(val => {
361
+ return arr2.includes(val);
362
+ });
363
+ if (hasCollision) {
364
+ collisions.push(key);
365
+ }
366
+ }
367
+ }
368
+ return collisions;
369
+ }
370
+
371
+ // Disallowed Methods
@@ -10,6 +10,14 @@ var _schema = require("./schema");
10
10
  var _utils = require("./utils");
11
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
12
  let counter = 0;
13
+
14
+ /**
15
+ * Helper to quickly create models for testing.
16
+ * Accepts a definition's `attributes` object and
17
+ * an optional model name as the first argument.
18
+ * [Link](https://github.com/bedrockio/model#testing)
19
+ * @returns mongoose.Model
20
+ */
13
21
  function createTestModel(...args) {
14
22
  let modelName, attributes, schema;
15
23
  if (typeof args[0] === 'string') {
@@ -3,29 +3,47 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.OBJECT_ID_SCHEMA = void 0;
6
7
  exports.addValidators = addValidators;
7
8
  exports.applyValidation = applyValidation;
8
9
  exports.getNamedValidator = getNamedValidator;
9
10
  exports.getTupleValidator = getTupleValidator;
10
11
  exports.getValidationSchema = getValidationSchema;
11
- var _lowerFirst2 = _interopRequireDefault(require("lodash/lowerFirst"));
12
- var _omit2 = _interopRequireDefault(require("lodash/omit"));
13
- var _get2 = _interopRequireDefault(require("lodash/get"));
14
12
  var _mongoose = _interopRequireDefault(require("mongoose"));
15
13
  var _yada = _interopRequireDefault(require("@bedrockio/yada"));
14
+ var _lodash = require("lodash");
16
15
  var _access = require("./access");
17
16
  var _search = require("./search");
18
17
  var _errors = require("./errors");
18
+ var _softDelete = require("./soft-delete");
19
19
  var _utils = require("./utils");
20
20
  var _schema = require("./schema");
21
21
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22
+ const DATE_SCHEMA = _yada.default.date().iso().tag({
23
+ 'x-schema': 'DateTime',
24
+ 'x-description': 'A `string` in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format.'
25
+ });
26
+ const OBJECT_ID_DESCRIPTION = `
27
+ A 24 character hexadecimal string representing a Mongo [ObjectId](https://bit.ly/3YPtGlU).
28
+ An object with an \`id\` field may also be passed, which will be converted into a string.
29
+ `;
30
+ const OBJECT_ID_SCHEMA = _yada.default.custom(async val => {
31
+ const id = String(val.id || val);
32
+ await namedSchemas.objectId.validate(id);
33
+ return id;
34
+ }).tag({
35
+ type: 'ObjectId',
36
+ 'x-schema': 'ObjectId',
37
+ 'x-description': OBJECT_ID_DESCRIPTION.trim()
38
+ });
39
+ exports.OBJECT_ID_SCHEMA = OBJECT_ID_SCHEMA;
22
40
  const namedSchemas = {
23
41
  // Email is special as we are assuming that in
24
42
  // all cases lowercase should be allowed but coerced.
25
43
  email: _yada.default.string().lowercase().email(),
26
- // Force "ObjectId" to have parity with refs.
44
+ // Force "objectId" to have parity with refs.
27
45
  // "mongo" is notably excluded here for this reason.
28
- ObjectId: _yada.default.string().mongo(),
46
+ objectId: _yada.default.string().mongo(),
29
47
  ascii: _yada.default.string().ascii(),
30
48
  base64: _yada.default.string().base64(),
31
49
  btc: _yada.default.string().btc(),
@@ -51,35 +69,50 @@ function addValidators(schemas) {
51
69
  Object.assign(namedSchemas, schemas);
52
70
  }
53
71
  function applyValidation(schema, definition) {
72
+ const hasUnique = (0, _softDelete.hasUniqueConstraints)(schema);
54
73
  schema.static('getCreateValidation', function getCreateValidation(appendSchema) {
55
74
  return getSchemaFromMongoose(schema, {
75
+ model: this,
56
76
  appendSchema,
57
77
  stripReserved: true,
58
78
  requireWriteAccess: true,
59
- modelName: this.modelName
79
+ ...(hasUnique && {
80
+ assertUniqueOptions: {
81
+ schema,
82
+ operation: 'create'
83
+ }
84
+ })
60
85
  });
61
86
  });
62
87
  schema.static('getUpdateValidation', function getUpdateValidation(appendSchema) {
63
88
  return getSchemaFromMongoose(schema, {
89
+ model: this,
64
90
  appendSchema,
65
91
  skipRequired: true,
66
92
  stripReserved: true,
67
93
  stripUnknown: true,
68
94
  requireWriteAccess: true,
69
- modelName: this.modelName
95
+ ...(hasUnique && {
96
+ assertUniqueOptions: {
97
+ schema,
98
+ operation: 'update'
99
+ }
100
+ })
70
101
  });
71
102
  });
72
103
  schema.static('getSearchValidation', function getSearchValidation(searchOptions) {
73
104
  return getSchemaFromMongoose(schema, {
74
- allowRanges: true,
105
+ allowSearch: true,
75
106
  skipRequired: true,
76
- allowMultiple: true,
77
107
  unwindArrayFields: true,
78
108
  requireReadAccess: true,
79
109
  appendSchema: (0, _search.searchValidation)(definition, searchOptions),
80
- modelName: this.modelName
110
+ model: this
81
111
  });
82
112
  });
113
+ schema.static('getBaseSchema', function getBaseSchema() {
114
+ return getSchemaFromMongoose(schema);
115
+ });
83
116
  }
84
117
 
85
118
  // Yada schemas
@@ -89,7 +122,7 @@ function getSchemaFromMongoose(schema, options) {
89
122
  obj
90
123
  } = schema;
91
124
  if (options.stripReserved) {
92
- obj = (0, _omit2.default)(obj, _schema.RESERVED_FIELDS);
125
+ obj = (0, _lodash.omit)(obj, _schema.RESERVED_FIELDS);
93
126
  }
94
127
  return getValidationSchema(obj, options);
95
128
  }
@@ -97,9 +130,18 @@ function getSchemaFromMongoose(schema, options) {
97
130
  // Exported for testing
98
131
  function getValidationSchema(attributes, options = {}) {
99
132
  const {
100
- appendSchema
133
+ appendSchema,
134
+ assertUniqueOptions
101
135
  } = options;
102
136
  let schema = getObjectSchema(attributes, options);
137
+ if (assertUniqueOptions) {
138
+ schema = _yada.default.custom(async obj => {
139
+ await (0, _softDelete.assertUnique)(obj, {
140
+ model: options.model,
141
+ ...assertUniqueOptions
142
+ });
143
+ });
144
+ }
103
145
  if (appendSchema) {
104
146
  schema = schema.append(appendSchema);
105
147
  }
@@ -164,6 +206,8 @@ function getSchemaForTypedef(typedef, options = {}) {
164
206
  }
165
207
  if (typedef.validate?.schema) {
166
208
  schema = schema.append(typedef.validate.schema);
209
+ } else if (typeof typedef.validate === 'function') {
210
+ schema = schema.custom(wrapMongooseValidator(typedef.validate));
167
211
  }
168
212
  if (typedef.enum) {
169
213
  schema = schema.allow(...typedef.enum);
@@ -177,11 +221,8 @@ function getSchemaForTypedef(typedef, options = {}) {
177
221
  if (typedef.max != null || typedef.maxLength != null) {
178
222
  schema = schema.max(typedef.max ?? typedef.maxLength);
179
223
  }
180
- if (options.allowRanges) {
181
- schema = getRangeSchema(schema, type);
182
- }
183
- if (options.allowMultiple) {
184
- schema = _yada.default.allow(schema, _yada.default.array(schema));
224
+ if (options.allowSearch) {
225
+ schema = getSearchSchema(schema, type);
185
226
  }
186
227
  if (typedef.readAccess && options.requireReadAccess) {
187
228
  schema = validateReadAccess(schema, typedef.readAccess, options);
@@ -200,39 +241,56 @@ function getSchemaForType(type) {
200
241
  case 'Boolean':
201
242
  return _yada.default.boolean();
202
243
  case 'Date':
203
- return _yada.default.date().iso();
244
+ return DATE_SCHEMA;
204
245
  case 'Mixed':
205
246
  case 'Object':
206
247
  return _yada.default.object();
207
248
  case 'Array':
208
249
  return _yada.default.array();
209
250
  case 'ObjectId':
210
- return _yada.default.custom(async val => {
211
- const id = String(val.id || val);
212
- await namedSchemas['ObjectId'].validate(id);
213
- return id;
214
- });
251
+ return OBJECT_ID_SCHEMA;
215
252
  default:
216
253
  throw new TypeError(`Unknown schema type ${type}`);
217
254
  }
218
255
  }
219
- function getRangeSchema(schema, type) {
256
+ function getSearchSchema(schema, type) {
220
257
  if (type === 'Number') {
221
- schema = _yada.default.allow(schema, _yada.default.object({
222
- lt: _yada.default.number(),
223
- gt: _yada.default.number(),
224
- lte: _yada.default.number(),
225
- gte: _yada.default.number()
226
- }));
258
+ return _yada.default.allow(schema, _yada.default.array(schema), _yada.default.object({
259
+ lt: _yada.default.number().description('Select values less than.'),
260
+ gt: _yada.default.number().description('Select values greater than.'),
261
+ lte: _yada.default.number().description('Select values less than or equal.'),
262
+ gte: _yada.default.number().description('Select values greater than or equal.')
263
+ }).tag({
264
+ 'x-schema': 'NumberRange',
265
+ 'x-description': 'An object representing numbers falling within a range.'
266
+ })).description('Allows searching by a value, array of values, or a numeric range.');
227
267
  } else if (type === 'Date') {
228
- return _yada.default.allow(schema, _yada.default.object({
229
- lt: _yada.default.date().iso(),
230
- gt: _yada.default.date().iso(),
231
- lte: _yada.default.date().iso(),
232
- gte: _yada.default.date().iso()
233
- }));
268
+ return _yada.default.allow(schema, _yada.default.array(schema), _yada.default.object({
269
+ lt: _yada.default.date().iso().tag({
270
+ 'x-ref': 'DateTime',
271
+ description: 'Select dates occurring before.'
272
+ }),
273
+ gt: _yada.default.date().iso().tag({
274
+ 'x-ref': 'DateTime',
275
+ description: 'Select dates occurring after.'
276
+ }),
277
+ lte: _yada.default.date().iso().tag({
278
+ 'x-ref': 'DateTime',
279
+ description: 'Select dates occurring on or before.'
280
+ }),
281
+ gte: _yada.default.date().iso().tag({
282
+ 'x-ref': 'DateTime',
283
+ description: 'Select dates occurring on or after.'
284
+ })
285
+ }).tag({
286
+ 'x-schema': 'DateRange',
287
+ 'x-description': 'An object representing dates falling within a range.'
288
+ })).description('Allows searching by a date, array of dates, or a range.');
289
+ } else if (type === 'String' || type === 'ObjectId') {
290
+ return _yada.default.allow(schema, _yada.default.array(schema));
291
+ } else {
292
+ return schema;
234
293
  }
235
- return schema;
236
294
  }
237
295
  function isRequired(typedef, options) {
238
296
  return typedef.required && !typedef.default && !options.skipRequired;
@@ -256,15 +314,15 @@ function validateWriteAccess(schema, allowed, options) {
256
314
  function validateAccess(type, schema, allowed, options) {
257
315
  const {
258
316
  modelName
259
- } = options;
317
+ } = options.model;
260
318
  return schema.custom((val, options) => {
261
- const document = options[(0, _lowerFirst2.default)(modelName)] || options['document'];
319
+ const document = options[(0, _lodash.lowerFirst)(modelName)] || options['document'];
262
320
  const isAllowed = (0, _access.hasAccess)(type, allowed, {
263
321
  ...options,
264
322
  document
265
323
  });
266
324
  if (!isAllowed) {
267
- const currentValue = (0, _get2.default)(document, options.path);
325
+ const currentValue = (0, _lodash.get)(document, options.path);
268
326
  if (val !== currentValue) {
269
327
  throw new _errors.PermissionsError('requires write permissions.');
270
328
  }
@@ -275,13 +333,13 @@ function validateAccess(type, schema, allowed, options) {
275
333
  // Mongoose Validators
276
334
 
277
335
  function getNamedValidator(name) {
278
- return wrapMongooseValidator(getNamedSchema(name));
336
+ return wrapSchemaAsValidator(getNamedSchema(name));
279
337
  }
280
338
  function getTupleValidator(types) {
281
339
  types = types.map(type => {
282
340
  return getSchemaForTypedef(type);
283
341
  });
284
- return wrapMongooseValidator(_yada.default.array(types).length(types.length));
342
+ return wrapSchemaAsValidator(_yada.default.array(types).length(types.length));
285
343
  }
286
344
 
287
345
  // Returns an async function that will error on failure.
@@ -297,13 +355,21 @@ function getTupleValidator(types) {
297
355
  // the first style here.
298
356
  //
299
357
  // https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-validate
300
- function wrapMongooseValidator(schema) {
358
+ function wrapSchemaAsValidator(schema) {
301
359
  const validator = async val => {
302
360
  await schema.validate(val);
303
361
  };
304
362
  validator.schema = schema;
305
363
  return validator;
306
364
  }
365
+ function wrapMongooseValidator(validator) {
366
+ return async val => {
367
+ const result = await validator(val);
368
+ if (!result && result !== undefined) {
369
+ throw new Error('Validation failed.');
370
+ }
371
+ };
372
+ }
307
373
  function getNamedSchema(name) {
308
374
  const schema = namedSchemas[name];
309
375
  if (!schema) {