@bedrockio/model 0.6.0 → 0.7.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/README.md +108 -7
- package/dist/cjs/assign.js +1 -1
- package/dist/cjs/search.js +7 -4
- package/dist/cjs/utils.js +5 -0
- package/dist/cjs/validation.js +64 -18
- package/package.json +5 -5
- package/src/assign.js +1 -1
- package/src/search.js +9 -5
- package/src/utils.js +5 -0
- package/src/validation.js +61 -21
- package/types/include.d.ts +5 -1
- package/types/include.d.ts.map +1 -1
- package/types/search.d.ts +5 -1
- package/types/search.d.ts.map +1 -1
- package/types/utils.d.ts +1 -0
- package/types/utils.d.ts.map +1 -1
- package/types/validation.d.ts +5 -2
- package/types/validation.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Bedrock utilities for model creation.
|
|
|
19
19
|
- [Delete Hooks](#delete-hooks)
|
|
20
20
|
- [Access Control](#access-control)
|
|
21
21
|
- [Assign](#assign)
|
|
22
|
+
- [Upsert](#upsert)
|
|
22
23
|
- [Slugs](#slugs)
|
|
23
24
|
- [Testing](#testing)
|
|
24
25
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -568,11 +569,55 @@ will be flattened to:
|
|
|
568
569
|
}
|
|
569
570
|
```
|
|
570
571
|
|
|
572
|
+
#### Searching Arrays
|
|
573
|
+
|
|
574
|
+
Passing an array to `search` will perform a query using `$in`, which matches on
|
|
575
|
+
the intersection of any elements. For example,
|
|
576
|
+
|
|
577
|
+
```json
|
|
578
|
+
tags: ["one", "two"]
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
is effectively saying "match any documents whose `tags` array contains any of
|
|
582
|
+
`one` or `two`".
|
|
583
|
+
|
|
584
|
+
For this reason, passing an empty array here is ambiguous as it could be asking:
|
|
585
|
+
|
|
586
|
+
- Match any documents whose `tags` array is empty.
|
|
587
|
+
- Match any documents whose `tags` array contains any of `[]` (ie. no elements
|
|
588
|
+
passed so no match).
|
|
589
|
+
|
|
590
|
+
The difference here is subtle, but for example in a UI field where tags can be
|
|
591
|
+
chosen but none happen to be selected, it would be unexpected to return
|
|
592
|
+
documents simply because their `tags` array happens to be empty.
|
|
593
|
+
|
|
594
|
+
The `search` method takes the simpler approach here where an empty array will
|
|
595
|
+
simply be passed along to `$in`, which will never result in a match.
|
|
596
|
+
|
|
597
|
+
However, as a special case, `null` may instead be passed to `search` for array
|
|
598
|
+
fields to explicitly search for empty arrays:
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
await User.search({
|
|
602
|
+
tags: null,
|
|
603
|
+
});
|
|
604
|
+
// Equivalent to:
|
|
605
|
+
await User.find({
|
|
606
|
+
tags: [],
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
This works because Mongoose will always initialize an array field in its
|
|
611
|
+
documents (ie. the field is always guarnateed to exist and be an array), so an
|
|
612
|
+
empty array can be thought of as equivalent to `{ $exists: false }` for array
|
|
613
|
+
fields. Thinking of it this way makes it more intuitive that `null` should
|
|
614
|
+
match.
|
|
615
|
+
|
|
571
616
|
#### Range Based Search
|
|
572
617
|
|
|
573
618
|
Additionally, date and number fields allow range queries in the form:
|
|
574
619
|
|
|
575
|
-
```
|
|
620
|
+
```json
|
|
576
621
|
age: {
|
|
577
622
|
gt: 1
|
|
578
623
|
lt: 2
|
|
@@ -653,7 +698,57 @@ schema.pre('save', function () {
|
|
|
653
698
|
});
|
|
654
699
|
```
|
|
655
700
|
|
|
656
|
-
|
|
701
|
+
#### Syncing Cached Fields
|
|
702
|
+
|
|
703
|
+
When a foreign document is updated the cached fields will be out of sync, for
|
|
704
|
+
example:
|
|
705
|
+
|
|
706
|
+
```js
|
|
707
|
+
await shop.save();
|
|
708
|
+
console.log(shop.userName);
|
|
709
|
+
// The current user name
|
|
710
|
+
|
|
711
|
+
user.name = 'New Name';
|
|
712
|
+
await user.save();
|
|
713
|
+
|
|
714
|
+
shop = await Shop.findById(shop.id);
|
|
715
|
+
console.log(shop.userName);
|
|
716
|
+
// Cached userName is out of sync as the user has been updated
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
A simple mechanism is provided via the `sync` key to keep the documents in sync:
|
|
720
|
+
|
|
721
|
+
```jsonc
|
|
722
|
+
// In user.json
|
|
723
|
+
{
|
|
724
|
+
"attributes": {
|
|
725
|
+
"name": "String"
|
|
726
|
+
},
|
|
727
|
+
"search": {
|
|
728
|
+
"sync": [
|
|
729
|
+
{
|
|
730
|
+
"ref": "Shop",
|
|
731
|
+
"path": "user"
|
|
732
|
+
}
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
This is the equivalent of running the following in a post save hook:
|
|
739
|
+
|
|
740
|
+
```js
|
|
741
|
+
const shops = await Shop.find({
|
|
742
|
+
user: user.id,
|
|
743
|
+
});
|
|
744
|
+
for (let shop of shops) {
|
|
745
|
+
await shop.save();
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
This will run the hooks on each shop, synchronizing the cached fields.
|
|
750
|
+
|
|
751
|
+
##### Initial Sync
|
|
657
752
|
|
|
658
753
|
When first applying or making changes to defined cached search fields, existing
|
|
659
754
|
documents will be out of sync. The static method `syncCacheFields` is provided
|
|
@@ -1484,11 +1579,17 @@ user.assign(ctx.request.body);
|
|
|
1484
1579
|
Object.assign(user, ctx.request.body);
|
|
1485
1580
|
```
|
|
1486
1581
|
|
|
1487
|
-
This is functionally identical to `Object.assign` with the exception that
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1582
|
+
This is functionally identical to `Object.assign` with the exception that fields
|
|
1583
|
+
can be unset by passing falsy values. This method is provided as `undefined`
|
|
1584
|
+
cannot be represented in JSON which requires using either a `null` or empty
|
|
1585
|
+
string, both of which would be stored in the database if naively assigned with
|
|
1586
|
+
`Object.assign`.
|
|
1587
|
+
|
|
1588
|
+
### Upsert
|
|
1589
|
+
|
|
1590
|
+
This module adds a single `findOrCreate` convenience method that is easy to
|
|
1591
|
+
understand and avoids some of the gotchas that come with upserting documents in
|
|
1592
|
+
Mongoose.
|
|
1492
1593
|
|
|
1493
1594
|
### Slugs
|
|
1494
1595
|
|
package/dist/cjs/assign.js
CHANGED
|
@@ -12,7 +12,7 @@ function applyAssign(schema) {
|
|
|
12
12
|
schema.method('assign', function assign(fields) {
|
|
13
13
|
unsetReferenceFields(fields, schema.obj);
|
|
14
14
|
for (let [path, value] of Object.entries(flattenObject(fields))) {
|
|
15
|
-
if (value === null) {
|
|
15
|
+
if (value === null || value === '') {
|
|
16
16
|
this.set(path, undefined);
|
|
17
17
|
} else {
|
|
18
18
|
this.set(path, value);
|
package/dist/cjs/search.js
CHANGED
|
@@ -237,6 +237,8 @@ function normalizeQuery(query, schema, root = {}, rootPath = []) {
|
|
|
237
237
|
root[path.join('.')] = {
|
|
238
238
|
$in: value
|
|
239
239
|
};
|
|
240
|
+
} else if (isEmptyArrayQuery(schema, key, value)) {
|
|
241
|
+
root[path.join('.')] = [];
|
|
240
242
|
} else {
|
|
241
243
|
root[path.join('.')] = value;
|
|
242
244
|
}
|
|
@@ -256,12 +258,15 @@ function isNestedQuery(key, value) {
|
|
|
256
258
|
function isArrayQuery(key, value) {
|
|
257
259
|
return !isMongoOperator(key) && !isInclude(key) && Array.isArray(value);
|
|
258
260
|
}
|
|
261
|
+
function isEmptyArrayQuery(schema, key, value) {
|
|
262
|
+
return !isMongoOperator(key) && (0, _utils.isArrayField)(schema, key) && value === null;
|
|
263
|
+
}
|
|
259
264
|
function isRangeQuery(schema, key, value) {
|
|
260
265
|
// Range queries only allowed on Date and Number fields.
|
|
261
266
|
if (!(0, _utils.isDateField)(schema, key) && !(0, _utils.isNumberField)(schema, key)) {
|
|
262
267
|
return false;
|
|
263
268
|
}
|
|
264
|
-
return typeof value === 'object';
|
|
269
|
+
return typeof value === 'object' && !!value;
|
|
265
270
|
}
|
|
266
271
|
function mapOperatorQuery(obj) {
|
|
267
272
|
const query = {};
|
|
@@ -323,9 +328,7 @@ function applySearchCache(schema, definition) {
|
|
|
323
328
|
if (!force) {
|
|
324
329
|
const $or = Object.keys(cache).map(cachedField => {
|
|
325
330
|
return {
|
|
326
|
-
[cachedField]:
|
|
327
|
-
$exists: false
|
|
328
|
-
}
|
|
331
|
+
[cachedField]: null
|
|
329
332
|
};
|
|
330
333
|
});
|
|
331
334
|
query.$or = $or;
|
package/dist/cjs/utils.js
CHANGED
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getField = getField;
|
|
7
7
|
exports.getInnerField = getInnerField;
|
|
8
|
+
exports.isArrayField = isArrayField;
|
|
8
9
|
exports.isDateField = isDateField;
|
|
9
10
|
exports.isEqual = isEqual;
|
|
10
11
|
exports.isMongooseSchema = isMongooseSchema;
|
|
@@ -44,6 +45,10 @@ function isDateField(obj, path) {
|
|
|
44
45
|
function isNumberField(obj, path) {
|
|
45
46
|
return isType(obj, path, 'Number');
|
|
46
47
|
}
|
|
48
|
+
function isArrayField(obj, path) {
|
|
49
|
+
const field = getField(obj, path);
|
|
50
|
+
return Array.isArray(field?.type);
|
|
51
|
+
}
|
|
47
52
|
function isType(obj, path, test) {
|
|
48
53
|
const {
|
|
49
54
|
type
|
package/dist/cjs/validation.js
CHANGED
|
@@ -83,7 +83,7 @@ function applyValidation(schema, definition) {
|
|
|
83
83
|
model: this,
|
|
84
84
|
appendSchema,
|
|
85
85
|
allowInclude,
|
|
86
|
-
|
|
86
|
+
stripEmpty: true,
|
|
87
87
|
stripDeleted: true,
|
|
88
88
|
stripTimestamps: true,
|
|
89
89
|
allowDefaultTags: true,
|
|
@@ -106,7 +106,7 @@ function applyValidation(schema, definition) {
|
|
|
106
106
|
model: this,
|
|
107
107
|
appendSchema,
|
|
108
108
|
allowInclude,
|
|
109
|
-
|
|
109
|
+
allowNull: true,
|
|
110
110
|
skipRequired: true,
|
|
111
111
|
stripUnknown: true,
|
|
112
112
|
stripDeleted: true,
|
|
@@ -122,13 +122,6 @@ function applyValidation(schema, definition) {
|
|
|
122
122
|
})
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
|
-
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
126
|
-
const allowed = definition.access?.delete || 'all';
|
|
127
|
-
return validateAccess('delete', _yada.default, allowed, {
|
|
128
|
-
model: this,
|
|
129
|
-
message: 'You do not have permissions to delete this document.'
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
125
|
schema.static('getSearchValidation', function getSearchValidation(options = {}) {
|
|
133
126
|
const {
|
|
134
127
|
defaults,
|
|
@@ -137,6 +130,8 @@ function applyValidation(schema, definition) {
|
|
|
137
130
|
} = options;
|
|
138
131
|
return getSchemaFromMongoose(schema, {
|
|
139
132
|
model: this,
|
|
133
|
+
allowNull: true,
|
|
134
|
+
stripEmpty: true,
|
|
140
135
|
allowSearch: true,
|
|
141
136
|
skipRequired: true,
|
|
142
137
|
allowInclude: true,
|
|
@@ -151,6 +146,13 @@ function applyValidation(schema, definition) {
|
|
|
151
146
|
})
|
|
152
147
|
});
|
|
153
148
|
});
|
|
149
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
150
|
+
const allowed = definition.access?.delete || 'all';
|
|
151
|
+
return validateAccess('delete', _yada.default, allowed, {
|
|
152
|
+
model: this,
|
|
153
|
+
message: 'You do not have permissions to delete this document.'
|
|
154
|
+
});
|
|
155
|
+
});
|
|
154
156
|
schema.static('getIncludeValidation', function getIncludeValidation() {
|
|
155
157
|
return _include.INCLUDE_FIELD_SCHEMA;
|
|
156
158
|
});
|
|
@@ -227,6 +229,7 @@ function getObjectSchema(arg, options) {
|
|
|
227
229
|
} else if (typeof arg === 'object') {
|
|
228
230
|
const {
|
|
229
231
|
stripUnknown,
|
|
232
|
+
stripEmpty,
|
|
230
233
|
expandDotSyntax
|
|
231
234
|
} = options;
|
|
232
235
|
const map = {};
|
|
@@ -241,6 +244,11 @@ function getObjectSchema(arg, options) {
|
|
|
241
244
|
stripUnknown: true
|
|
242
245
|
});
|
|
243
246
|
}
|
|
247
|
+
if (stripEmpty) {
|
|
248
|
+
schema = schema.options({
|
|
249
|
+
stripEmpty: true
|
|
250
|
+
});
|
|
251
|
+
}
|
|
244
252
|
if (expandDotSyntax) {
|
|
245
253
|
schema = schema.options({
|
|
246
254
|
expandDotSyntax: true
|
|
@@ -284,13 +292,24 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
284
292
|
} else {
|
|
285
293
|
schema = getSchemaForType(type, options);
|
|
286
294
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
// Null may be allowed to unset non-required fields
|
|
296
|
+
// in an update operation or to search for non-existent
|
|
297
|
+
// fields in a search operation.
|
|
298
|
+
if (allowNull(typedef, options)) {
|
|
299
|
+
schema = schema.nullable();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Empty strings are allowed to unset non-required fields
|
|
303
|
+
// in an update operation. Technically this should be null,
|
|
304
|
+
// however empty strings are allowed here as well as they
|
|
305
|
+
// generally play nicer with front-end components. For
|
|
306
|
+
// ObjectId fields the empty string must be appended here.
|
|
307
|
+
if (disallowEmpty(typedef, options)) {
|
|
291
308
|
schema = schema.options({
|
|
292
|
-
allowEmpty:
|
|
309
|
+
allowEmpty: false
|
|
293
310
|
});
|
|
311
|
+
} else if (appendEmpty(typedef, options)) {
|
|
312
|
+
schema = _yada.default.allow(schema, '');
|
|
294
313
|
}
|
|
295
314
|
}
|
|
296
315
|
if (isRequired(typedef, options)) {
|
|
@@ -402,11 +421,38 @@ function getSearchSchema(schema, type) {
|
|
|
402
421
|
function isRequired(typedef, options) {
|
|
403
422
|
return typedef.required && !typedef.default && !options.skipRequired;
|
|
404
423
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
424
|
+
function allowNull(typedef, options) {
|
|
425
|
+
if (!options.allowNull) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const {
|
|
429
|
+
required,
|
|
430
|
+
type
|
|
431
|
+
} = typedef;
|
|
432
|
+
return !required && type !== 'Boolean';
|
|
407
433
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
434
|
+
function disallowEmpty(typedef, options) {
|
|
435
|
+
if (!options.allowNull) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const {
|
|
439
|
+
type
|
|
440
|
+
} = typedef;
|
|
441
|
+
if (type === 'String' || type === 'ObjectId') {
|
|
442
|
+
return typedef.required;
|
|
443
|
+
} else {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function appendEmpty(typedef, options) {
|
|
448
|
+
if (!options.allowNull) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const {
|
|
452
|
+
required,
|
|
453
|
+
type
|
|
454
|
+
} = typedef;
|
|
455
|
+
return !required && type === 'ObjectId';
|
|
410
456
|
}
|
|
411
457
|
function isExcludedField(field, options) {
|
|
412
458
|
if ((0, _utils.isSchemaTypedef)(field)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrockio/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -30,22 +30,22 @@
|
|
|
30
30
|
"lodash": "^4.17.21"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@bedrockio/yada": "^1.1
|
|
34
|
-
"mongoose": "^8.
|
|
33
|
+
"@bedrockio/yada": "^1.2.1",
|
|
34
|
+
"mongoose": "^8.6.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@babel/cli": "^7.20.7",
|
|
38
38
|
"@babel/core": "^7.20.12",
|
|
39
39
|
"@babel/preset-env": "^7.20.2",
|
|
40
40
|
"@bedrockio/prettier-config": "^1.0.2",
|
|
41
|
-
"@bedrockio/yada": "^1.1
|
|
41
|
+
"@bedrockio/yada": "^1.2.1",
|
|
42
42
|
"@shelf/jest-mongodb": "^4.3.2",
|
|
43
43
|
"eslint": "^8.33.0",
|
|
44
44
|
"eslint-plugin-bedrock": "^1.0.26",
|
|
45
45
|
"jest": "^29.4.1",
|
|
46
46
|
"jest-environment-node": "^29.4.1",
|
|
47
47
|
"mongodb": "^6.5.0",
|
|
48
|
-
"mongoose": "^8.
|
|
48
|
+
"mongoose": "^8.6.2",
|
|
49
49
|
"prettier-eslint": "^15.0.1",
|
|
50
50
|
"typescript": "^4.9.5"
|
|
51
51
|
},
|
package/src/assign.js
CHANGED
|
@@ -7,7 +7,7 @@ export function applyAssign(schema) {
|
|
|
7
7
|
schema.method('assign', function assign(fields) {
|
|
8
8
|
unsetReferenceFields(fields, schema.obj);
|
|
9
9
|
for (let [path, value] of Object.entries(flattenObject(fields))) {
|
|
10
|
-
if (value === null) {
|
|
10
|
+
if (value === null || value === '') {
|
|
11
11
|
this.set(path, undefined);
|
|
12
12
|
} else {
|
|
13
13
|
this.set(path, value);
|
package/src/search.js
CHANGED
|
@@ -3,7 +3,7 @@ import logger from '@bedrockio/logger';
|
|
|
3
3
|
import mongoose from 'mongoose';
|
|
4
4
|
import { get, pick, isEmpty, escapeRegExp, isPlainObject } from 'lodash';
|
|
5
5
|
|
|
6
|
-
import { isDateField, isNumberField, getField } from './utils';
|
|
6
|
+
import { isArrayField, isDateField, isNumberField, getField } from './utils';
|
|
7
7
|
import { SEARCH_DEFAULTS } from './const';
|
|
8
8
|
import { OBJECT_ID_SCHEMA } from './validation';
|
|
9
9
|
import { debug } from './env';
|
|
@@ -241,6 +241,8 @@ function normalizeQuery(query, schema, root = {}, rootPath = []) {
|
|
|
241
241
|
root[path.join('.')] = parseRegexQuery(value);
|
|
242
242
|
} else if (isArrayQuery(key, value)) {
|
|
243
243
|
root[path.join('.')] = { $in: value };
|
|
244
|
+
} else if (isEmptyArrayQuery(schema, key, value)) {
|
|
245
|
+
root[path.join('.')] = [];
|
|
244
246
|
} else {
|
|
245
247
|
root[path.join('.')] = value;
|
|
246
248
|
}
|
|
@@ -262,12 +264,16 @@ function isArrayQuery(key, value) {
|
|
|
262
264
|
return !isMongoOperator(key) && !isInclude(key) && Array.isArray(value);
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
function isEmptyArrayQuery(schema, key, value) {
|
|
268
|
+
return !isMongoOperator(key) && isArrayField(schema, key) && value === null;
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
function isRangeQuery(schema, key, value) {
|
|
266
272
|
// Range queries only allowed on Date and Number fields.
|
|
267
273
|
if (!isDateField(schema, key) && !isNumberField(schema, key)) {
|
|
268
274
|
return false;
|
|
269
275
|
}
|
|
270
|
-
return typeof value === 'object';
|
|
276
|
+
return typeof value === 'object' && !!value;
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
function mapOperatorQuery(obj) {
|
|
@@ -340,9 +346,7 @@ function applySearchCache(schema, definition) {
|
|
|
340
346
|
if (!force) {
|
|
341
347
|
const $or = Object.keys(cache).map((cachedField) => {
|
|
342
348
|
return {
|
|
343
|
-
[cachedField]:
|
|
344
|
-
$exists: false,
|
|
345
|
-
},
|
|
349
|
+
[cachedField]: null,
|
|
346
350
|
};
|
|
347
351
|
});
|
|
348
352
|
query.$or = $or;
|
package/src/utils.js
CHANGED
|
@@ -36,6 +36,11 @@ export function isNumberField(obj, path) {
|
|
|
36
36
|
return isType(obj, path, 'Number');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function isArrayField(obj, path) {
|
|
40
|
+
const field = getField(obj, path);
|
|
41
|
+
return Array.isArray(field?.type);
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
function isType(obj, path, test) {
|
|
40
45
|
const { type } = getInnerField(obj, path);
|
|
41
46
|
return type === test || type === mongoose.Schema.Types[test];
|
package/src/validation.js
CHANGED
|
@@ -95,7 +95,7 @@ export function applyValidation(schema, definition) {
|
|
|
95
95
|
model: this,
|
|
96
96
|
appendSchema,
|
|
97
97
|
allowInclude,
|
|
98
|
-
|
|
98
|
+
stripEmpty: true,
|
|
99
99
|
stripDeleted: true,
|
|
100
100
|
stripTimestamps: true,
|
|
101
101
|
allowDefaultTags: true,
|
|
@@ -119,7 +119,7 @@ export function applyValidation(schema, definition) {
|
|
|
119
119
|
model: this,
|
|
120
120
|
appendSchema,
|
|
121
121
|
allowInclude,
|
|
122
|
-
|
|
122
|
+
allowNull: true,
|
|
123
123
|
skipRequired: true,
|
|
124
124
|
stripUnknown: true,
|
|
125
125
|
stripDeleted: true,
|
|
@@ -137,21 +137,14 @@ export function applyValidation(schema, definition) {
|
|
|
137
137
|
}
|
|
138
138
|
);
|
|
139
139
|
|
|
140
|
-
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
141
|
-
const allowed = definition.access?.delete || 'all';
|
|
142
|
-
return validateAccess('delete', yd, allowed, {
|
|
143
|
-
model: this,
|
|
144
|
-
message: 'You do not have permissions to delete this document.',
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
140
|
schema.static(
|
|
149
141
|
'getSearchValidation',
|
|
150
142
|
function getSearchValidation(options = {}) {
|
|
151
143
|
const { defaults, includeDeleted, ...appendSchema } = options;
|
|
152
|
-
|
|
153
144
|
return getSchemaFromMongoose(schema, {
|
|
154
145
|
model: this,
|
|
146
|
+
allowNull: true,
|
|
147
|
+
stripEmpty: true,
|
|
155
148
|
allowSearch: true,
|
|
156
149
|
skipRequired: true,
|
|
157
150
|
allowInclude: true,
|
|
@@ -168,6 +161,14 @@ export function applyValidation(schema, definition) {
|
|
|
168
161
|
}
|
|
169
162
|
);
|
|
170
163
|
|
|
164
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
165
|
+
const allowed = definition.access?.delete || 'all';
|
|
166
|
+
return validateAccess('delete', yd, allowed, {
|
|
167
|
+
model: this,
|
|
168
|
+
message: 'You do not have permissions to delete this document.',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
171
172
|
schema.static('getIncludeValidation', function getIncludeValidation() {
|
|
172
173
|
return INCLUDE_FIELD_SCHEMA;
|
|
173
174
|
});
|
|
@@ -238,7 +239,7 @@ function getObjectSchema(arg, options) {
|
|
|
238
239
|
} else if (Array.isArray(arg)) {
|
|
239
240
|
return getArraySchema(arg, options);
|
|
240
241
|
} else if (typeof arg === 'object') {
|
|
241
|
-
const { stripUnknown, expandDotSyntax } = options;
|
|
242
|
+
const { stripUnknown, stripEmpty, expandDotSyntax } = options;
|
|
242
243
|
const map = {};
|
|
243
244
|
for (let [key, field] of Object.entries(arg)) {
|
|
244
245
|
if (!isExcludedField(field, options)) {
|
|
@@ -253,6 +254,13 @@ function getObjectSchema(arg, options) {
|
|
|
253
254
|
stripUnknown: true,
|
|
254
255
|
});
|
|
255
256
|
}
|
|
257
|
+
|
|
258
|
+
if (stripEmpty) {
|
|
259
|
+
schema = schema.options({
|
|
260
|
+
stripEmpty: true,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
256
264
|
if (expandDotSyntax) {
|
|
257
265
|
schema = schema.options({
|
|
258
266
|
expandDotSyntax: true,
|
|
@@ -299,13 +307,24 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
299
307
|
} else {
|
|
300
308
|
schema = getSchemaForType(type, options);
|
|
301
309
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
// Null may be allowed to unset non-required fields
|
|
311
|
+
// in an update operation or to search for non-existent
|
|
312
|
+
// fields in a search operation.
|
|
313
|
+
if (allowNull(typedef, options)) {
|
|
314
|
+
schema = schema.nullable();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Empty strings are allowed to unset non-required fields
|
|
318
|
+
// in an update operation. Technically this should be null,
|
|
319
|
+
// however empty strings are allowed here as well as they
|
|
320
|
+
// generally play nicer with front-end components. For
|
|
321
|
+
// ObjectId fields the empty string must be appended here.
|
|
322
|
+
if (disallowEmpty(typedef, options)) {
|
|
306
323
|
schema = schema.options({
|
|
307
|
-
allowEmpty:
|
|
324
|
+
allowEmpty: false,
|
|
308
325
|
});
|
|
326
|
+
} else if (appendEmpty(typedef, options)) {
|
|
327
|
+
schema = yd.allow(schema, '');
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
330
|
|
|
@@ -352,6 +371,7 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
352
371
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
353
372
|
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
354
373
|
}
|
|
374
|
+
|
|
355
375
|
return schema;
|
|
356
376
|
}
|
|
357
377
|
|
|
@@ -460,12 +480,32 @@ function isRequired(typedef, options) {
|
|
|
460
480
|
return typedef.required && !typedef.default && !options.skipRequired;
|
|
461
481
|
}
|
|
462
482
|
|
|
463
|
-
function
|
|
464
|
-
|
|
483
|
+
function allowNull(typedef, options) {
|
|
484
|
+
if (!options.allowNull) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
const { required, type } = typedef;
|
|
488
|
+
return !required && type !== 'Boolean';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function disallowEmpty(typedef, options) {
|
|
492
|
+
if (!options.allowNull) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
const { type } = typedef;
|
|
496
|
+
if (type === 'String' || type === 'ObjectId') {
|
|
497
|
+
return typedef.required;
|
|
498
|
+
} else {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
465
501
|
}
|
|
466
502
|
|
|
467
|
-
function
|
|
468
|
-
|
|
503
|
+
function appendEmpty(typedef, options) {
|
|
504
|
+
if (!options.allowNull) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const { required, type } = typedef;
|
|
508
|
+
return !required && type === 'ObjectId';
|
|
469
509
|
}
|
|
470
510
|
|
|
471
511
|
function isExcludedField(field, options) {
|
package/types/include.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const INCLUDE_FIELD_SCHEMA: {
|
|
|
18
18
|
strip(strip: any): any;
|
|
19
19
|
allow(...set: any[]): any;
|
|
20
20
|
reject(...set: any[]): any;
|
|
21
|
+
nullable(): any;
|
|
21
22
|
message(message: any): any;
|
|
22
23
|
tag(tags: any): any;
|
|
23
24
|
description(description: any): any;
|
|
@@ -33,8 +34,11 @@ export const INCLUDE_FIELD_SCHEMA: {
|
|
|
33
34
|
} | {
|
|
34
35
|
default: any;
|
|
35
36
|
};
|
|
36
|
-
|
|
37
|
+
getNullable(): {
|
|
38
|
+
nullable: boolean;
|
|
39
|
+
};
|
|
37
40
|
expandExtra(extra?: {}): {};
|
|
41
|
+
inspect(): string;
|
|
38
42
|
assertEnum(set: any, allow: any): any;
|
|
39
43
|
assert(type: any, fn: any): any;
|
|
40
44
|
pushAssertion(assertion: any): void;
|
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;AAGD,yEAUC;AApID
|
|
1
|
+
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,yDAIC;AAGD,yEAUC;AApID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKG"}
|
package/types/search.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function searchValidation(options?: {}): {
|
|
|
15
15
|
strip(strip: any): any;
|
|
16
16
|
allow(...set: any[]): any;
|
|
17
17
|
reject(...set: any[]): any;
|
|
18
|
+
nullable(): any;
|
|
18
19
|
message(message: any): any;
|
|
19
20
|
tag(tags: any): any;
|
|
20
21
|
description(description: any): any;
|
|
@@ -30,8 +31,11 @@ export function searchValidation(options?: {}): {
|
|
|
30
31
|
} | {
|
|
31
32
|
default: any;
|
|
32
33
|
};
|
|
33
|
-
|
|
34
|
+
getNullable(): {
|
|
35
|
+
nullable: boolean;
|
|
36
|
+
};
|
|
34
37
|
expandExtra(extra?: {}): {};
|
|
38
|
+
inspect(): string;
|
|
35
39
|
assertEnum(set: any, allow: any): any;
|
|
36
40
|
assert(type: any, fn: any): any;
|
|
37
41
|
pushAssertion(assertion: any): void;
|
package/types/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAgBA,gEA0DC;AAED
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAgBA,gEA0DC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBC"}
|
package/types/utils.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export function isMongooseSchema(obj: any): boolean;
|
|
|
3
3
|
export function isReferenceField(obj: any, path: any): boolean;
|
|
4
4
|
export function isDateField(obj: any, path: any): boolean;
|
|
5
5
|
export function isNumberField(obj: any, path: any): boolean;
|
|
6
|
+
export function isArrayField(obj: any, path: any): boolean;
|
|
6
7
|
export function isSchemaTypedef(arg: any): boolean;
|
|
7
8
|
export function getField(obj: any, path: any): any;
|
|
8
9
|
export function getInnerField(obj: any, path: any): any;
|
package/types/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAMA,iDAcC;AAED,oDAEC;AAED,+DAEC;AAED,0DAEC;AAED,4DAEC;AAOD,mDAGC;AAuBD,mDAYC;AAKD,wDAEC"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAMA,iDAcC;AAED,oDAEC;AAED,+DAEC;AAED,0DAEC;AAED,4DAEC;AAED,2DAGC;AAOD,mDAGC;AAuBD,mDAYC;AAKD,wDAEC"}
|
package/types/validation.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ export function getTupleValidator(types: any): {
|
|
|
11
11
|
};
|
|
12
12
|
export const OBJECT_ID_SCHEMA: {
|
|
13
13
|
required(): any;
|
|
14
|
-
allowEmpty(): any;
|
|
15
14
|
length(length: number): any;
|
|
16
15
|
min(length: number): any;
|
|
17
16
|
max(length: number): any;
|
|
@@ -77,6 +76,7 @@ export const OBJECT_ID_SCHEMA: {
|
|
|
77
76
|
strip(strip: any): any;
|
|
78
77
|
allow(...set: any[]): any;
|
|
79
78
|
reject(...set: any[]): any;
|
|
79
|
+
nullable(): any;
|
|
80
80
|
message(message: any): any;
|
|
81
81
|
tag(tags: any): any;
|
|
82
82
|
description(description: any): any;
|
|
@@ -93,8 +93,11 @@ export const OBJECT_ID_SCHEMA: {
|
|
|
93
93
|
} | {
|
|
94
94
|
default: any;
|
|
95
95
|
};
|
|
96
|
-
|
|
96
|
+
getNullable(): {
|
|
97
|
+
nullable: boolean;
|
|
98
|
+
};
|
|
97
99
|
expandExtra(extra?: {}): {};
|
|
100
|
+
inspect(): string;
|
|
98
101
|
assertEnum(set: any, allow: any): any;
|
|
99
102
|
assert(type: any, fn: any): any;
|
|
100
103
|
pushAssertion(assertion: any): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAkFA,kDAEC;AAED,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAkFA,kDAEC;AAED,oEAgGC;AAsBD,wEA2BC;AAkVD;;;EAEC;AAED;;;EAOC;AAljBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQK"}
|