@bedrockio/model 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +24 -11
- package/dist/cjs/errors.js +15 -2
- package/dist/cjs/soft-delete.js +91 -114
- package/dist/cjs/validation.js +22 -28
- package/eslint.config.js +3 -0
- package/package.json +6 -8
- package/src/delete-hooks.js +2 -2
- package/src/disallowed.js +4 -4
- package/src/errors.js +14 -0
- package/src/include.js +2 -2
- package/src/slug.js +3 -3
- package/src/soft-delete.js +99 -113
- package/src/validation.js +27 -34
- package/types/errors.d.ts +8 -0
- package/types/errors.d.ts.map +1 -1
- package/types/include.d.ts +4 -2
- package/types/include.d.ts.map +1 -1
- package/types/search.d.ts +4 -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 +1 -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.1
|
|
2
|
+
|
|
3
|
+
- Fix to not expose details on unique constraint errors.
|
|
4
|
+
|
|
5
|
+
## 0.10.0
|
|
6
|
+
|
|
7
|
+
- Unique constraints now run sequentially and will not run on nested validations
|
|
8
|
+
unless their parent fields are passed.
|
|
9
|
+
|
|
1
10
|
## 0.9.1
|
|
2
11
|
|
|
3
12
|
- Allowed deriving individual paths from a create schema.
|
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,17 @@ 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
|
+
toJSON() {
|
|
29
|
+
return {
|
|
30
|
+
type: 'unique',
|
|
31
|
+
message: this.message
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.UniqueConstraintError = UniqueConstraintError;
|
package/dist/cjs/soft-delete.js
CHANGED
|
@@ -5,16 +5,52 @@ 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
|
+
model,
|
|
38
|
+
field,
|
|
39
|
+
value
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getUniqueErrorMessage(model, field) {
|
|
44
|
+
const {
|
|
45
|
+
modelName
|
|
46
|
+
} = model;
|
|
47
|
+
if (modelName === 'User' && !field.includes('.')) {
|
|
48
|
+
const name = field === 'phone' ? 'phone number' : field;
|
|
49
|
+
return `A user with that ${name} already exists.`;
|
|
50
|
+
} else {
|
|
51
|
+
return `"${field}" already exists.`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
18
54
|
|
|
19
55
|
// Soft Delete Querying
|
|
20
56
|
|
|
@@ -235,20 +271,24 @@ function getWithDeletedQuery() {
|
|
|
235
271
|
// Unique Constraints
|
|
236
272
|
|
|
237
273
|
function applyUniqueConstraints(schema) {
|
|
238
|
-
const
|
|
239
|
-
if (!
|
|
274
|
+
const uniquePaths = getUniqueConstraints(schema);
|
|
275
|
+
if (!uniquePaths.length) {
|
|
240
276
|
return;
|
|
241
277
|
}
|
|
242
278
|
schema.pre('save', async function () {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
for (let path of uniquePaths) {
|
|
280
|
+
await assertUnique({
|
|
281
|
+
path,
|
|
282
|
+
id: this.id,
|
|
283
|
+
value: this.get(path),
|
|
284
|
+
model: this.constructor
|
|
285
|
+
});
|
|
286
|
+
}
|
|
248
287
|
});
|
|
249
288
|
schema.pre(/^(update|replace)/, async function () {
|
|
250
289
|
await assertUniqueForQuery(this, {
|
|
251
|
-
schema
|
|
290
|
+
schema,
|
|
291
|
+
uniquePaths
|
|
252
292
|
});
|
|
253
293
|
});
|
|
254
294
|
schema.pre('insertMany', async function (next, obj) {
|
|
@@ -258,54 +298,39 @@ function applyUniqueConstraints(schema) {
|
|
|
258
298
|
// as the last argument, however as we are passing an async
|
|
259
299
|
// function it appears to not stop the middleware if we
|
|
260
300
|
// don't call it directly.
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
|
|
302
|
+
await runUniqueConstraints(obj, {
|
|
263
303
|
model: this,
|
|
264
|
-
|
|
304
|
+
uniquePaths
|
|
265
305
|
});
|
|
266
306
|
});
|
|
267
307
|
}
|
|
268
|
-
async function
|
|
308
|
+
async function runUniqueConstraints(arg, options) {
|
|
269
309
|
const {
|
|
270
|
-
|
|
271
|
-
model
|
|
272
|
-
schema
|
|
310
|
+
uniquePaths,
|
|
311
|
+
model
|
|
273
312
|
} = options;
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
313
|
+
// Updates or inserts
|
|
314
|
+
const operations = Array.isArray(arg) ? arg : [arg];
|
|
315
|
+
for (let operation of operations) {
|
|
316
|
+
for (let path of uniquePaths) {
|
|
317
|
+
const value = operation[path];
|
|
318
|
+
if (value) {
|
|
319
|
+
await assertUnique({
|
|
320
|
+
path,
|
|
321
|
+
value,
|
|
322
|
+
model
|
|
323
|
+
});
|
|
284
324
|
}
|
|
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}.`);
|
|
325
|
+
}
|
|
297
326
|
}
|
|
298
327
|
}
|
|
299
|
-
function getId(arg) {
|
|
300
|
-
const id = arg.id || arg._id;
|
|
301
|
-
return id ? String(id) : null;
|
|
302
|
-
}
|
|
303
328
|
|
|
304
329
|
// Asserts than an update or insert query will not
|
|
305
330
|
// result in duplicate unique fields being present
|
|
306
331
|
// within non-deleted documents.
|
|
307
332
|
async function assertUniqueForQuery(query, options) {
|
|
308
|
-
|
|
333
|
+
const update = query.getUpdate();
|
|
309
334
|
const operation = getOperationForQuery(update);
|
|
310
335
|
// Note: No need to check unique constraints
|
|
311
336
|
// if the operation is a delete.
|
|
@@ -314,6 +339,7 @@ async function assertUniqueForQuery(query, options) {
|
|
|
314
339
|
model
|
|
315
340
|
} = query;
|
|
316
341
|
const filter = query.getFilter();
|
|
342
|
+
let updates;
|
|
317
343
|
if (operation === 'restore') {
|
|
318
344
|
// A restore operation is functionally identical to a new
|
|
319
345
|
// insert so we need to fetch the deleted documents with
|
|
@@ -321,18 +347,30 @@ async function assertUniqueForQuery(query, options) {
|
|
|
321
347
|
const docs = await model.findWithDeleted(filter, {}, {
|
|
322
348
|
lean: true
|
|
323
349
|
});
|
|
324
|
-
|
|
350
|
+
updates = docs.map(doc => {
|
|
325
351
|
return {
|
|
326
352
|
...doc,
|
|
327
353
|
...update
|
|
328
354
|
};
|
|
329
355
|
});
|
|
356
|
+
} else {
|
|
357
|
+
updates = [update];
|
|
358
|
+
}
|
|
359
|
+
const {
|
|
360
|
+
uniquePaths
|
|
361
|
+
} = options;
|
|
362
|
+
for (let update of updates) {
|
|
363
|
+
for (let path of uniquePaths) {
|
|
364
|
+
const value = update[path];
|
|
365
|
+
if (value) {
|
|
366
|
+
await assertUnique({
|
|
367
|
+
path,
|
|
368
|
+
value,
|
|
369
|
+
model
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
330
373
|
}
|
|
331
|
-
await assertUnique(update, {
|
|
332
|
-
...options,
|
|
333
|
-
operation,
|
|
334
|
-
model
|
|
335
|
-
});
|
|
336
374
|
}
|
|
337
375
|
}
|
|
338
376
|
function getOperationForQuery(update) {
|
|
@@ -344,9 +382,9 @@ function getOperationForQuery(update) {
|
|
|
344
382
|
return 'update';
|
|
345
383
|
}
|
|
346
384
|
}
|
|
347
|
-
function
|
|
385
|
+
function getUniqueConstraints(schema) {
|
|
348
386
|
const paths = [...Object.keys(schema.paths), ...Object.keys(schema.subpaths)];
|
|
349
|
-
return paths.
|
|
387
|
+
return paths.filter(key => {
|
|
350
388
|
return isUniquePath(schema, key);
|
|
351
389
|
});
|
|
352
390
|
}
|
|
@@ -354,67 +392,6 @@ function isUniquePath(schema, key) {
|
|
|
354
392
|
return schema.path(key)?.options?.softUnique === true;
|
|
355
393
|
}
|
|
356
394
|
|
|
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
395
|
// Hook Patch
|
|
419
396
|
|
|
420
397
|
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.1",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,11 +37,11 @@
|
|
|
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",
|
|
@@ -49,11 +49,9 @@
|
|
|
49
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
|
@@ -13,3 +13,17 @@ export class ReferenceError extends Error {
|
|
|
13
13
|
this.details = details;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export class UniqueConstraintError extends Error {
|
|
18
|
+
constructor(message, details) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.details = details;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
type: 'unique',
|
|
26
|
+
message: this.message,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
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,41 @@ 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
|
+
model,
|
|
31
|
+
field,
|
|
32
|
+
value,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getUniqueErrorMessage(model, field) {
|
|
38
|
+
const { modelName } = model;
|
|
39
|
+
if (modelName === 'User' && !field.includes('.')) {
|
|
40
|
+
const name = field === 'phone' ? 'phone number' : field;
|
|
41
|
+
return `A user with that ${name} already exists.`;
|
|
42
|
+
} else {
|
|
43
|
+
return `"${field}" already exists.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
12
47
|
// Soft Delete Querying
|
|
13
48
|
|
|
14
49
|
function applyQueries(schema) {
|
|
@@ -58,7 +93,7 @@ function applyQueries(schema) {
|
|
|
58
93
|
deleted: false,
|
|
59
94
|
},
|
|
60
95
|
update,
|
|
61
|
-
...omitCallback(rest)
|
|
96
|
+
...omitCallback(rest),
|
|
62
97
|
);
|
|
63
98
|
return wrapQuery(query, async (promise) => {
|
|
64
99
|
const res = await promise;
|
|
@@ -77,7 +112,7 @@ function applyQueries(schema) {
|
|
|
77
112
|
deleted: false,
|
|
78
113
|
},
|
|
79
114
|
update,
|
|
80
|
-
...omitCallback(rest)
|
|
115
|
+
...omitCallback(rest),
|
|
81
116
|
);
|
|
82
117
|
return wrapQuery(query, async (promise) => {
|
|
83
118
|
const res = await promise;
|
|
@@ -95,7 +130,7 @@ function applyQueries(schema) {
|
|
|
95
130
|
deleted: false,
|
|
96
131
|
},
|
|
97
132
|
getDelete(),
|
|
98
|
-
...omitCallback(rest)
|
|
133
|
+
...omitCallback(rest),
|
|
99
134
|
);
|
|
100
135
|
});
|
|
101
136
|
|
|
@@ -106,7 +141,7 @@ function applyQueries(schema) {
|
|
|
106
141
|
deleted: true,
|
|
107
142
|
},
|
|
108
143
|
getRestore(),
|
|
109
|
-
...omitCallback(rest)
|
|
144
|
+
...omitCallback(rest),
|
|
110
145
|
);
|
|
111
146
|
return wrapQuery(query, async (promise) => {
|
|
112
147
|
const res = await promise;
|
|
@@ -124,7 +159,7 @@ function applyQueries(schema) {
|
|
|
124
159
|
deleted: true,
|
|
125
160
|
},
|
|
126
161
|
getRestore(),
|
|
127
|
-
...omitCallback(rest)
|
|
162
|
+
...omitCallback(rest),
|
|
128
163
|
);
|
|
129
164
|
return wrapQuery(query, async (promise) => {
|
|
130
165
|
const res = await promise;
|
|
@@ -139,7 +174,7 @@ function applyQueries(schema) {
|
|
|
139
174
|
// Following Mongoose patterns here
|
|
140
175
|
const query = new this.Query({}, {}, this, this.collection).deleteOne(
|
|
141
176
|
conditions,
|
|
142
|
-
...omitCallback(rest)
|
|
177
|
+
...omitCallback(rest),
|
|
143
178
|
);
|
|
144
179
|
return wrapQuery(query, async (promise) => {
|
|
145
180
|
const res = await promise;
|
|
@@ -154,7 +189,7 @@ function applyQueries(schema) {
|
|
|
154
189
|
// Following Mongoose patterns here
|
|
155
190
|
const query = new this.Query({}, {}, this, this.collection).deleteMany(
|
|
156
191
|
conditions,
|
|
157
|
-
...omitCallback(rest)
|
|
192
|
+
...omitCallback(rest),
|
|
158
193
|
);
|
|
159
194
|
return wrapQuery(query, async (promise) => {
|
|
160
195
|
const res = await promise;
|
|
@@ -205,7 +240,7 @@ function applyQueries(schema) {
|
|
|
205
240
|
deleted: true,
|
|
206
241
|
};
|
|
207
242
|
return this.countDocuments(filter, ...omitCallback(rest));
|
|
208
|
-
}
|
|
243
|
+
},
|
|
209
244
|
);
|
|
210
245
|
|
|
211
246
|
schema.static('findWithDeleted', function findWithDeleted(filter, ...rest) {
|
|
@@ -224,7 +259,7 @@ function applyQueries(schema) {
|
|
|
224
259
|
...getWithDeletedQuery(),
|
|
225
260
|
};
|
|
226
261
|
return this.findOne(filter, ...omitCallback(rest));
|
|
227
|
-
}
|
|
262
|
+
},
|
|
228
263
|
);
|
|
229
264
|
|
|
230
265
|
schema.static(
|
|
@@ -235,7 +270,7 @@ function applyQueries(schema) {
|
|
|
235
270
|
...getWithDeletedQuery(),
|
|
236
271
|
};
|
|
237
272
|
return this.findOne(filter, ...omitCallback(rest));
|
|
238
|
-
}
|
|
273
|
+
},
|
|
239
274
|
);
|
|
240
275
|
|
|
241
276
|
schema.static(
|
|
@@ -246,7 +281,7 @@ function applyQueries(schema) {
|
|
|
246
281
|
...getWithDeletedQuery(),
|
|
247
282
|
};
|
|
248
283
|
return this.exists(filter, ...omitCallback(rest));
|
|
249
|
-
}
|
|
284
|
+
},
|
|
250
285
|
);
|
|
251
286
|
|
|
252
287
|
schema.static(
|
|
@@ -257,7 +292,7 @@ function applyQueries(schema) {
|
|
|
257
292
|
...getWithDeletedQuery(),
|
|
258
293
|
};
|
|
259
294
|
return this.countDocuments(filter, ...omitCallback(rest));
|
|
260
|
-
}
|
|
295
|
+
},
|
|
261
296
|
);
|
|
262
297
|
}
|
|
263
298
|
|
|
@@ -284,23 +319,27 @@ function getWithDeletedQuery() {
|
|
|
284
319
|
// Unique Constraints
|
|
285
320
|
|
|
286
321
|
function applyUniqueConstraints(schema) {
|
|
287
|
-
const
|
|
322
|
+
const uniquePaths = getUniqueConstraints(schema);
|
|
288
323
|
|
|
289
|
-
if (!
|
|
324
|
+
if (!uniquePaths.length) {
|
|
290
325
|
return;
|
|
291
326
|
}
|
|
292
327
|
|
|
293
328
|
schema.pre('save', async function () {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
329
|
+
for (let path of uniquePaths) {
|
|
330
|
+
await assertUnique({
|
|
331
|
+
path,
|
|
332
|
+
id: this.id,
|
|
333
|
+
value: this.get(path),
|
|
334
|
+
model: this.constructor,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
299
337
|
});
|
|
300
338
|
|
|
301
339
|
schema.pre(/^(update|replace)/, async function () {
|
|
302
340
|
await assertUniqueForQuery(this, {
|
|
303
341
|
schema,
|
|
342
|
+
uniquePaths,
|
|
304
343
|
});
|
|
305
344
|
});
|
|
306
345
|
|
|
@@ -311,71 +350,73 @@ function applyUniqueConstraints(schema) {
|
|
|
311
350
|
// as the last argument, however as we are passing an async
|
|
312
351
|
// function it appears to not stop the middleware if we
|
|
313
352
|
// don't call it directly.
|
|
314
|
-
|
|
315
|
-
|
|
353
|
+
|
|
354
|
+
await runUniqueConstraints(obj, {
|
|
316
355
|
model: this,
|
|
317
|
-
|
|
356
|
+
uniquePaths,
|
|
318
357
|
});
|
|
319
358
|
});
|
|
320
359
|
}
|
|
321
360
|
|
|
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
|
-
);
|
|
361
|
+
async function runUniqueConstraints(arg, options) {
|
|
362
|
+
const { uniquePaths, model } = options;
|
|
363
|
+
// Updates or inserts
|
|
364
|
+
const operations = Array.isArray(arg) ? arg : [arg];
|
|
365
|
+
for (let operation of operations) {
|
|
366
|
+
for (let path of uniquePaths) {
|
|
367
|
+
const value = operation[path];
|
|
368
|
+
if (value) {
|
|
369
|
+
await assertUnique({
|
|
370
|
+
path,
|
|
371
|
+
value,
|
|
372
|
+
model,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
343
376
|
}
|
|
344
377
|
}
|
|
345
378
|
|
|
346
|
-
function getId(arg) {
|
|
347
|
-
const id = arg.id || arg._id;
|
|
348
|
-
return id ? String(id) : null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
379
|
// Asserts than an update or insert query will not
|
|
352
380
|
// result in duplicate unique fields being present
|
|
353
381
|
// within non-deleted documents.
|
|
354
382
|
async function assertUniqueForQuery(query, options) {
|
|
355
|
-
|
|
383
|
+
const update = query.getUpdate();
|
|
356
384
|
const operation = getOperationForQuery(update);
|
|
357
385
|
// Note: No need to check unique constraints
|
|
358
386
|
// if the operation is a delete.
|
|
359
387
|
if (operation === 'restore' || operation === 'update') {
|
|
360
388
|
const { model } = query;
|
|
361
389
|
const filter = query.getFilter();
|
|
390
|
+
|
|
391
|
+
let updates;
|
|
362
392
|
if (operation === 'restore') {
|
|
363
393
|
// A restore operation is functionally identical to a new
|
|
364
394
|
// insert so we need to fetch the deleted documents with
|
|
365
395
|
// all fields available to check against.
|
|
366
396
|
const docs = await model.findWithDeleted(filter, {}, { lean: true });
|
|
367
|
-
|
|
397
|
+
updates = docs.map((doc) => {
|
|
368
398
|
return {
|
|
369
399
|
...doc,
|
|
370
400
|
...update,
|
|
371
401
|
};
|
|
372
402
|
});
|
|
403
|
+
} else {
|
|
404
|
+
updates = [update];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { uniquePaths } = options;
|
|
408
|
+
for (let update of updates) {
|
|
409
|
+
for (let path of uniquePaths) {
|
|
410
|
+
const value = update[path];
|
|
411
|
+
if (value) {
|
|
412
|
+
await assertUnique({
|
|
413
|
+
path,
|
|
414
|
+
value,
|
|
415
|
+
model,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
373
419
|
}
|
|
374
|
-
await assertUnique(update, {
|
|
375
|
-
...options,
|
|
376
|
-
operation,
|
|
377
|
-
model,
|
|
378
|
-
});
|
|
379
420
|
}
|
|
380
421
|
}
|
|
381
422
|
|
|
@@ -389,9 +430,9 @@ function getOperationForQuery(update) {
|
|
|
389
430
|
}
|
|
390
431
|
}
|
|
391
432
|
|
|
392
|
-
|
|
433
|
+
function getUniqueConstraints(schema) {
|
|
393
434
|
const paths = [...Object.keys(schema.paths), ...Object.keys(schema.subpaths)];
|
|
394
|
-
return paths.
|
|
435
|
+
return paths.filter((key) => {
|
|
395
436
|
return isUniquePath(schema, key);
|
|
396
437
|
});
|
|
397
438
|
}
|
|
@@ -400,61 +441,6 @@ function isUniquePath(schema, key) {
|
|
|
400
441
|
return schema.path(key)?.options?.softUnique === true;
|
|
401
442
|
}
|
|
402
443
|
|
|
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
444
|
// Hook Patch
|
|
459
445
|
|
|
460
446
|
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,12 @@ 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
|
+
toJSON(): {
|
|
15
|
+
type: string;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
11
19
|
//# 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;IAGxB;;;MAKC;CACF"}
|
package/types/include.d.ts
CHANGED
|
@@ -4,11 +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
|
-
getFields(): any;
|
|
8
|
-
append(arg: import("@bedrockio/yada/types/object").SchemaMap | import("@bedrockio/yada/types/Schema").default): /*elided*/ any;
|
|
9
7
|
get(path?: string | Array<string>): any;
|
|
8
|
+
unwind(path?: string | Array<string>): any;
|
|
10
9
|
pick(...names?: string[]): /*elided*/ any;
|
|
11
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;
|
|
12
14
|
format(name: any, fn: any): /*elided*/ any;
|
|
13
15
|
toString(): any;
|
|
14
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,11 +1,13 @@
|
|
|
1
1
|
export function applySearch(schema: any, definition: any): void;
|
|
2
2
|
export function searchValidation(options?: {}): {
|
|
3
3
|
setup(): void;
|
|
4
|
-
getFields(): any;
|
|
5
|
-
append(arg: import("@bedrockio/yada/types/object").SchemaMap | import("@bedrockio/yada/types/Schema").default): /*elided*/ any;
|
|
6
4
|
get(path?: string | Array<string>): any;
|
|
5
|
+
unwind(path?: string | Array<string>): any;
|
|
7
6
|
pick(...names?: string[]): /*elided*/ any;
|
|
8
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;
|
|
9
11
|
format(name: any, fn: any): /*elided*/ any;
|
|
10
12
|
toString(): any;
|
|
11
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,0DAuBC"}
|
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[];
|
|
@@ -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');
|