@bedrockio/model 0.1.0 → 0.1.2
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 +24 -0
- package/dist/cjs/access.js +4 -0
- package/dist/cjs/assign.js +2 -3
- package/dist/cjs/disallowed.js +40 -0
- package/dist/cjs/include.js +3 -2
- package/dist/cjs/load.js +15 -3
- package/dist/cjs/schema.js +29 -12
- package/dist/cjs/search.js +16 -16
- package/dist/cjs/serialization.js +2 -3
- package/dist/cjs/soft-delete.js +234 -55
- package/dist/cjs/testing.js +8 -0
- package/dist/cjs/validation.js +110 -44
- package/package.json +9 -7
- package/src/access.js +3 -0
- package/src/disallowed.js +63 -0
- package/src/include.js +1 -0
- package/src/load.js +12 -1
- package/src/schema.js +25 -5
- package/src/search.js +21 -10
- package/src/soft-delete.js +238 -85
- package/src/testing.js +7 -0
- package/src/validation.js +135 -44
- package/types/access.d.ts +7 -0
- package/types/access.d.ts.map +1 -0
- package/types/assign.d.ts +2 -0
- package/types/assign.d.ts.map +1 -0
- package/types/const.d.ts +9 -0
- package/types/const.d.ts.map +1 -0
- package/types/disallowed.d.ts +2 -0
- package/types/disallowed.d.ts.map +1 -0
- package/types/errors.d.ts +9 -0
- package/types/errors.d.ts.map +1 -0
- package/types/include.d.ts +4 -0
- package/types/include.d.ts.map +1 -0
- package/types/index.d.ts +6 -0
- package/types/index.d.ts.map +1 -0
- package/types/load.d.ts +15 -0
- package/types/load.d.ts.map +1 -0
- package/types/references.d.ts +2 -0
- package/types/references.d.ts.map +1 -0
- package/types/schema.d.ts +71 -0
- package/types/schema.d.ts.map +1 -0
- package/types/search.d.ts +303 -0
- package/types/search.d.ts.map +1 -0
- package/types/serialization.d.ts +6 -0
- package/types/serialization.d.ts.map +1 -0
- package/types/slug.d.ts +2 -0
- package/types/slug.d.ts.map +1 -0
- package/types/soft-delete.d.ts +4 -0
- package/types/soft-delete.d.ts.map +1 -0
- package/types/testing.d.ts +11 -0
- package/types/testing.d.ts.map +1 -0
- package/types/utils.d.ts +8 -0
- package/types/utils.d.ts.map +1 -0
- package/types/validation.d.ts +13 -0
- package/types/validation.d.ts.map +1 -0
- package/types/warn.d.ts +2 -0
- package/types/warn.d.ts.map +1 -0
- package/babel.config.cjs +0 -41
- package/jest.config.js +0 -8
- package/test/assign.test.js +0 -225
- package/test/definitions/custom-model.json +0 -9
- package/test/definitions/special-category.json +0 -18
- package/test/include.test.js +0 -896
- package/test/load.test.js +0 -47
- package/test/references.test.js +0 -71
- package/test/schema.test.js +0 -919
- package/test/search.test.js +0 -652
- package/test/serialization.test.js +0 -748
- package/test/setup.js +0 -27
- package/test/slug.test.js +0 -112
- package/test/soft-delete.test.js +0 -333
- package/test/validation.test.js +0 -1925
package/dist/cjs/soft-delete.js
CHANGED
|
@@ -4,9 +4,16 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.applySoftDelete = applySoftDelete;
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/cjs/testing.js
CHANGED
|
@@ -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') {
|
package/dist/cjs/validation.js
CHANGED
|
@@ -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 "
|
|
44
|
+
// Force "objectId" to have parity with refs.
|
|
27
45
|
// "mongo" is notably excluded here for this reason.
|
|
28
|
-
|
|
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,45 +69,60 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
119
|
|
|
87
|
-
function getSchemaFromMongoose(schema, options) {
|
|
120
|
+
function getSchemaFromMongoose(schema, options = {}) {
|
|
88
121
|
let {
|
|
89
122
|
obj
|
|
90
123
|
} = schema;
|
|
91
124
|
if (options.stripReserved) {
|
|
92
|
-
obj = (0,
|
|
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.
|
|
181
|
-
schema =
|
|
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
|
|
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
|
|
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
|
|
256
|
+
function getSearchSchema(schema, type) {
|
|
220
257
|
if (type === 'Number') {
|
|
221
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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) {
|