@bedrockio/model 0.9.0 → 0.10.0
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/CHANGELOG.md +9 -0
- package/README.md +24 -11
- package/dist/cjs/errors.js +9 -2
- package/dist/cjs/soft-delete.js +90 -114
- package/dist/cjs/validation.js +22 -28
- package/eslint.config.js +3 -0
- package/package.json +8 -10
- package/src/delete-hooks.js +2 -2
- package/src/disallowed.js +4 -4
- package/src/errors.js +7 -0
- package/src/include.js +2 -2
- package/src/slug.js +3 -3
- package/src/soft-delete.js +98 -113
- package/src/validation.js +27 -34
- package/types/errors.d.ts +4 -0
- package/types/errors.d.ts.map +1 -1
- package/types/include.d.ts +5 -2
- package/types/include.d.ts.map +1 -1
- package/types/search.d.ts +5 -2
- package/types/search.d.ts.map +1 -1
- package/types/soft-delete.d.ts +1 -2
- package/types/soft-delete.d.ts.map +1 -1
- package/types/validation.d.ts +2 -1
- package/types/validation.d.ts.map +1 -1
- package/.prettierrc.cjs +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.10.0
|
|
2
|
+
|
|
3
|
+
- Unique constraints now run sequentially and will not run on nested validations
|
|
4
|
+
unless their parent fields are passed.
|
|
5
|
+
|
|
6
|
+
## 0.9.1
|
|
7
|
+
|
|
8
|
+
- Allowed deriving individual paths from a create schema.
|
|
9
|
+
|
|
1
10
|
## 0.9.0
|
|
2
11
|
|
|
3
12
|
- Added keyword search decomposition.
|
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ Bedrock utilities for model creation.
|
|
|
9
9
|
- [Schema Extensions](#schema-extensions)
|
|
10
10
|
- [Attributes](#attributes)
|
|
11
11
|
- [Scopes](#scopes)
|
|
12
|
-
- [
|
|
12
|
+
- [Unique Constraints](#unique-constraints)
|
|
13
13
|
- [Array Extensions](#array-extensions)
|
|
14
14
|
- [String Trimming](#string-trimming)
|
|
15
|
+
- [Tuples](#tuples)
|
|
15
16
|
- [Modules](#modules)
|
|
16
17
|
- [Soft Delete](#soft-delete)
|
|
17
18
|
- [Validation](#validation)
|
|
@@ -214,8 +215,8 @@ type `Scope` helps make this possible:
|
|
|
214
215
|
"readAccess": "none",
|
|
215
216
|
"writeAccess": "none",
|
|
216
217
|
"attributes": {
|
|
217
|
-
"
|
|
218
|
-
"
|
|
218
|
+
"token": "String",
|
|
219
|
+
"hashedPassword": "String",
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
};
|
|
@@ -225,12 +226,12 @@ This syntax expands into the following:
|
|
|
225
226
|
|
|
226
227
|
```js
|
|
227
228
|
{
|
|
228
|
-
"
|
|
229
|
+
"token": {
|
|
229
230
|
"type": "String",
|
|
230
231
|
"readAccess": "none",
|
|
231
232
|
"writeAccess": "none",
|
|
232
233
|
},
|
|
233
|
-
"
|
|
234
|
+
"hashedPassword": {
|
|
234
235
|
"type": "String",
|
|
235
236
|
"readAccess": "none",
|
|
236
237
|
"writeAccess": "none",
|
|
@@ -437,9 +438,8 @@ an error:
|
|
|
437
438
|
|
|
438
439
|
#### Unique Constraints
|
|
439
440
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
deletion.
|
|
441
|
+
Although monogoose allows a `unique` option on fields, this will add a unique
|
|
442
|
+
index to the mongo collection itself which is incompatible with soft deletion.
|
|
443
443
|
|
|
444
444
|
This module will intercept `unique: true` to create a soft delete compatible
|
|
445
445
|
validation which will:
|
|
@@ -462,7 +462,7 @@ validation which will:
|
|
|
462
462
|
> unique field exists on any document **including the document being updated**.
|
|
463
463
|
> This is an intentional constraint that allows `updateOne` better peformance by
|
|
464
464
|
> not having to fetch the ids of the documents being updated in order to exclude
|
|
465
|
-
> them. To avoid this call `Document
|
|
465
|
+
> them. To avoid this call `Document#save` instead.
|
|
466
466
|
>
|
|
467
467
|
> Note also that calling `Model.updateMany` with a unique field passed will
|
|
468
468
|
> always throw an error as the result would inherently be non-unique.
|
|
@@ -525,8 +525,8 @@ Named validations can be specified on the model:
|
|
|
525
525
|
}
|
|
526
526
|
```
|
|
527
527
|
|
|
528
|
-
Validator functions are
|
|
529
|
-
|
|
528
|
+
Validator functions are [yada](https://github.com/bedrockio/yada#methods)
|
|
529
|
+
schemas. Note that:
|
|
530
530
|
|
|
531
531
|
- `email` - Will additionally downcase any input.
|
|
532
532
|
- `password` - Is not supported as it requires options to be passed and is not a
|
|
@@ -538,6 +538,19 @@ Validator functions are derived from
|
|
|
538
538
|
- `max` - Defined instead directly on the field with `maxLength` for strings and
|
|
539
539
|
`max` for numbers.
|
|
540
540
|
|
|
541
|
+
Schemas may also be
|
|
542
|
+
[merged together](https://github.com/bedrockio/yada#merging-fields) to produce
|
|
543
|
+
new ones:
|
|
544
|
+
|
|
545
|
+
```js
|
|
546
|
+
import yd from '@bedrockio/yada';
|
|
547
|
+
|
|
548
|
+
const signupSchema = yd.object({
|
|
549
|
+
...User.getCreateSchema().export(),
|
|
550
|
+
additionalField: yd.string().required(),
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
541
554
|
### Search
|
|
542
555
|
|
|
543
556
|
Models are extended with a `search` method that allows for complex searching:
|
package/dist/cjs/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.ReferenceError = exports.PermissionsError = exports.ImplementationError = void 0;
|
|
6
|
+
exports.UniqueConstraintError = exports.ReferenceError = exports.PermissionsError = exports.ImplementationError = void 0;
|
|
7
7
|
class PermissionsError extends Error {}
|
|
8
8
|
exports.PermissionsError = PermissionsError;
|
|
9
9
|
class ImplementationError extends Error {
|
|
@@ -19,4 +19,11 @@ class ReferenceError extends Error {
|
|
|
19
19
|
this.details = details;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
exports.ReferenceError = ReferenceError;
|
|
22
|
+
exports.ReferenceError = ReferenceError;
|
|
23
|
+
class UniqueConstraintError extends Error {
|
|
24
|
+
constructor(message, details) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.details = details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.UniqueConstraintError = UniqueConstraintError;
|
package/dist/cjs/soft-delete.js
CHANGED
|
@@ -5,16 +5,51 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.applySoftDelete = applySoftDelete;
|
|
7
7
|
exports.assertUnique = assertUnique;
|
|
8
|
-
exports.hasUniqueConstraints = hasUniqueConstraints;
|
|
9
|
-
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
10
8
|
var _lodash = require("lodash");
|
|
11
9
|
var _query = require("./query");
|
|
12
|
-
|
|
10
|
+
var _errors = require("./errors");
|
|
13
11
|
function applySoftDelete(schema) {
|
|
14
12
|
applyQueries(schema);
|
|
15
13
|
applyUniqueConstraints(schema);
|
|
16
14
|
applyHookPatch(schema);
|
|
17
15
|
}
|
|
16
|
+
async function assertUnique(options) {
|
|
17
|
+
let {
|
|
18
|
+
id,
|
|
19
|
+
model,
|
|
20
|
+
path,
|
|
21
|
+
value
|
|
22
|
+
} = options;
|
|
23
|
+
if (!value) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const field = Array.isArray(path) ? path.join('.') : path;
|
|
27
|
+
const query = {
|
|
28
|
+
[field]: value,
|
|
29
|
+
_id: {
|
|
30
|
+
$ne: id
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const exists = await model.exists(query);
|
|
34
|
+
if (exists) {
|
|
35
|
+
const message = getUniqueErrorMessage(model, field);
|
|
36
|
+
throw new _errors.UniqueConstraintError(message, {
|
|
37
|
+
...options,
|
|
38
|
+
field
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getUniqueErrorMessage(model, field) {
|
|
43
|
+
const {
|
|
44
|
+
modelName
|
|
45
|
+
} = model;
|
|
46
|
+
if (modelName === 'User' && !field.includes('.')) {
|
|
47
|
+
const name = field === 'phone' ? 'phone number' : field;
|
|
48
|
+
return `A user with that ${name} already exists.`;
|
|
49
|
+
} else {
|
|
50
|
+
return `"${field}" already exists.`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
18
53
|
|
|
19
54
|
// Soft Delete Querying
|
|
20
55
|
|
|
@@ -235,20 +270,24 @@ function getWithDeletedQuery() {
|
|
|
235
270
|
// Unique Constraints
|
|
236
271
|
|
|
237
272
|
function applyUniqueConstraints(schema) {
|
|
238
|
-
const
|
|
239
|
-
if (!
|
|
273
|
+
const uniquePaths = getUniqueConstraints(schema);
|
|
274
|
+
if (!uniquePaths.length) {
|
|
240
275
|
return;
|
|
241
276
|
}
|
|
242
277
|
schema.pre('save', async function () {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
278
|
+
for (let path of uniquePaths) {
|
|
279
|
+
await assertUnique({
|
|
280
|
+
path,
|
|
281
|
+
id: this.id,
|
|
282
|
+
value: this.get(path),
|
|
283
|
+
model: this.constructor
|
|
284
|
+
});
|
|
285
|
+
}
|
|
248
286
|
});
|
|
249
287
|
schema.pre(/^(update|replace)/, async function () {
|
|
250
288
|
await assertUniqueForQuery(this, {
|
|
251
|
-
schema
|
|
289
|
+
schema,
|
|
290
|
+
uniquePaths
|
|
252
291
|
});
|
|
253
292
|
});
|
|
254
293
|
schema.pre('insertMany', async function (next, obj) {
|
|
@@ -258,54 +297,39 @@ function applyUniqueConstraints(schema) {
|
|
|
258
297
|
// as the last argument, however as we are passing an async
|
|
259
298
|
// function it appears to not stop the middleware if we
|
|
260
299
|
// don't call it directly.
|
|
261
|
-
|
|
262
|
-
|
|
300
|
+
|
|
301
|
+
await runUniqueConstraints(obj, {
|
|
263
302
|
model: this,
|
|
264
|
-
|
|
303
|
+
uniquePaths
|
|
265
304
|
});
|
|
266
305
|
});
|
|
267
306
|
}
|
|
268
|
-
async function
|
|
307
|
+
async function runUniqueConstraints(arg, options) {
|
|
269
308
|
const {
|
|
270
|
-
|
|
271
|
-
model
|
|
272
|
-
schema
|
|
309
|
+
uniquePaths,
|
|
310
|
+
model
|
|
273
311
|
} = options;
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
312
|
+
// Updates or inserts
|
|
313
|
+
const operations = Array.isArray(arg) ? arg : [arg];
|
|
314
|
+
for (let operation of operations) {
|
|
315
|
+
for (let path of uniquePaths) {
|
|
316
|
+
const value = operation[path];
|
|
317
|
+
if (value) {
|
|
318
|
+
await assertUnique({
|
|
319
|
+
path,
|
|
320
|
+
value,
|
|
321
|
+
model
|
|
322
|
+
});
|
|
284
323
|
}
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
const found = await model.findOne(query, {}, {
|
|
288
|
-
lean: true
|
|
289
|
-
});
|
|
290
|
-
if (found) {
|
|
291
|
-
const {
|
|
292
|
-
modelName
|
|
293
|
-
} = model;
|
|
294
|
-
const foundFields = resolveUnique(schema, found);
|
|
295
|
-
const collisions = getCollisions(objFields, foundFields).join(', ');
|
|
296
|
-
throw new Error(`Cannot ${operation} ${modelName}. Duplicate fields exist: ${collisions}.`);
|
|
324
|
+
}
|
|
297
325
|
}
|
|
298
326
|
}
|
|
299
|
-
function getId(arg) {
|
|
300
|
-
const id = arg.id || arg._id;
|
|
301
|
-
return id ? String(id) : null;
|
|
302
|
-
}
|
|
303
327
|
|
|
304
328
|
// Asserts than an update or insert query will not
|
|
305
329
|
// result in duplicate unique fields being present
|
|
306
330
|
// within non-deleted documents.
|
|
307
331
|
async function assertUniqueForQuery(query, options) {
|
|
308
|
-
|
|
332
|
+
const update = query.getUpdate();
|
|
309
333
|
const operation = getOperationForQuery(update);
|
|
310
334
|
// Note: No need to check unique constraints
|
|
311
335
|
// if the operation is a delete.
|
|
@@ -314,6 +338,7 @@ async function assertUniqueForQuery(query, options) {
|
|
|
314
338
|
model
|
|
315
339
|
} = query;
|
|
316
340
|
const filter = query.getFilter();
|
|
341
|
+
let updates;
|
|
317
342
|
if (operation === 'restore') {
|
|
318
343
|
// A restore operation is functionally identical to a new
|
|
319
344
|
// insert so we need to fetch the deleted documents with
|
|
@@ -321,18 +346,30 @@ async function assertUniqueForQuery(query, options) {
|
|
|
321
346
|
const docs = await model.findWithDeleted(filter, {}, {
|
|
322
347
|
lean: true
|
|
323
348
|
});
|
|
324
|
-
|
|
349
|
+
updates = docs.map(doc => {
|
|
325
350
|
return {
|
|
326
351
|
...doc,
|
|
327
352
|
...update
|
|
328
353
|
};
|
|
329
354
|
});
|
|
355
|
+
} else {
|
|
356
|
+
updates = [update];
|
|
357
|
+
}
|
|
358
|
+
const {
|
|
359
|
+
uniquePaths
|
|
360
|
+
} = options;
|
|
361
|
+
for (let update of updates) {
|
|
362
|
+
for (let path of uniquePaths) {
|
|
363
|
+
const value = update[path];
|
|
364
|
+
if (value) {
|
|
365
|
+
await assertUnique({
|
|
366
|
+
path,
|
|
367
|
+
value,
|
|
368
|
+
model
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
330
372
|
}
|
|
331
|
-
await assertUnique(update, {
|
|
332
|
-
...options,
|
|
333
|
-
operation,
|
|
334
|
-
model
|
|
335
|
-
});
|
|
336
373
|
}
|
|
337
374
|
}
|
|
338
375
|
function getOperationForQuery(update) {
|
|
@@ -344,9 +381,9 @@ function getOperationForQuery(update) {
|
|
|
344
381
|
return 'update';
|
|
345
382
|
}
|
|
346
383
|
}
|
|
347
|
-
function
|
|
384
|
+
function getUniqueConstraints(schema) {
|
|
348
385
|
const paths = [...Object.keys(schema.paths), ...Object.keys(schema.subpaths)];
|
|
349
|
-
return paths.
|
|
386
|
+
return paths.filter(key => {
|
|
350
387
|
return isUniquePath(schema, key);
|
|
351
388
|
});
|
|
352
389
|
}
|
|
@@ -354,67 +391,6 @@ function isUniquePath(schema, key) {
|
|
|
354
391
|
return schema.path(key)?.options?.softUnique === true;
|
|
355
392
|
}
|
|
356
393
|
|
|
357
|
-
// Returns a flattened map of key -> [...values]
|
|
358
|
-
// consisting of only paths defined as unique on the schema.
|
|
359
|
-
function resolveUnique(schema, obj, map = {}, path = []) {
|
|
360
|
-
if (Array.isArray(obj)) {
|
|
361
|
-
for (let el of obj) {
|
|
362
|
-
resolveUnique(schema, el, map, path);
|
|
363
|
-
}
|
|
364
|
-
} else if (obj instanceof _mongoose.default.Document) {
|
|
365
|
-
obj.schema.eachPath(key => {
|
|
366
|
-
const val = obj.get(key);
|
|
367
|
-
resolveUnique(schema, val, map, [...path, key]);
|
|
368
|
-
});
|
|
369
|
-
} else if (obj && typeof obj === 'object') {
|
|
370
|
-
for (let [key, val] of Object.entries(obj)) {
|
|
371
|
-
resolveUnique(schema, val, map, [...path, key]);
|
|
372
|
-
}
|
|
373
|
-
} else if (obj) {
|
|
374
|
-
const key = path.join('.');
|
|
375
|
-
if (isUniquePath(schema, key)) {
|
|
376
|
-
map[key] ||= [];
|
|
377
|
-
map[key].push(obj);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return map;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Argument is guaranteed to be flattened.
|
|
384
|
-
function getUniqueQueries(obj) {
|
|
385
|
-
return Object.entries(obj).map(([key, val]) => {
|
|
386
|
-
if (val.length > 1) {
|
|
387
|
-
return {
|
|
388
|
-
[key]: {
|
|
389
|
-
$in: val
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
} else {
|
|
393
|
-
return {
|
|
394
|
-
[key]: val[0]
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Both arguments here are guaranteed to be flattened
|
|
401
|
-
// maps of key -> [values] of unique fields only.
|
|
402
|
-
function getCollisions(obj1, obj2) {
|
|
403
|
-
const collisions = [];
|
|
404
|
-
for (let [key, arr1] of Object.entries(obj1)) {
|
|
405
|
-
const arr2 = obj2[key];
|
|
406
|
-
if (arr2) {
|
|
407
|
-
const hasCollision = arr1.some(val => {
|
|
408
|
-
return arr2.includes(val);
|
|
409
|
-
});
|
|
410
|
-
if (hasCollision) {
|
|
411
|
-
collisions.push(key);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return collisions;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
394
|
// Hook Patch
|
|
419
395
|
|
|
420
396
|
function applyHookPatch(schema) {
|
package/dist/cjs/validation.js
CHANGED
|
@@ -14,8 +14,8 @@ var _yada = _interopRequireDefault(require("@bedrockio/yada"));
|
|
|
14
14
|
var _lodash = require("lodash");
|
|
15
15
|
var _access = require("./access");
|
|
16
16
|
var _search = require("./search");
|
|
17
|
-
var _errors = require("./errors");
|
|
18
17
|
var _softDelete = require("./soft-delete");
|
|
18
|
+
var _errors = require("./errors");
|
|
19
19
|
var _utils = require("./utils");
|
|
20
20
|
var _include = require("./include");
|
|
21
21
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -75,7 +75,6 @@ function addValidators(schemas) {
|
|
|
75
75
|
Object.assign(NAMED_SCHEMAS, schemas);
|
|
76
76
|
}
|
|
77
77
|
function applyValidation(schema, definition) {
|
|
78
|
-
const hasUnique = (0, _softDelete.hasUniqueConstraints)(schema);
|
|
79
78
|
schema.static('getCreateValidation', function getCreateValidation(options = {}) {
|
|
80
79
|
const {
|
|
81
80
|
allowInclude,
|
|
@@ -90,13 +89,7 @@ function applyValidation(schema, definition) {
|
|
|
90
89
|
stripTimestamps: true,
|
|
91
90
|
allowDefaultTags: true,
|
|
92
91
|
allowExpandedRefs: true,
|
|
93
|
-
requireWriteAccess: true
|
|
94
|
-
...(hasUnique && {
|
|
95
|
-
assertUniqueOptions: {
|
|
96
|
-
schema,
|
|
97
|
-
operation: 'create'
|
|
98
|
-
}
|
|
99
|
-
})
|
|
92
|
+
requireWriteAccess: true
|
|
100
93
|
});
|
|
101
94
|
});
|
|
102
95
|
schema.static('getUpdateValidation', function getUpdateValidation(options = {}) {
|
|
@@ -115,13 +108,7 @@ function applyValidation(schema, definition) {
|
|
|
115
108
|
stripTimestamps: true,
|
|
116
109
|
allowExpandedRefs: true,
|
|
117
110
|
requireWriteAccess: true,
|
|
118
|
-
updateAccess: definition.access?.update
|
|
119
|
-
...(hasUnique && {
|
|
120
|
-
assertUniqueOptions: {
|
|
121
|
-
schema,
|
|
122
|
-
operation: 'update'
|
|
123
|
-
}
|
|
124
|
-
})
|
|
111
|
+
updateAccess: definition.access?.update
|
|
125
112
|
});
|
|
126
113
|
});
|
|
127
114
|
schema.static('getSearchValidation', function getSearchValidation(options = {}) {
|
|
@@ -192,21 +179,10 @@ function getMongooseFields(schema, options) {
|
|
|
192
179
|
function getValidationSchema(attributes, options = {}) {
|
|
193
180
|
const {
|
|
194
181
|
appendSchema,
|
|
195
|
-
assertUniqueOptions,
|
|
196
182
|
allowInclude,
|
|
197
183
|
updateAccess
|
|
198
184
|
} = options;
|
|
199
185
|
let schema = getObjectSchema(attributes, options);
|
|
200
|
-
if (assertUniqueOptions) {
|
|
201
|
-
schema = schema.custom(async (obj, {
|
|
202
|
-
root
|
|
203
|
-
}) => {
|
|
204
|
-
await (0, _softDelete.assertUnique)(root, {
|
|
205
|
-
model: options.model,
|
|
206
|
-
...assertUniqueOptions
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
186
|
if (appendSchema) {
|
|
211
187
|
schema = schema.append(appendSchema);
|
|
212
188
|
}
|
|
@@ -353,6 +329,22 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
353
329
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
354
330
|
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
355
331
|
}
|
|
332
|
+
if (typedef.softUnique) {
|
|
333
|
+
schema = schema.custom(async (value, {
|
|
334
|
+
path,
|
|
335
|
+
originalRoot
|
|
336
|
+
}) => {
|
|
337
|
+
const {
|
|
338
|
+
id
|
|
339
|
+
} = originalRoot;
|
|
340
|
+
await (0, _softDelete.assertUnique)({
|
|
341
|
+
...options,
|
|
342
|
+
value,
|
|
343
|
+
path,
|
|
344
|
+
id
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
356
348
|
return schema;
|
|
357
349
|
}
|
|
358
350
|
function getSchemaForType(type, options) {
|
|
@@ -507,7 +499,9 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
507
499
|
// throw the error.
|
|
508
500
|
return;
|
|
509
501
|
}
|
|
510
|
-
|
|
502
|
+
|
|
503
|
+
// Default to not exposing the existence of this field.
|
|
504
|
+
message ||= `Unknown field "${path.join('.')}".`;
|
|
511
505
|
}
|
|
512
506
|
throw new _errors.PermissionsError(message);
|
|
513
507
|
}
|
package/eslint.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrockio/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -30,30 +30,28 @@
|
|
|
30
30
|
"lodash": "^4.17.21"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@bedrockio/yada": "^1.
|
|
33
|
+
"@bedrockio/yada": "^1.3.0",
|
|
34
34
|
"mongoose": "^8.6.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@babel/cli": "^7.26.4",
|
|
38
38
|
"@babel/core": "^7.26.0",
|
|
39
39
|
"@babel/preset-env": "^7.26.0",
|
|
40
|
+
"@bedrockio/eslint-plugin": "^1.1.7",
|
|
40
41
|
"@bedrockio/prettier-config": "^1.0.2",
|
|
41
|
-
"@bedrockio/yada": "^1.
|
|
42
|
+
"@bedrockio/yada": "^1.4.1",
|
|
42
43
|
"@shelf/jest-mongodb": "^4.3.2",
|
|
43
|
-
"eslint": "^
|
|
44
|
-
"eslint-plugin-bedrock": "^1.0.26",
|
|
44
|
+
"eslint": "^9.19.0",
|
|
45
45
|
"jest": "^29.7.0",
|
|
46
46
|
"jest-environment-node": "^29.7.0",
|
|
47
47
|
"mongodb": "^6.12.0",
|
|
48
48
|
"mongoose": "^8.8.4",
|
|
49
|
-
"prettier
|
|
49
|
+
"prettier": "^3.4.2",
|
|
50
50
|
"typescript": "^5.7.2"
|
|
51
51
|
},
|
|
52
|
-
"
|
|
53
|
-
"whatwg-url": "14.1.0"
|
|
54
|
-
},
|
|
52
|
+
"prettier": "@bedrockio/prettier-config",
|
|
55
53
|
"volta": {
|
|
56
|
-
"node": "
|
|
54
|
+
"node": "23.7.0",
|
|
57
55
|
"yarn": "1.22.22"
|
|
58
56
|
}
|
|
59
57
|
}
|
package/src/delete-hooks.js
CHANGED
|
@@ -147,7 +147,7 @@ async function errorOnForeignReferences(doc, options) {
|
|
|
147
147
|
{
|
|
148
148
|
[path]: doc.id,
|
|
149
149
|
},
|
|
150
|
-
{ _id: 1 }
|
|
150
|
+
{ _id: 1 },
|
|
151
151
|
)
|
|
152
152
|
.lean();
|
|
153
153
|
|
|
@@ -226,7 +226,7 @@ function getModelReferences(model, targetName) {
|
|
|
226
226
|
refs = model.schema.path(refPath).options.enum;
|
|
227
227
|
} else {
|
|
228
228
|
throw new Error(
|
|
229
|
-
`Cannot derive refs for ${model.modelName}#${schemaPath}
|
|
229
|
+
`Cannot derive refs for ${model.modelName}#${schemaPath}.`,
|
|
230
230
|
);
|
|
231
231
|
}
|
|
232
232
|
if (refs.includes(targetName)) {
|
package/src/disallowed.js
CHANGED
|
@@ -4,7 +4,7 @@ export function applyDisallowed(schema) {
|
|
|
4
4
|
schema.method('deleteOne', function () {
|
|
5
5
|
warn(
|
|
6
6
|
'The "deleteOne" method on documents is disallowed due to ambiguity',
|
|
7
|
-
'Use either "delete" or "deleteOne" on the model.'
|
|
7
|
+
'Use either "delete" or "deleteOne" on the model.',
|
|
8
8
|
);
|
|
9
9
|
throw new Error('Method not allowed.');
|
|
10
10
|
});
|
|
@@ -12,7 +12,7 @@ export function applyDisallowed(schema) {
|
|
|
12
12
|
schema.static('findOneAndRemove', function () {
|
|
13
13
|
warn(
|
|
14
14
|
'The "findOneAndRemove" method on models is disallowed due to ambiguity.',
|
|
15
|
-
'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".'
|
|
15
|
+
'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".',
|
|
16
16
|
);
|
|
17
17
|
throw new Error('Method not allowed.');
|
|
18
18
|
});
|
|
@@ -20,14 +20,14 @@ export function applyDisallowed(schema) {
|
|
|
20
20
|
schema.static('findByIdAndRemove', function () {
|
|
21
21
|
warn(
|
|
22
22
|
'The "findByIdAndRemove" method on models is disallowed due to ambiguity.',
|
|
23
|
-
'To permanently delete a document use "findByIdAndDestroy", otherwise "findByIdAndDelete".'
|
|
23
|
+
'To permanently delete a document use "findByIdAndDestroy", otherwise "findByIdAndDelete".',
|
|
24
24
|
);
|
|
25
25
|
throw new Error('Method not allowed.');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
schema.static('count', function () {
|
|
29
29
|
warn(
|
|
30
|
-
'The "count" method on models is deprecated. Use "countDocuments" instead.'
|
|
30
|
+
'The "count" method on models is deprecated. Use "countDocuments" instead.',
|
|
31
31
|
);
|
|
32
32
|
throw new Error('Method not allowed.');
|
|
33
33
|
});
|
package/src/errors.js
CHANGED
package/src/include.js
CHANGED
|
@@ -21,7 +21,7 @@ const DESCRIPTION = 'Field to be selected or populated.';
|
|
|
21
21
|
export const INCLUDE_FIELD_SCHEMA = yd.object({
|
|
22
22
|
include: yd.allow(
|
|
23
23
|
yd.string().description(DESCRIPTION),
|
|
24
|
-
yd.array(yd.string().description(DESCRIPTION))
|
|
24
|
+
yd.array(yd.string().description(DESCRIPTION)),
|
|
25
25
|
),
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -58,7 +58,7 @@ export function applyInclude(schema) {
|
|
|
58
58
|
await doc.include(include);
|
|
59
59
|
}
|
|
60
60
|
return doc;
|
|
61
|
-
}
|
|
61
|
+
},
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
// Synchronous method assigns the includes to locals.
|
package/src/slug.js
CHANGED
|
@@ -13,7 +13,7 @@ export function applySlug(schema) {
|
|
|
13
13
|
return find(this, str, args, {
|
|
14
14
|
deleted: true,
|
|
15
15
|
});
|
|
16
|
-
}
|
|
16
|
+
},
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
schema.static(
|
|
@@ -22,7 +22,7 @@ export function applySlug(schema) {
|
|
|
22
22
|
return find(this, str, args, {
|
|
23
23
|
deleted: { $in: [true, false] },
|
|
24
24
|
});
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -42,6 +42,6 @@ function find(Model, str, args, deleted) {
|
|
|
42
42
|
...deleted,
|
|
43
43
|
...query,
|
|
44
44
|
},
|
|
45
|
-
...args
|
|
45
|
+
...args,
|
|
46
46
|
);
|
|
47
47
|
}
|
package/src/soft-delete.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import mongoose from 'mongoose';
|
|
2
1
|
import { isEqual } from 'lodash';
|
|
3
2
|
|
|
4
3
|
import { wrapQuery } from './query';
|
|
4
|
+
import { UniqueConstraintError } from './errors';
|
|
5
5
|
|
|
6
6
|
export function applySoftDelete(schema) {
|
|
7
7
|
applyQueries(schema);
|
|
@@ -9,6 +9,40 @@ export function applySoftDelete(schema) {
|
|
|
9
9
|
applyHookPatch(schema);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export async function assertUnique(options) {
|
|
13
|
+
let { id, model, path, value } = options;
|
|
14
|
+
|
|
15
|
+
if (!value) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const field = Array.isArray(path) ? path.join('.') : path;
|
|
20
|
+
|
|
21
|
+
const query = {
|
|
22
|
+
[field]: value,
|
|
23
|
+
_id: { $ne: id },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const exists = await model.exists(query);
|
|
27
|
+
if (exists) {
|
|
28
|
+
const message = getUniqueErrorMessage(model, field);
|
|
29
|
+
throw new UniqueConstraintError(message, {
|
|
30
|
+
...options,
|
|
31
|
+
field,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getUniqueErrorMessage(model, field) {
|
|
37
|
+
const { modelName } = model;
|
|
38
|
+
if (modelName === 'User' && !field.includes('.')) {
|
|
39
|
+
const name = field === 'phone' ? 'phone number' : field;
|
|
40
|
+
return `A user with that ${name} already exists.`;
|
|
41
|
+
} else {
|
|
42
|
+
return `"${field}" already exists.`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
// Soft Delete Querying
|
|
13
47
|
|
|
14
48
|
function applyQueries(schema) {
|
|
@@ -58,7 +92,7 @@ function applyQueries(schema) {
|
|
|
58
92
|
deleted: false,
|
|
59
93
|
},
|
|
60
94
|
update,
|
|
61
|
-
...omitCallback(rest)
|
|
95
|
+
...omitCallback(rest),
|
|
62
96
|
);
|
|
63
97
|
return wrapQuery(query, async (promise) => {
|
|
64
98
|
const res = await promise;
|
|
@@ -77,7 +111,7 @@ function applyQueries(schema) {
|
|
|
77
111
|
deleted: false,
|
|
78
112
|
},
|
|
79
113
|
update,
|
|
80
|
-
...omitCallback(rest)
|
|
114
|
+
...omitCallback(rest),
|
|
81
115
|
);
|
|
82
116
|
return wrapQuery(query, async (promise) => {
|
|
83
117
|
const res = await promise;
|
|
@@ -95,7 +129,7 @@ function applyQueries(schema) {
|
|
|
95
129
|
deleted: false,
|
|
96
130
|
},
|
|
97
131
|
getDelete(),
|
|
98
|
-
...omitCallback(rest)
|
|
132
|
+
...omitCallback(rest),
|
|
99
133
|
);
|
|
100
134
|
});
|
|
101
135
|
|
|
@@ -106,7 +140,7 @@ function applyQueries(schema) {
|
|
|
106
140
|
deleted: true,
|
|
107
141
|
},
|
|
108
142
|
getRestore(),
|
|
109
|
-
...omitCallback(rest)
|
|
143
|
+
...omitCallback(rest),
|
|
110
144
|
);
|
|
111
145
|
return wrapQuery(query, async (promise) => {
|
|
112
146
|
const res = await promise;
|
|
@@ -124,7 +158,7 @@ function applyQueries(schema) {
|
|
|
124
158
|
deleted: true,
|
|
125
159
|
},
|
|
126
160
|
getRestore(),
|
|
127
|
-
...omitCallback(rest)
|
|
161
|
+
...omitCallback(rest),
|
|
128
162
|
);
|
|
129
163
|
return wrapQuery(query, async (promise) => {
|
|
130
164
|
const res = await promise;
|
|
@@ -139,7 +173,7 @@ function applyQueries(schema) {
|
|
|
139
173
|
// Following Mongoose patterns here
|
|
140
174
|
const query = new this.Query({}, {}, this, this.collection).deleteOne(
|
|
141
175
|
conditions,
|
|
142
|
-
...omitCallback(rest)
|
|
176
|
+
...omitCallback(rest),
|
|
143
177
|
);
|
|
144
178
|
return wrapQuery(query, async (promise) => {
|
|
145
179
|
const res = await promise;
|
|
@@ -154,7 +188,7 @@ function applyQueries(schema) {
|
|
|
154
188
|
// Following Mongoose patterns here
|
|
155
189
|
const query = new this.Query({}, {}, this, this.collection).deleteMany(
|
|
156
190
|
conditions,
|
|
157
|
-
...omitCallback(rest)
|
|
191
|
+
...omitCallback(rest),
|
|
158
192
|
);
|
|
159
193
|
return wrapQuery(query, async (promise) => {
|
|
160
194
|
const res = await promise;
|
|
@@ -205,7 +239,7 @@ function applyQueries(schema) {
|
|
|
205
239
|
deleted: true,
|
|
206
240
|
};
|
|
207
241
|
return this.countDocuments(filter, ...omitCallback(rest));
|
|
208
|
-
}
|
|
242
|
+
},
|
|
209
243
|
);
|
|
210
244
|
|
|
211
245
|
schema.static('findWithDeleted', function findWithDeleted(filter, ...rest) {
|
|
@@ -224,7 +258,7 @@ function applyQueries(schema) {
|
|
|
224
258
|
...getWithDeletedQuery(),
|
|
225
259
|
};
|
|
226
260
|
return this.findOne(filter, ...omitCallback(rest));
|
|
227
|
-
}
|
|
261
|
+
},
|
|
228
262
|
);
|
|
229
263
|
|
|
230
264
|
schema.static(
|
|
@@ -235,7 +269,7 @@ function applyQueries(schema) {
|
|
|
235
269
|
...getWithDeletedQuery(),
|
|
236
270
|
};
|
|
237
271
|
return this.findOne(filter, ...omitCallback(rest));
|
|
238
|
-
}
|
|
272
|
+
},
|
|
239
273
|
);
|
|
240
274
|
|
|
241
275
|
schema.static(
|
|
@@ -246,7 +280,7 @@ function applyQueries(schema) {
|
|
|
246
280
|
...getWithDeletedQuery(),
|
|
247
281
|
};
|
|
248
282
|
return this.exists(filter, ...omitCallback(rest));
|
|
249
|
-
}
|
|
283
|
+
},
|
|
250
284
|
);
|
|
251
285
|
|
|
252
286
|
schema.static(
|
|
@@ -257,7 +291,7 @@ function applyQueries(schema) {
|
|
|
257
291
|
...getWithDeletedQuery(),
|
|
258
292
|
};
|
|
259
293
|
return this.countDocuments(filter, ...omitCallback(rest));
|
|
260
|
-
}
|
|
294
|
+
},
|
|
261
295
|
);
|
|
262
296
|
}
|
|
263
297
|
|
|
@@ -284,23 +318,27 @@ function getWithDeletedQuery() {
|
|
|
284
318
|
// Unique Constraints
|
|
285
319
|
|
|
286
320
|
function applyUniqueConstraints(schema) {
|
|
287
|
-
const
|
|
321
|
+
const uniquePaths = getUniqueConstraints(schema);
|
|
288
322
|
|
|
289
|
-
if (!
|
|
323
|
+
if (!uniquePaths.length) {
|
|
290
324
|
return;
|
|
291
325
|
}
|
|
292
326
|
|
|
293
327
|
schema.pre('save', async function () {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
328
|
+
for (let path of uniquePaths) {
|
|
329
|
+
await assertUnique({
|
|
330
|
+
path,
|
|
331
|
+
id: this.id,
|
|
332
|
+
value: this.get(path),
|
|
333
|
+
model: this.constructor,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
299
336
|
});
|
|
300
337
|
|
|
301
338
|
schema.pre(/^(update|replace)/, async function () {
|
|
302
339
|
await assertUniqueForQuery(this, {
|
|
303
340
|
schema,
|
|
341
|
+
uniquePaths,
|
|
304
342
|
});
|
|
305
343
|
});
|
|
306
344
|
|
|
@@ -311,71 +349,73 @@ function applyUniqueConstraints(schema) {
|
|
|
311
349
|
// as the last argument, however as we are passing an async
|
|
312
350
|
// function it appears to not stop the middleware if we
|
|
313
351
|
// don't call it directly.
|
|
314
|
-
|
|
315
|
-
|
|
352
|
+
|
|
353
|
+
await runUniqueConstraints(obj, {
|
|
316
354
|
model: this,
|
|
317
|
-
|
|
355
|
+
uniquePaths,
|
|
318
356
|
});
|
|
319
357
|
});
|
|
320
358
|
}
|
|
321
359
|
|
|
322
|
-
|
|
323
|
-
const {
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const { modelName } = model;
|
|
338
|
-
const foundFields = resolveUnique(schema, found);
|
|
339
|
-
const collisions = getCollisions(objFields, foundFields).join(', ');
|
|
340
|
-
throw new Error(
|
|
341
|
-
`Cannot ${operation} ${modelName}. Duplicate fields exist: ${collisions}.`
|
|
342
|
-
);
|
|
360
|
+
async function runUniqueConstraints(arg, options) {
|
|
361
|
+
const { uniquePaths, model } = options;
|
|
362
|
+
// Updates or inserts
|
|
363
|
+
const operations = Array.isArray(arg) ? arg : [arg];
|
|
364
|
+
for (let operation of operations) {
|
|
365
|
+
for (let path of uniquePaths) {
|
|
366
|
+
const value = operation[path];
|
|
367
|
+
if (value) {
|
|
368
|
+
await assertUnique({
|
|
369
|
+
path,
|
|
370
|
+
value,
|
|
371
|
+
model,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
343
375
|
}
|
|
344
376
|
}
|
|
345
377
|
|
|
346
|
-
function getId(arg) {
|
|
347
|
-
const id = arg.id || arg._id;
|
|
348
|
-
return id ? String(id) : null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
378
|
// Asserts than an update or insert query will not
|
|
352
379
|
// result in duplicate unique fields being present
|
|
353
380
|
// within non-deleted documents.
|
|
354
381
|
async function assertUniqueForQuery(query, options) {
|
|
355
|
-
|
|
382
|
+
const update = query.getUpdate();
|
|
356
383
|
const operation = getOperationForQuery(update);
|
|
357
384
|
// Note: No need to check unique constraints
|
|
358
385
|
// if the operation is a delete.
|
|
359
386
|
if (operation === 'restore' || operation === 'update') {
|
|
360
387
|
const { model } = query;
|
|
361
388
|
const filter = query.getFilter();
|
|
389
|
+
|
|
390
|
+
let updates;
|
|
362
391
|
if (operation === 'restore') {
|
|
363
392
|
// A restore operation is functionally identical to a new
|
|
364
393
|
// insert so we need to fetch the deleted documents with
|
|
365
394
|
// all fields available to check against.
|
|
366
395
|
const docs = await model.findWithDeleted(filter, {}, { lean: true });
|
|
367
|
-
|
|
396
|
+
updates = docs.map((doc) => {
|
|
368
397
|
return {
|
|
369
398
|
...doc,
|
|
370
399
|
...update,
|
|
371
400
|
};
|
|
372
401
|
});
|
|
402
|
+
} else {
|
|
403
|
+
updates = [update];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { uniquePaths } = options;
|
|
407
|
+
for (let update of updates) {
|
|
408
|
+
for (let path of uniquePaths) {
|
|
409
|
+
const value = update[path];
|
|
410
|
+
if (value) {
|
|
411
|
+
await assertUnique({
|
|
412
|
+
path,
|
|
413
|
+
value,
|
|
414
|
+
model,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
373
418
|
}
|
|
374
|
-
await assertUnique(update, {
|
|
375
|
-
...options,
|
|
376
|
-
operation,
|
|
377
|
-
model,
|
|
378
|
-
});
|
|
379
419
|
}
|
|
380
420
|
}
|
|
381
421
|
|
|
@@ -389,9 +429,9 @@ function getOperationForQuery(update) {
|
|
|
389
429
|
}
|
|
390
430
|
}
|
|
391
431
|
|
|
392
|
-
|
|
432
|
+
function getUniqueConstraints(schema) {
|
|
393
433
|
const paths = [...Object.keys(schema.paths), ...Object.keys(schema.subpaths)];
|
|
394
|
-
return paths.
|
|
434
|
+
return paths.filter((key) => {
|
|
395
435
|
return isUniquePath(schema, key);
|
|
396
436
|
});
|
|
397
437
|
}
|
|
@@ -400,61 +440,6 @@ function isUniquePath(schema, key) {
|
|
|
400
440
|
return schema.path(key)?.options?.softUnique === true;
|
|
401
441
|
}
|
|
402
442
|
|
|
403
|
-
// Returns a flattened map of key -> [...values]
|
|
404
|
-
// consisting of only paths defined as unique on the schema.
|
|
405
|
-
function resolveUnique(schema, obj, map = {}, path = []) {
|
|
406
|
-
if (Array.isArray(obj)) {
|
|
407
|
-
for (let el of obj) {
|
|
408
|
-
resolveUnique(schema, el, map, path);
|
|
409
|
-
}
|
|
410
|
-
} else if (obj instanceof mongoose.Document) {
|
|
411
|
-
obj.schema.eachPath((key) => {
|
|
412
|
-
const val = obj.get(key);
|
|
413
|
-
resolveUnique(schema, val, map, [...path, key]);
|
|
414
|
-
});
|
|
415
|
-
} else if (obj && typeof obj === 'object') {
|
|
416
|
-
for (let [key, val] of Object.entries(obj)) {
|
|
417
|
-
resolveUnique(schema, val, map, [...path, key]);
|
|
418
|
-
}
|
|
419
|
-
} else if (obj) {
|
|
420
|
-
const key = path.join('.');
|
|
421
|
-
if (isUniquePath(schema, key)) {
|
|
422
|
-
map[key] ||= [];
|
|
423
|
-
map[key].push(obj);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return map;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Argument is guaranteed to be flattened.
|
|
430
|
-
function getUniqueQueries(obj) {
|
|
431
|
-
return Object.entries(obj).map(([key, val]) => {
|
|
432
|
-
if (val.length > 1) {
|
|
433
|
-
return { [key]: { $in: val } };
|
|
434
|
-
} else {
|
|
435
|
-
return { [key]: val[0] };
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Both arguments here are guaranteed to be flattened
|
|
441
|
-
// maps of key -> [values] of unique fields only.
|
|
442
|
-
function getCollisions(obj1, obj2) {
|
|
443
|
-
const collisions = [];
|
|
444
|
-
for (let [key, arr1] of Object.entries(obj1)) {
|
|
445
|
-
const arr2 = obj2[key];
|
|
446
|
-
if (arr2) {
|
|
447
|
-
const hasCollision = arr1.some((val) => {
|
|
448
|
-
return arr2.includes(val);
|
|
449
|
-
});
|
|
450
|
-
if (hasCollision) {
|
|
451
|
-
collisions.push(key);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return collisions;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
443
|
// Hook Patch
|
|
459
444
|
|
|
460
445
|
function applyHookPatch(schema) {
|
package/src/validation.js
CHANGED
|
@@ -5,8 +5,8 @@ import { get, omit, lowerFirst } from 'lodash';
|
|
|
5
5
|
|
|
6
6
|
import { hasAccess } from './access';
|
|
7
7
|
import { searchValidation } from './search';
|
|
8
|
+
import { assertUnique } from './soft-delete';
|
|
8
9
|
import { PermissionsError, ImplementationError } from './errors';
|
|
9
|
-
import { hasUniqueConstraints, assertUnique } from './soft-delete';
|
|
10
10
|
import { isMongooseSchema, isSchemaTypedef } from './utils';
|
|
11
11
|
import { INCLUDE_FIELD_SCHEMA } from './include';
|
|
12
12
|
|
|
@@ -38,7 +38,7 @@ const REFERENCE_SCHEMA = yd
|
|
|
38
38
|
})
|
|
39
39
|
.custom((obj) => {
|
|
40
40
|
return obj.id;
|
|
41
|
-
})
|
|
41
|
+
}),
|
|
42
42
|
)
|
|
43
43
|
.message('Must be an id or object containing an "id" field.')
|
|
44
44
|
.tag({
|
|
@@ -87,8 +87,6 @@ export function addValidators(schemas) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export function applyValidation(schema, definition) {
|
|
90
|
-
const hasUnique = hasUniqueConstraints(schema);
|
|
91
|
-
|
|
92
90
|
schema.static(
|
|
93
91
|
'getCreateValidation',
|
|
94
92
|
function getCreateValidation(options = {}) {
|
|
@@ -103,14 +101,8 @@ export function applyValidation(schema, definition) {
|
|
|
103
101
|
allowDefaultTags: true,
|
|
104
102
|
allowExpandedRefs: true,
|
|
105
103
|
requireWriteAccess: true,
|
|
106
|
-
...(hasUnique && {
|
|
107
|
-
assertUniqueOptions: {
|
|
108
|
-
schema,
|
|
109
|
-
operation: 'create',
|
|
110
|
-
},
|
|
111
|
-
}),
|
|
112
104
|
});
|
|
113
|
-
}
|
|
105
|
+
},
|
|
114
106
|
);
|
|
115
107
|
|
|
116
108
|
schema.static(
|
|
@@ -129,14 +121,8 @@ export function applyValidation(schema, definition) {
|
|
|
129
121
|
allowExpandedRefs: true,
|
|
130
122
|
requireWriteAccess: true,
|
|
131
123
|
updateAccess: definition.access?.update,
|
|
132
|
-
...(hasUnique && {
|
|
133
|
-
assertUniqueOptions: {
|
|
134
|
-
schema,
|
|
135
|
-
operation: 'update',
|
|
136
|
-
},
|
|
137
|
-
}),
|
|
138
124
|
});
|
|
139
|
-
}
|
|
125
|
+
},
|
|
140
126
|
);
|
|
141
127
|
|
|
142
128
|
schema.static(
|
|
@@ -160,7 +146,7 @@ export function applyValidation(schema, definition) {
|
|
|
160
146
|
appendSchema,
|
|
161
147
|
}),
|
|
162
148
|
});
|
|
163
|
-
}
|
|
149
|
+
},
|
|
164
150
|
);
|
|
165
151
|
|
|
166
152
|
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
@@ -205,17 +191,10 @@ function getMongooseFields(schema, options) {
|
|
|
205
191
|
|
|
206
192
|
// Exported for testing
|
|
207
193
|
export function getValidationSchema(attributes, options = {}) {
|
|
208
|
-
const { appendSchema,
|
|
209
|
-
|
|
194
|
+
const { appendSchema, allowInclude, updateAccess } = options;
|
|
195
|
+
|
|
210
196
|
let schema = getObjectSchema(attributes, options);
|
|
211
|
-
|
|
212
|
-
schema = schema.custom(async (obj, { root }) => {
|
|
213
|
-
await assertUnique(root, {
|
|
214
|
-
model: options.model,
|
|
215
|
-
...assertUniqueOptions,
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
197
|
+
|
|
219
198
|
if (appendSchema) {
|
|
220
199
|
schema = schema.append(appendSchema);
|
|
221
200
|
}
|
|
@@ -374,6 +353,18 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
374
353
|
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
375
354
|
}
|
|
376
355
|
|
|
356
|
+
if (typedef.softUnique) {
|
|
357
|
+
schema = schema.custom(async (value, { path, originalRoot }) => {
|
|
358
|
+
const { id } = originalRoot;
|
|
359
|
+
await assertUnique({
|
|
360
|
+
...options,
|
|
361
|
+
value,
|
|
362
|
+
path,
|
|
363
|
+
id,
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
377
368
|
return schema;
|
|
378
369
|
}
|
|
379
370
|
|
|
@@ -423,10 +414,10 @@ function getSearchSchema(schema, type) {
|
|
|
423
414
|
'x-schema': 'NumberRange',
|
|
424
415
|
'x-description':
|
|
425
416
|
'An object representing numbers falling within a range.',
|
|
426
|
-
})
|
|
417
|
+
}),
|
|
427
418
|
)
|
|
428
419
|
.description(
|
|
429
|
-
'Allows searching by a value, array of values, or a numeric range.'
|
|
420
|
+
'Allows searching by a value, array of values, or a numeric range.',
|
|
430
421
|
);
|
|
431
422
|
} else if (type === 'Date') {
|
|
432
423
|
return yd
|
|
@@ -468,7 +459,7 @@ function getSearchSchema(schema, type) {
|
|
|
468
459
|
'x-schema': 'DateRange',
|
|
469
460
|
'x-description':
|
|
470
461
|
'An object representing dates falling within a range.',
|
|
471
|
-
})
|
|
462
|
+
}),
|
|
472
463
|
)
|
|
473
464
|
.description('Allows searching by a date, array of dates, or a range.');
|
|
474
465
|
} else if (type === 'String' || type === 'ObjectId') {
|
|
@@ -543,7 +534,7 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
543
534
|
isAllowed = false;
|
|
544
535
|
} else {
|
|
545
536
|
throw new Error(
|
|
546
|
-
`Access validation "${error.name}" requires passing { document, authUser } to the validator
|
|
537
|
+
`Access validation "${error.name}" requires passing { document, authUser } to the validator.`,
|
|
547
538
|
);
|
|
548
539
|
}
|
|
549
540
|
} else {
|
|
@@ -560,7 +551,9 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
560
551
|
// throw the error.
|
|
561
552
|
return;
|
|
562
553
|
}
|
|
563
|
-
|
|
554
|
+
|
|
555
|
+
// Default to not exposing the existence of this field.
|
|
556
|
+
message ||= `Unknown field "${path.join('.')}".`;
|
|
564
557
|
}
|
|
565
558
|
throw new PermissionsError(message);
|
|
566
559
|
}
|
package/types/errors.d.ts
CHANGED
|
@@ -8,4 +8,8 @@ export class ReferenceError extends Error {
|
|
|
8
8
|
constructor(message: any, details: any);
|
|
9
9
|
details: any;
|
|
10
10
|
}
|
|
11
|
+
export class UniqueConstraintError extends Error {
|
|
12
|
+
constructor(message: any, details: any);
|
|
13
|
+
details: any;
|
|
14
|
+
}
|
|
11
15
|
//# sourceMappingURL=errors.d.ts.map
|
package/types/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAE9C;IACE,uBAGC;IADC,UAAgB;CAEnB;AAED;IACE,wCAGC;IADC,aAAsB;CAEzB"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAE9C;IACE,uBAGC;IADC,UAAgB;CAEnB;AAED;IACE,wCAGC;IADC,aAAsB;CAEzB;AAED;IACE,wCAGC;IADC,aAAsB;CAEzB"}
|
package/types/include.d.ts
CHANGED
|
@@ -4,10 +4,13 @@ export function getParams(modelName: any, arg: any): any;
|
|
|
4
4
|
export function getDocumentParams(doc: any, arg: any, options?: {}): any;
|
|
5
5
|
export const INCLUDE_FIELD_SCHEMA: {
|
|
6
6
|
setup(): void;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
get(path?: string | Array<string>): any;
|
|
8
|
+
unwind(path?: string | Array<string>): any;
|
|
9
9
|
pick(...names?: string[]): /*elided*/ any;
|
|
10
10
|
omit(...names?: string[]): /*elided*/ any;
|
|
11
|
+
require(...fields: string[]): /*elided*/ any;
|
|
12
|
+
export(): any;
|
|
13
|
+
append(arg: import("@bedrockio/yada/types/object").SchemaMap | import("@bedrockio/yada/types/Schema").default): /*elided*/ any;
|
|
11
14
|
format(name: any, fn: any): /*elided*/ any;
|
|
12
15
|
toString(): any;
|
|
13
16
|
assertions: any[];
|
package/types/include.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,yDAIC;AAaD,yEAUC;AA9ID
|
|
1
|
+
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,yDAIC;AAaD,yEAUC;AA9ID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAsDsB,CAAC;;;;;;;;;;;;;;;;;EAjDpB"}
|
package/types/search.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export function applySearch(schema: any, definition: any): void;
|
|
2
2
|
export function searchValidation(options?: {}): {
|
|
3
3
|
setup(): void;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
get(path?: string | Array<string>): any;
|
|
5
|
+
unwind(path?: string | Array<string>): any;
|
|
6
6
|
pick(...names?: string[]): /*elided*/ any;
|
|
7
7
|
omit(...names?: string[]): /*elided*/ any;
|
|
8
|
+
require(...fields: string[]): /*elided*/ any;
|
|
9
|
+
export(): any;
|
|
10
|
+
append(arg: import("@bedrockio/yada/types/object").SchemaMap | import("@bedrockio/yada/types/Schema").default): /*elided*/ any;
|
|
8
11
|
format(name: any, fn: any): /*elided*/ any;
|
|
9
12
|
toString(): any;
|
|
10
13
|
assertions: any[];
|
package/types/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAsBA,gEAwDC;AAED
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAsBA,gEAwDC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAWY,CAAC;;;;;;;;;;;;;;;;;EAcZ"}
|
package/types/soft-delete.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export function applySoftDelete(schema: any): void;
|
|
2
|
-
export function assertUnique(
|
|
3
|
-
export function hasUniqueConstraints(schema: any): boolean;
|
|
2
|
+
export function assertUnique(options: any): Promise<void>;
|
|
4
3
|
//# sourceMappingURL=soft-delete.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"soft-delete.d.ts","sourceRoot":"","sources":["../src/soft-delete.js"],"names":[],"mappings":"AAKA,mDAIC;
|
|
1
|
+
{"version":3,"file":"soft-delete.d.ts","sourceRoot":"","sources":["../src/soft-delete.js"],"names":[],"mappings":"AAKA,mDAIC;AAED,0DAsBC"}
|
package/types/validation.d.ts
CHANGED
|
@@ -84,7 +84,7 @@ export const OBJECT_ID_SCHEMA: {
|
|
|
84
84
|
options(options: any): /*elided*/ any;
|
|
85
85
|
validate(value: any, options?: {}): Promise<any>;
|
|
86
86
|
clone(meta: any): /*elided*/ any;
|
|
87
|
-
append(schema: any):
|
|
87
|
+
append(schema: any): import("@bedrockio/yada/types/Schema").default;
|
|
88
88
|
toOpenApi(extra: any): any;
|
|
89
89
|
getAnyType(): {
|
|
90
90
|
type: string[];
|
|
@@ -99,6 +99,7 @@ export const OBJECT_ID_SCHEMA: {
|
|
|
99
99
|
};
|
|
100
100
|
expandExtra(extra?: {}): {};
|
|
101
101
|
inspect(): string;
|
|
102
|
+
get(): void;
|
|
102
103
|
assertEnum(set: any, allow: any): /*elided*/ any;
|
|
103
104
|
assert(type: any, fn: any): /*elided*/ any;
|
|
104
105
|
pushAssertion(assertion: any): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAoFA,kDAEC;AAED,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAoFA,kDAEC;AAED,oEAkFC;AAsBD,wEAoBC;AAgWD;;;EAEC;AAED;;;EAOC;AA7iBD;;;;;;;;;;;;;;;;eAwBoB,CAAC;;;;;;;;;;;;iBAenB,CAAA;kBAEA,CAAF;kBAA4B,CAAC;oBACA,CAAC;oBAE5B,CAAC;;;wBAkBc,CAAC;8BAIjB,CAAC;oBAA+B,CAAC;oBACV,CAAC;oCAGG,CAAC;uBACpB,CAAC;8BAEH,CAAC;uBAAmC,CAAA;iBACrB,CAAC;;;mBAkBX,CAAC;yBAAoC,CAAC;0BACpB,CAAC;yBAEvB,CAAN;sBACW,CAAC;yBAEN,CAAJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eA9CC,CAAC;;;;;;;;;;;;;;;;;;EA5CD"}
|
package/.prettierrc.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('@bedrockio/prettier-config');
|