@bedrockio/model 0.1.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 +60 -0
- package/README.md +932 -0
- package/babel.config.cjs +41 -0
- package/dist/cjs/access.js +66 -0
- package/dist/cjs/assign.js +50 -0
- package/dist/cjs/const.js +16 -0
- package/dist/cjs/errors.js +17 -0
- package/dist/cjs/include.js +222 -0
- package/dist/cjs/index.js +62 -0
- package/dist/cjs/load.js +40 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/references.js +104 -0
- package/dist/cjs/schema.js +277 -0
- package/dist/cjs/search.js +266 -0
- package/dist/cjs/serialization.js +55 -0
- package/dist/cjs/slug.js +47 -0
- package/dist/cjs/soft-delete.js +192 -0
- package/dist/cjs/testing.js +33 -0
- package/dist/cjs/utils.js +73 -0
- package/dist/cjs/validation.js +313 -0
- package/dist/cjs/warn.js +13 -0
- package/jest-mongodb-config.js +10 -0
- package/jest.config.js +8 -0
- package/package.json +53 -0
- package/src/access.js +60 -0
- package/src/assign.js +45 -0
- package/src/const.js +9 -0
- package/src/errors.js +9 -0
- package/src/include.js +209 -0
- package/src/index.js +5 -0
- package/src/load.js +37 -0
- package/src/references.js +101 -0
- package/src/schema.js +286 -0
- package/src/search.js +263 -0
- package/src/serialization.js +49 -0
- package/src/slug.js +45 -0
- package/src/soft-delete.js +234 -0
- package/src/testing.js +29 -0
- package/src/utils.js +63 -0
- package/src/validation.js +329 -0
- package/src/warn.js +7 -0
- package/test/assign.test.js +225 -0
- package/test/definitions/custom-model.json +9 -0
- package/test/definitions/special-category.json +18 -0
- package/test/include.test.js +896 -0
- package/test/load.test.js +47 -0
- package/test/references.test.js +71 -0
- package/test/schema.test.js +919 -0
- package/test/search.test.js +652 -0
- package/test/serialization.test.js +748 -0
- package/test/setup.js +27 -0
- package/test/slug.test.js +112 -0
- package/test/soft-delete.test.js +333 -0
- package/test/validation.test.js +1925 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import warn from './warn';
|
|
2
|
+
|
|
3
|
+
export function applySoftDelete(schema) {
|
|
4
|
+
// Implementation
|
|
5
|
+
|
|
6
|
+
schema.pre(/^find|count|exists/, function (next) {
|
|
7
|
+
const filter = this.getFilter();
|
|
8
|
+
if (filter.deleted === undefined) {
|
|
9
|
+
// Search non-deleted docs by default
|
|
10
|
+
filter.deleted = false;
|
|
11
|
+
}
|
|
12
|
+
return next();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Instance Methods
|
|
16
|
+
|
|
17
|
+
schema.method('delete', function () {
|
|
18
|
+
this.deleted = true;
|
|
19
|
+
this.deletedAt = new Date();
|
|
20
|
+
return this.save();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
schema.method('restore', function restore() {
|
|
24
|
+
this.deleted = false;
|
|
25
|
+
this.deletedAt = undefined;
|
|
26
|
+
return this.save();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
schema.method('destroy', function destroy(...args) {
|
|
30
|
+
const filter = { _id: this._id };
|
|
31
|
+
return this.constructor.destroyOne(filter, ...args);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Static Methods
|
|
35
|
+
|
|
36
|
+
schema.static('deleteOne', async function deleteOne(filter, ...rest) {
|
|
37
|
+
const update = getDelete();
|
|
38
|
+
const res = await this.updateOne(filter, update, ...rest);
|
|
39
|
+
return {
|
|
40
|
+
acknowledged: res.acknowledged,
|
|
41
|
+
deletedCount: res.modifiedCount,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
schema.static('deleteMany', async function deleteMany(filter, ...rest) {
|
|
46
|
+
const update = getDelete();
|
|
47
|
+
const res = await this.updateMany(filter, update, ...rest);
|
|
48
|
+
return {
|
|
49
|
+
acknowledged: res.acknowledged,
|
|
50
|
+
deletedCount: res.modifiedCount,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
schema.static(
|
|
55
|
+
'findOneAndDelete',
|
|
56
|
+
async function findOneAndDelete(filter, ...args) {
|
|
57
|
+
return await this.findOneAndUpdate(filter, getDelete(), ...args);
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
schema.static('restoreOne', async function restoreOne(filter, ...rest) {
|
|
62
|
+
const update = getRestore();
|
|
63
|
+
const res = await this.updateOne(filter, update, ...rest);
|
|
64
|
+
return {
|
|
65
|
+
acknowledged: res.acknowledged,
|
|
66
|
+
restoredCount: res.modifiedCount,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
schema.static('restoreMany', async function restoreMany(filter, ...rest) {
|
|
71
|
+
const update = getRestore();
|
|
72
|
+
const res = await this.updateMany(filter, update, ...rest);
|
|
73
|
+
return {
|
|
74
|
+
acknowledged: res.acknowledged,
|
|
75
|
+
restoredCount: res.modifiedCount,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
schema.static('destroyOne', async function destroyOne(...args) {
|
|
80
|
+
const res = await this.collection.deleteOne(...args);
|
|
81
|
+
return {
|
|
82
|
+
acknowledged: res.acknowledged,
|
|
83
|
+
destroyedCount: res.deletedCount,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
schema.static('destroyMany', async function destroyMany(...args) {
|
|
88
|
+
const res = await this.collection.deleteMany(...args);
|
|
89
|
+
return {
|
|
90
|
+
acknowledged: res.acknowledged,
|
|
91
|
+
destroyedCount: res.deletedCount,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
schema.static('findDeleted', function findDeleted(filter) {
|
|
96
|
+
return this.find({
|
|
97
|
+
...filter,
|
|
98
|
+
deleted: true,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
schema.static('findOneDeleted', function findOneDeleted(filter) {
|
|
103
|
+
return this.findOne({
|
|
104
|
+
...filter,
|
|
105
|
+
deleted: true,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
schema.static('findByIdDeleted', function findByIdDeleted(id) {
|
|
110
|
+
return this.findOne({
|
|
111
|
+
_id: id,
|
|
112
|
+
deleted: true,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
schema.static('existsDeleted', function existsDeleted() {
|
|
117
|
+
return this.exists({
|
|
118
|
+
deleted: true,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
schema.static(
|
|
123
|
+
'countDocumentsDeleted',
|
|
124
|
+
function countDocumentsDeleted(filter) {
|
|
125
|
+
return this.countDocuments({
|
|
126
|
+
...filter,
|
|
127
|
+
deleted: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
schema.static('findWithDeleted', function findWithDeleted(filter) {
|
|
133
|
+
return this.find({
|
|
134
|
+
...filter,
|
|
135
|
+
...getWithDeletedQuery(),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
schema.static('findOneWithDeleted', function findOneWithDeleted(filter) {
|
|
140
|
+
return this.findOne({
|
|
141
|
+
...filter,
|
|
142
|
+
...getWithDeletedQuery(),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
schema.static('findByIdWithDeleted', function findByIdWithDeleted(id) {
|
|
147
|
+
return this.findOne({
|
|
148
|
+
_id: id,
|
|
149
|
+
...getWithDeletedQuery(),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
schema.static('existsWithDeleted', function existsWithDeleted() {
|
|
154
|
+
return this.exists({
|
|
155
|
+
...getWithDeletedQuery(),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
schema.static(
|
|
160
|
+
'countDocumentsWithDeleted',
|
|
161
|
+
function countDocumentsWithDeleted(filter) {
|
|
162
|
+
return this.countDocuments({
|
|
163
|
+
...filter,
|
|
164
|
+
...getWithDeletedQuery(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
schema.method(
|
|
170
|
+
'remove',
|
|
171
|
+
function () {
|
|
172
|
+
warn(
|
|
173
|
+
'The "remove" method on documents is disallowed due to ambiguity.',
|
|
174
|
+
'To permanently delete a document use "destroy", otherwise "delete".'
|
|
175
|
+
);
|
|
176
|
+
throw new Error('Method not allowed.');
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
suppressWarning: true,
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
schema.method('deleteOne', function () {
|
|
184
|
+
warn(
|
|
185
|
+
'The "deleteOne" method on documents is disallowed due to ambiguity',
|
|
186
|
+
'Use either "delete" or "deleteOne" on the model.'
|
|
187
|
+
);
|
|
188
|
+
throw new Error('Method not allowed.');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
schema.static('remove', function () {
|
|
192
|
+
warn(
|
|
193
|
+
'The "remove" method on models is disallowed due to ambiguity.',
|
|
194
|
+
'To permanently delete a document use "destroyMany", otherwise "deleteMany".'
|
|
195
|
+
);
|
|
196
|
+
throw new Error('Method not allowed.');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
schema.static('findOneAndRemove', function () {
|
|
200
|
+
warn(
|
|
201
|
+
'The "findOneAndRemove" method on models is disallowed due to ambiguity.',
|
|
202
|
+
'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".'
|
|
203
|
+
);
|
|
204
|
+
throw new Error('Method not allowed.');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
schema.static('findByIdAndRemove', function () {
|
|
208
|
+
warn(
|
|
209
|
+
'The "findByIdAndRemove" method on models is disallowed due to ambiguity.',
|
|
210
|
+
'To permanently delete a document use "findByIdAndDestroy", otherwise "findByIdAndDelete".'
|
|
211
|
+
);
|
|
212
|
+
throw new Error('Method not allowed.');
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getDelete() {
|
|
217
|
+
return {
|
|
218
|
+
deleted: true,
|
|
219
|
+
deletedAt: new Date(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getRestore() {
|
|
224
|
+
return {
|
|
225
|
+
deleted: false,
|
|
226
|
+
$unset: { deletedAt: true },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getWithDeletedQuery() {
|
|
231
|
+
return {
|
|
232
|
+
deleted: { $in: [true, false] },
|
|
233
|
+
};
|
|
234
|
+
}
|
package/src/testing.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
import { createSchema } from './schema';
|
|
4
|
+
import { isMongooseSchema } from './utils';
|
|
5
|
+
|
|
6
|
+
let counter = 0;
|
|
7
|
+
|
|
8
|
+
export function createTestModel(...args) {
|
|
9
|
+
let modelName, attributes, schema;
|
|
10
|
+
if (typeof args[0] === 'string') {
|
|
11
|
+
modelName = args[0];
|
|
12
|
+
attributes = args[1];
|
|
13
|
+
} else {
|
|
14
|
+
attributes = args[0];
|
|
15
|
+
}
|
|
16
|
+
if (isMongooseSchema(attributes)) {
|
|
17
|
+
schema = attributes;
|
|
18
|
+
} else {
|
|
19
|
+
schema = createSchema({
|
|
20
|
+
attributes,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
modelName ||= getTestModelName();
|
|
24
|
+
return mongoose.model(modelName, schema);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getTestModelName() {
|
|
28
|
+
return `TestModel${counter++}`;
|
|
29
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
export function isMongooseSchema(obj) {
|
|
4
|
+
return obj instanceof mongoose.Schema;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isReferenceField(obj, path) {
|
|
8
|
+
return isType(obj, path, 'ObjectId');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isDateField(obj, path) {
|
|
12
|
+
return isType(obj, path, 'Date');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isNumberField(obj, path) {
|
|
16
|
+
return isType(obj, path, 'Number');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isType(obj, path, test) {
|
|
20
|
+
const { type } = resolveInnerField(obj, path);
|
|
21
|
+
return type === test || type === mongoose.Schema.Types[test];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isSchemaTypedef(arg) {
|
|
25
|
+
// Has a type defined and is not a literal type field.
|
|
26
|
+
return arg?.type && !arg.type?.type;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Note: Resolved field may be an object or a function
|
|
30
|
+
// from mongoose.Schema.Types that is resolved from the
|
|
31
|
+
// shorthand: field: 'String'.
|
|
32
|
+
export function resolveField(obj, path) {
|
|
33
|
+
let typedef = obj;
|
|
34
|
+
for (let key of path.split('.')) {
|
|
35
|
+
typedef = resolveFieldForKey(typedef, key);
|
|
36
|
+
}
|
|
37
|
+
return typedef;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The same as resolveField but gets the element
|
|
41
|
+
// typedef in the case of arrays.
|
|
42
|
+
export function resolveInnerField(obj, path) {
|
|
43
|
+
let typedef = resolveField(obj, path);
|
|
44
|
+
if (Array.isArray(typedef.type)) {
|
|
45
|
+
typedef = typedef.type[0];
|
|
46
|
+
}
|
|
47
|
+
return typedef;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveFieldForKey(obj, key) {
|
|
51
|
+
let typedef;
|
|
52
|
+
if (isSchemaTypedef(obj)) {
|
|
53
|
+
const { type } = obj;
|
|
54
|
+
if (Array.isArray(type)) {
|
|
55
|
+
typedef = type[0][key];
|
|
56
|
+
} else {
|
|
57
|
+
typedef = type[key];
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
typedef = obj[key];
|
|
61
|
+
}
|
|
62
|
+
return typedef || {};
|
|
63
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import yd from '@bedrockio/yada';
|
|
3
|
+
|
|
4
|
+
import { get, omit, lowerFirst } from 'lodash';
|
|
5
|
+
|
|
6
|
+
import { hasAccess } from './access';
|
|
7
|
+
import { searchValidation } from './search';
|
|
8
|
+
import { PermissionsError } from './errors';
|
|
9
|
+
import { isMongooseSchema, isSchemaTypedef } from './utils';
|
|
10
|
+
import { RESERVED_FIELDS } from './schema';
|
|
11
|
+
|
|
12
|
+
const namedSchemas = {
|
|
13
|
+
// Email is special as we are assuming that in
|
|
14
|
+
// all cases lowercase should be allowed but coerced.
|
|
15
|
+
email: yd.string().lowercase().email(),
|
|
16
|
+
// Force "ObjectId" to have parity with refs.
|
|
17
|
+
// "mongo" is notably excluded here for this reason.
|
|
18
|
+
ObjectId: yd.string().mongo(),
|
|
19
|
+
|
|
20
|
+
ascii: yd.string().ascii(),
|
|
21
|
+
base64: yd.string().base64(),
|
|
22
|
+
btc: yd.string().btc(),
|
|
23
|
+
country: yd.string().country(),
|
|
24
|
+
creditCard: yd.string().creditCard(),
|
|
25
|
+
domain: yd.string().domain(),
|
|
26
|
+
eth: yd.string().eth(),
|
|
27
|
+
hex: yd.string().hex(),
|
|
28
|
+
ip: yd.string().ip(),
|
|
29
|
+
jwt: yd.string().jwt(),
|
|
30
|
+
latlng: yd.string().latlng(),
|
|
31
|
+
locale: yd.string().locale(),
|
|
32
|
+
md5: yd.string().md5(),
|
|
33
|
+
phone: yd.string().phone(),
|
|
34
|
+
postalCode: yd.string().postalCode(),
|
|
35
|
+
sha1: yd.string().sha1(),
|
|
36
|
+
slug: yd.string().slug(),
|
|
37
|
+
swift: yd.string().swift(),
|
|
38
|
+
url: yd.string().url(),
|
|
39
|
+
uuid: yd.string().uuid(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function addValidators(schemas) {
|
|
43
|
+
Object.assign(namedSchemas, schemas);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function applyValidation(schema, definition) {
|
|
47
|
+
schema.static(
|
|
48
|
+
'getCreateValidation',
|
|
49
|
+
function getCreateValidation(appendSchema) {
|
|
50
|
+
return getSchemaFromMongoose(schema, {
|
|
51
|
+
appendSchema,
|
|
52
|
+
stripReserved: true,
|
|
53
|
+
requireWriteAccess: true,
|
|
54
|
+
modelName: this.modelName,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
schema.static(
|
|
60
|
+
'getUpdateValidation',
|
|
61
|
+
function getUpdateValidation(appendSchema) {
|
|
62
|
+
return getSchemaFromMongoose(schema, {
|
|
63
|
+
appendSchema,
|
|
64
|
+
skipRequired: true,
|
|
65
|
+
stripReserved: true,
|
|
66
|
+
stripUnknown: true,
|
|
67
|
+
requireWriteAccess: true,
|
|
68
|
+
modelName: this.modelName,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
schema.static(
|
|
74
|
+
'getSearchValidation',
|
|
75
|
+
function getSearchValidation(searchOptions) {
|
|
76
|
+
return getSchemaFromMongoose(schema, {
|
|
77
|
+
allowRanges: true,
|
|
78
|
+
skipRequired: true,
|
|
79
|
+
allowMultiple: true,
|
|
80
|
+
unwindArrayFields: true,
|
|
81
|
+
requireReadAccess: true,
|
|
82
|
+
appendSchema: searchValidation(definition, searchOptions),
|
|
83
|
+
modelName: this.modelName,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Yada schemas
|
|
90
|
+
|
|
91
|
+
function getSchemaFromMongoose(schema, options) {
|
|
92
|
+
let { obj } = schema;
|
|
93
|
+
if (options.stripReserved) {
|
|
94
|
+
obj = omit(obj, RESERVED_FIELDS);
|
|
95
|
+
}
|
|
96
|
+
return getValidationSchema(obj, options);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Exported for testing
|
|
100
|
+
export function getValidationSchema(attributes, options = {}) {
|
|
101
|
+
const { appendSchema } = options;
|
|
102
|
+
let schema = getObjectSchema(attributes, options);
|
|
103
|
+
if (appendSchema) {
|
|
104
|
+
schema = schema.append(appendSchema);
|
|
105
|
+
}
|
|
106
|
+
return schema;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getObjectSchema(arg, options) {
|
|
110
|
+
const { stripUnknown } = options;
|
|
111
|
+
if (isSchemaTypedef(arg)) {
|
|
112
|
+
return getSchemaForTypedef(arg, options);
|
|
113
|
+
} else if (arg instanceof mongoose.Schema) {
|
|
114
|
+
return getObjectSchema(arg.obj, options);
|
|
115
|
+
} else if (Array.isArray(arg)) {
|
|
116
|
+
return getArraySchema(arg, options);
|
|
117
|
+
} else if (typeof arg === 'object') {
|
|
118
|
+
const map = {};
|
|
119
|
+
for (let [key, field] of Object.entries(arg)) {
|
|
120
|
+
if (!isExcludedField(field, options)) {
|
|
121
|
+
map[key] = getObjectSchema(field, options);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let schema = yd.object(map);
|
|
126
|
+
|
|
127
|
+
if (stripUnknown) {
|
|
128
|
+
schema = schema.options({
|
|
129
|
+
stripUnknown: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return schema;
|
|
134
|
+
} else {
|
|
135
|
+
return getSchemaForType(arg);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getArraySchema(obj, options) {
|
|
140
|
+
// Nested array fields may not skip required
|
|
141
|
+
// validations as they are a new context.
|
|
142
|
+
let schema = getObjectSchema(obj[0], {
|
|
143
|
+
...options,
|
|
144
|
+
skipRequired: false,
|
|
145
|
+
});
|
|
146
|
+
if (!options.unwindArrayFields) {
|
|
147
|
+
schema = yd.array(schema);
|
|
148
|
+
}
|
|
149
|
+
return schema;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getSchemaForTypedef(typedef, options = {}) {
|
|
153
|
+
let { type } = typedef;
|
|
154
|
+
|
|
155
|
+
let schema;
|
|
156
|
+
|
|
157
|
+
if (isMongooseSchema(type)) {
|
|
158
|
+
schema = getSchemaFromMongoose(type, options);
|
|
159
|
+
} else if (Array.isArray(type)) {
|
|
160
|
+
schema = getArraySchema(type, options);
|
|
161
|
+
} else if (typeof type === 'object') {
|
|
162
|
+
schema = getObjectSchema(type, options);
|
|
163
|
+
} else {
|
|
164
|
+
schema = getSchemaForType(type);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isRequired(typedef, options)) {
|
|
168
|
+
schema = schema.required();
|
|
169
|
+
}
|
|
170
|
+
if (typedef.validate?.schema) {
|
|
171
|
+
schema = schema.append(typedef.validate.schema);
|
|
172
|
+
}
|
|
173
|
+
if (typedef.enum) {
|
|
174
|
+
schema = schema.allow(...typedef.enum);
|
|
175
|
+
}
|
|
176
|
+
if (typedef.match) {
|
|
177
|
+
schema = schema.match(RegExp(typedef.match));
|
|
178
|
+
}
|
|
179
|
+
if (typedef.min != null || typedef.minLength != null) {
|
|
180
|
+
schema = schema.min(typedef.min ?? typedef.minLength);
|
|
181
|
+
}
|
|
182
|
+
if (typedef.max != null || typedef.maxLength != null) {
|
|
183
|
+
schema = schema.max(typedef.max ?? typedef.maxLength);
|
|
184
|
+
}
|
|
185
|
+
if (options.allowRanges) {
|
|
186
|
+
schema = getRangeSchema(schema, type);
|
|
187
|
+
}
|
|
188
|
+
if (options.allowMultiple) {
|
|
189
|
+
schema = yd.allow(schema, yd.array(schema));
|
|
190
|
+
}
|
|
191
|
+
if (typedef.readAccess && options.requireReadAccess) {
|
|
192
|
+
schema = validateReadAccess(schema, typedef.readAccess, options);
|
|
193
|
+
}
|
|
194
|
+
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
195
|
+
schema = validateWriteAccess(schema, typedef.writeAccess, options);
|
|
196
|
+
}
|
|
197
|
+
return schema;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getSchemaForType(type) {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'String':
|
|
203
|
+
return yd.string();
|
|
204
|
+
case 'Number':
|
|
205
|
+
return yd.number();
|
|
206
|
+
case 'Boolean':
|
|
207
|
+
return yd.boolean();
|
|
208
|
+
case 'Date':
|
|
209
|
+
return yd.date().iso();
|
|
210
|
+
case 'Mixed':
|
|
211
|
+
case 'Object':
|
|
212
|
+
return yd.object();
|
|
213
|
+
case 'Array':
|
|
214
|
+
return yd.array();
|
|
215
|
+
case 'ObjectId':
|
|
216
|
+
return yd.custom(async (val) => {
|
|
217
|
+
const id = String(val.id || val);
|
|
218
|
+
await namedSchemas['ObjectId'].validate(id);
|
|
219
|
+
return id;
|
|
220
|
+
});
|
|
221
|
+
default:
|
|
222
|
+
throw new TypeError(`Unknown schema type ${type}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getRangeSchema(schema, type) {
|
|
227
|
+
if (type === 'Number') {
|
|
228
|
+
schema = yd.allow(
|
|
229
|
+
schema,
|
|
230
|
+
yd.object({
|
|
231
|
+
lt: yd.number(),
|
|
232
|
+
gt: yd.number(),
|
|
233
|
+
lte: yd.number(),
|
|
234
|
+
gte: yd.number(),
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
} else if (type === 'Date') {
|
|
238
|
+
return yd.allow(
|
|
239
|
+
schema,
|
|
240
|
+
yd.object({
|
|
241
|
+
lt: yd.date().iso(),
|
|
242
|
+
gt: yd.date().iso(),
|
|
243
|
+
lte: yd.date().iso(),
|
|
244
|
+
gte: yd.date().iso(),
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return schema;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isRequired(typedef, options) {
|
|
252
|
+
return typedef.required && !typedef.default && !options.skipRequired;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isExcludedField(field, options) {
|
|
256
|
+
if (isSchemaTypedef(field)) {
|
|
257
|
+
const { requireWriteAccess } = options;
|
|
258
|
+
return requireWriteAccess && field.writeAccess === 'none';
|
|
259
|
+
} else {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateReadAccess(schema, allowed, options) {
|
|
265
|
+
return validateAccess('read', schema, allowed, options);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function validateWriteAccess(schema, allowed, options) {
|
|
269
|
+
return validateAccess('write', schema, allowed, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateAccess(type, schema, allowed, options) {
|
|
273
|
+
const { modelName } = options;
|
|
274
|
+
return schema.custom((val, options) => {
|
|
275
|
+
const document = options[lowerFirst(modelName)] || options['document'];
|
|
276
|
+
const isAllowed = hasAccess(type, allowed, {
|
|
277
|
+
...options,
|
|
278
|
+
document,
|
|
279
|
+
});
|
|
280
|
+
if (!isAllowed) {
|
|
281
|
+
const currentValue = get(document, options.path);
|
|
282
|
+
if (val !== currentValue) {
|
|
283
|
+
throw new PermissionsError('requires write permissions.');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Mongoose Validators
|
|
290
|
+
|
|
291
|
+
export function getNamedValidator(name) {
|
|
292
|
+
return wrapMongooseValidator(getNamedSchema(name));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function getTupleValidator(types) {
|
|
296
|
+
types = types.map((type) => {
|
|
297
|
+
return getSchemaForTypedef(type);
|
|
298
|
+
});
|
|
299
|
+
return wrapMongooseValidator(yd.array(types).length(types.length));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Returns an async function that will error on failure.
|
|
303
|
+
//
|
|
304
|
+
// Note that mongoose validator functions will not be called
|
|
305
|
+
// if the field is optional and not set or unset with undefined.
|
|
306
|
+
// If the field is not optional the "required" field will also
|
|
307
|
+
// perform valdation so no additional checks are necessary.
|
|
308
|
+
//
|
|
309
|
+
// Also note that throwing an error inside a validator and passing
|
|
310
|
+
// the "message" field result in an identical error message. In this
|
|
311
|
+
// case we want the schema error messages to trickle down so using
|
|
312
|
+
// the first style here.
|
|
313
|
+
//
|
|
314
|
+
// https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-validate
|
|
315
|
+
function wrapMongooseValidator(schema) {
|
|
316
|
+
const validator = async (val) => {
|
|
317
|
+
await schema.validate(val);
|
|
318
|
+
};
|
|
319
|
+
validator.schema = schema;
|
|
320
|
+
return validator;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getNamedSchema(name) {
|
|
324
|
+
const schema = namedSchemas[name];
|
|
325
|
+
if (!schema) {
|
|
326
|
+
throw new Error(`Cannot find schema for "${name}".`);
|
|
327
|
+
}
|
|
328
|
+
return schema;
|
|
329
|
+
}
|