@bedrockio/model 0.2.22 → 0.3.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/README.md +81 -8
- package/dist/cjs/include.js +8 -6
- package/dist/cjs/validation.js +35 -14
- package/package.json +1 -1
- package/src/include.js +10 -6
- package/src/validation.js +34 -16
- package/types/include.d.ts +1 -1
- package/types/include.d.ts.map +1 -1
- package/types/validation.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -472,7 +472,7 @@ In the above example `getCreateValidation` returns a
|
|
|
472
472
|
`validateBody` middleware. The `password` field is an additional field that is
|
|
473
473
|
appended to the create schema.
|
|
474
474
|
|
|
475
|
-
There are
|
|
475
|
+
There are 4 main methods to generate schemas:
|
|
476
476
|
|
|
477
477
|
- `getCreateValidation`: Validates all fields while disallowing reserved fields
|
|
478
478
|
like `id`, `createdAt`, and `updatedAt`.
|
|
@@ -488,6 +488,7 @@ There are 3 main methods to generate schemas:
|
|
|
488
488
|
allowed.
|
|
489
489
|
- Array fields are "unwound". This means that for example given an array field
|
|
490
490
|
`categories`, input may be either a string or an array of strings.
|
|
491
|
+
- `getDeleteValidation`: Only used for access validation (more below).
|
|
491
492
|
|
|
492
493
|
#### Named Validations
|
|
493
494
|
|
|
@@ -752,8 +753,7 @@ queries and more unneeded data transfer over the wire.
|
|
|
752
753
|
|
|
753
754
|
For this reason calling `populate` manually is highly preferable, however in
|
|
754
755
|
complex situations this can easily be a lot of overhead. The include module
|
|
755
|
-
attempts to
|
|
756
|
-
queries:
|
|
756
|
+
attempts to streamline this process by adding an `include` method to queries:
|
|
757
757
|
|
|
758
758
|
```js
|
|
759
759
|
const product = await Product.findById(id).include([
|
|
@@ -905,18 +905,48 @@ The `getSearchValidation` will allow the `include` property to be passed,
|
|
|
905
905
|
letting the client populate documents as they require. Note that the fields a
|
|
906
906
|
client is able to include is subject to [access control](#access-control).
|
|
907
907
|
|
|
908
|
+
#### Other Differences with Populate
|
|
909
|
+
|
|
910
|
+
Calling `populate` on a Mongoose document will always load the current data. In
|
|
911
|
+
contrast, `include` will only load when not yet populated, providing better
|
|
912
|
+
performance for most situations such as pre save hooks:
|
|
913
|
+
|
|
914
|
+
```js
|
|
915
|
+
schema.pre('save', async () => {
|
|
916
|
+
// Will not result in a populate call if the
|
|
917
|
+
// owner document has already been populated.
|
|
918
|
+
await this.include('owner');
|
|
919
|
+
|
|
920
|
+
this.ownerName = this.owner.name;
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
If always fetching the current document is preferred, the `force` option can be
|
|
925
|
+
passed:
|
|
926
|
+
|
|
927
|
+
```js
|
|
928
|
+
await shop.include('owner', {
|
|
929
|
+
force: true,
|
|
930
|
+
});
|
|
931
|
+
```
|
|
932
|
+
|
|
908
933
|
### Access Control
|
|
909
934
|
|
|
910
935
|
This package applies two forms of access control:
|
|
911
936
|
|
|
912
|
-
|
|
937
|
+
- [Field Access](#field-access)
|
|
938
|
+
- [Document Access](#document-access)
|
|
939
|
+
|
|
940
|
+
#### Field Access:
|
|
941
|
+
|
|
942
|
+
##### Read Access
|
|
913
943
|
|
|
914
944
|
Read access influences how documents are serialized. Fields that have been
|
|
915
945
|
denied access will be stripped out. Additionally it will influence the
|
|
916
946
|
validation schema for `getSearchValidation`. Fields that have been denied access
|
|
917
947
|
are not allowed to be searched on and will throw an error.
|
|
918
948
|
|
|
919
|
-
|
|
949
|
+
##### Write Access
|
|
920
950
|
|
|
921
951
|
Write access influences validation in `getCreateValidation` and
|
|
922
952
|
`getUpdateValidation`. Fields that have been denied access will throw an error
|
|
@@ -924,7 +954,7 @@ unless they are identical to what is already set on the document. Note that in
|
|
|
924
954
|
the case of `getCreateValidation` no document has been created yet so a denied
|
|
925
955
|
field will always result in an error if passed.
|
|
926
956
|
|
|
927
|
-
|
|
957
|
+
##### Defining Field Access
|
|
928
958
|
|
|
929
959
|
Access is defined in schemas with the `readAccess` and `writeAccess` options:
|
|
930
960
|
|
|
@@ -1098,7 +1128,7 @@ owner, as this example illustrates:
|
|
|
1098
1128
|
}
|
|
1099
1129
|
```
|
|
1100
1130
|
|
|
1101
|
-
|
|
1131
|
+
##### Notes on Read Access
|
|
1102
1132
|
|
|
1103
1133
|
Note that all forms of read access require that `.toObject` is called on the
|
|
1104
1134
|
document with special parameters, however this method is called on internal
|
|
@@ -1107,11 +1137,54 @@ this reason it will never fail even if it cannot perform the correct access
|
|
|
1107
1137
|
checks. Instead any fields with `readAccess` defined on them will be stripped
|
|
1108
1138
|
out.
|
|
1109
1139
|
|
|
1110
|
-
|
|
1140
|
+
##### Notes on Write Access
|
|
1111
1141
|
|
|
1112
1142
|
Note that `self` is generally only meaningful on a User model as it will always
|
|
1113
1143
|
check the document is the same as `authUser`.
|
|
1114
1144
|
|
|
1145
|
+
#### Document Access
|
|
1146
|
+
|
|
1147
|
+
In addition to the fine grained control of accessing fields, documents
|
|
1148
|
+
themselves may also have access control. This can be defined in the `access` key
|
|
1149
|
+
of the model definition:
|
|
1150
|
+
|
|
1151
|
+
```jsonc
|
|
1152
|
+
// user.json
|
|
1153
|
+
{
|
|
1154
|
+
"attributes": {
|
|
1155
|
+
// ...
|
|
1156
|
+
},
|
|
1157
|
+
"access": {
|
|
1158
|
+
// A user may update themselves or an admin.
|
|
1159
|
+
"update": ["self", "admin"],
|
|
1160
|
+
// Only an admin may delete a user.
|
|
1161
|
+
"delete": ["admin"]
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
The same options can be used as
|
|
1167
|
+
[document based access on fields](#document-based-access), so this could be
|
|
1168
|
+
`owner`, etc:
|
|
1169
|
+
|
|
1170
|
+
```jsonc
|
|
1171
|
+
// shop.json
|
|
1172
|
+
{
|
|
1173
|
+
"attributes": {
|
|
1174
|
+
"owner": {
|
|
1175
|
+
"type": "ObjectId",
|
|
1176
|
+
"ref": "User"
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
"access": {
|
|
1180
|
+
// An owner may update their own shop.
|
|
1181
|
+
"update": ["owner", "admin"],
|
|
1182
|
+
// Only an admin may delete a shop.
|
|
1183
|
+
"delete": ["admin"]
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1115
1188
|
### Delete Hooks
|
|
1116
1189
|
|
|
1117
1190
|
Delete hooks are a powerful way to define what actions are taken on document
|
package/dist/cjs/include.js
CHANGED
|
@@ -99,12 +99,12 @@ function applyInclude(schema) {
|
|
|
99
99
|
// Perform population immediately when instance method is called.
|
|
100
100
|
// Store selects as a local variable which will be checked
|
|
101
101
|
// during serialization.
|
|
102
|
-
schema.method('include', async function include(include) {
|
|
102
|
+
schema.method('include', async function include(include, options) {
|
|
103
103
|
if (include) {
|
|
104
104
|
const {
|
|
105
105
|
select,
|
|
106
106
|
populate
|
|
107
|
-
} = getDocumentParams(this, include);
|
|
107
|
+
} = getDocumentParams(this, include, options);
|
|
108
108
|
this.$locals.select = select;
|
|
109
109
|
await this.populate(populate);
|
|
110
110
|
}
|
|
@@ -155,11 +155,13 @@ function getParams(modelName, arg) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// Exported for testing.
|
|
158
|
-
function getDocumentParams(doc, arg) {
|
|
158
|
+
function getDocumentParams(doc, arg, options = {}) {
|
|
159
159
|
const params = getParams(doc.constructor.modelName, arg);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
if (!options.force) {
|
|
161
|
+
params.populate = params.populate.filter(p => {
|
|
162
|
+
return !isDocumentPopulated(doc, p);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
163
165
|
return params;
|
|
164
166
|
}
|
|
165
167
|
function isDocumentPopulated(doc, params) {
|
package/dist/cjs/validation.js
CHANGED
|
@@ -112,6 +112,7 @@ function applyValidation(schema, definition) {
|
|
|
112
112
|
stripTimestamps: true,
|
|
113
113
|
allowExpandedRefs: true,
|
|
114
114
|
requireWriteAccess: true,
|
|
115
|
+
updateAccess: definition.access?.update,
|
|
115
116
|
...(hasUnique && {
|
|
116
117
|
assertUniqueOptions: {
|
|
117
118
|
schema,
|
|
@@ -120,6 +121,13 @@ function applyValidation(schema, definition) {
|
|
|
120
121
|
})
|
|
121
122
|
});
|
|
122
123
|
});
|
|
124
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
125
|
+
const allowed = definition.access?.delete || 'all';
|
|
126
|
+
return validateAccess('delete', _yada.default, allowed, {
|
|
127
|
+
model: this,
|
|
128
|
+
message: 'You do not have permissions to delete this document.'
|
|
129
|
+
});
|
|
130
|
+
});
|
|
123
131
|
schema.static('getSearchValidation', function getSearchValidation(options = {}) {
|
|
124
132
|
const {
|
|
125
133
|
defaults,
|
|
@@ -180,7 +188,8 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
180
188
|
const {
|
|
181
189
|
appendSchema,
|
|
182
190
|
assertUniqueOptions,
|
|
183
|
-
allowInclude
|
|
191
|
+
allowInclude,
|
|
192
|
+
updateAccess
|
|
184
193
|
} = options;
|
|
185
194
|
let schema = getObjectSchema(attributes, options);
|
|
186
195
|
if (assertUniqueOptions) {
|
|
@@ -199,6 +208,12 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
199
208
|
if (allowInclude) {
|
|
200
209
|
schema = schema.append(_include.INCLUDE_FIELD_SCHEMA);
|
|
201
210
|
}
|
|
211
|
+
if (updateAccess) {
|
|
212
|
+
return validateAccess('update', schema, updateAccess, {
|
|
213
|
+
...options,
|
|
214
|
+
message: 'You do not have permissions to update this document.'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
202
217
|
return schema;
|
|
203
218
|
}
|
|
204
219
|
function getObjectSchema(arg, options) {
|
|
@@ -304,10 +319,10 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
304
319
|
schema = getSearchSchema(schema, type);
|
|
305
320
|
}
|
|
306
321
|
if (typedef.readAccess && options.requireReadAccess) {
|
|
307
|
-
schema =
|
|
322
|
+
schema = validateAccess('read', schema, typedef.readAccess, options);
|
|
308
323
|
}
|
|
309
324
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
310
|
-
schema =
|
|
325
|
+
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
311
326
|
}
|
|
312
327
|
return schema;
|
|
313
328
|
}
|
|
@@ -392,13 +407,10 @@ function isExcludedField(field, options) {
|
|
|
392
407
|
}
|
|
393
408
|
return false;
|
|
394
409
|
}
|
|
395
|
-
function validateReadAccess(schema, allowed, options) {
|
|
396
|
-
return validateAccess('read', schema, allowed, options);
|
|
397
|
-
}
|
|
398
|
-
function validateWriteAccess(schema, allowed, options) {
|
|
399
|
-
return validateAccess('write', schema, allowed, options);
|
|
400
|
-
}
|
|
401
410
|
function validateAccess(type, schema, allowed, options) {
|
|
411
|
+
let {
|
|
412
|
+
message
|
|
413
|
+
} = options;
|
|
402
414
|
const {
|
|
403
415
|
modelName
|
|
404
416
|
} = options.model;
|
|
@@ -418,18 +430,27 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
418
430
|
// against, so continue on to throw a normal permissions error
|
|
419
431
|
// here instead of raising a problem with the implementation.
|
|
420
432
|
isAllowed = false;
|
|
421
|
-
} else
|
|
422
|
-
throw new Error(`
|
|
433
|
+
} else {
|
|
434
|
+
throw new Error(`Access validation "${error.name}" requires passing { document, authUser } to the validator.`);
|
|
423
435
|
}
|
|
424
436
|
} else {
|
|
425
437
|
throw error;
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
if (!isAllowed) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
const {
|
|
442
|
+
path
|
|
443
|
+
} = options;
|
|
444
|
+
if (path) {
|
|
445
|
+
if ((0, _lodash.get)(document, path) === val) {
|
|
446
|
+
// If there is a path being accessed and the current
|
|
447
|
+
// value is the same as what is being set then do not
|
|
448
|
+
// throw the error.
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
message ||= `Field "${path.join('.')}" requires ${type} permissions.`;
|
|
432
452
|
}
|
|
453
|
+
throw new _errors.PermissionsError(message);
|
|
433
454
|
}
|
|
434
455
|
});
|
|
435
456
|
}
|
package/package.json
CHANGED
package/src/include.js
CHANGED
|
@@ -89,9 +89,9 @@ export function applyInclude(schema) {
|
|
|
89
89
|
// Perform population immediately when instance method is called.
|
|
90
90
|
// Store selects as a local variable which will be checked
|
|
91
91
|
// during serialization.
|
|
92
|
-
schema.method('include', async function include(include) {
|
|
92
|
+
schema.method('include', async function include(include, options) {
|
|
93
93
|
if (include) {
|
|
94
|
-
const { select, populate } = getDocumentParams(this, include);
|
|
94
|
+
const { select, populate } = getDocumentParams(this, include, options);
|
|
95
95
|
this.$locals.select = select;
|
|
96
96
|
await this.populate(populate);
|
|
97
97
|
}
|
|
@@ -140,11 +140,15 @@ export function getParams(modelName, arg) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// Exported for testing.
|
|
143
|
-
export function getDocumentParams(doc, arg) {
|
|
143
|
+
export function getDocumentParams(doc, arg, options = {}) {
|
|
144
144
|
const params = getParams(doc.constructor.modelName, arg);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
|
|
146
|
+
if (!options.force) {
|
|
147
|
+
params.populate = params.populate.filter((p) => {
|
|
148
|
+
return !isDocumentPopulated(doc, p);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
return params;
|
|
149
153
|
}
|
|
150
154
|
|
package/src/validation.js
CHANGED
|
@@ -125,6 +125,7 @@ export function applyValidation(schema, definition) {
|
|
|
125
125
|
stripTimestamps: true,
|
|
126
126
|
allowExpandedRefs: true,
|
|
127
127
|
requireWriteAccess: true,
|
|
128
|
+
updateAccess: definition.access?.update,
|
|
128
129
|
...(hasUnique && {
|
|
129
130
|
assertUniqueOptions: {
|
|
130
131
|
schema,
|
|
@@ -135,6 +136,14 @@ export function applyValidation(schema, definition) {
|
|
|
135
136
|
}
|
|
136
137
|
);
|
|
137
138
|
|
|
139
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
140
|
+
const allowed = definition.access?.delete || 'all';
|
|
141
|
+
return validateAccess('delete', yd, allowed, {
|
|
142
|
+
model: this,
|
|
143
|
+
message: 'You do not have permissions to delete this document.',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
138
147
|
schema.static(
|
|
139
148
|
'getSearchValidation',
|
|
140
149
|
function getSearchValidation(options = {}) {
|
|
@@ -192,7 +201,8 @@ function getMongooseFields(schema, options) {
|
|
|
192
201
|
|
|
193
202
|
// Exported for testing
|
|
194
203
|
export function getValidationSchema(attributes, options = {}) {
|
|
195
|
-
const { appendSchema, assertUniqueOptions, allowInclude } =
|
|
204
|
+
const { appendSchema, assertUniqueOptions, allowInclude, updateAccess } =
|
|
205
|
+
options;
|
|
196
206
|
let schema = getObjectSchema(attributes, options);
|
|
197
207
|
if (assertUniqueOptions) {
|
|
198
208
|
schema = schema.custom(async (obj, { root }) => {
|
|
@@ -208,6 +218,14 @@ export function getValidationSchema(attributes, options = {}) {
|
|
|
208
218
|
if (allowInclude) {
|
|
209
219
|
schema = schema.append(INCLUDE_FIELD_SCHEMA);
|
|
210
220
|
}
|
|
221
|
+
|
|
222
|
+
if (updateAccess) {
|
|
223
|
+
return validateAccess('update', schema, updateAccess, {
|
|
224
|
+
...options,
|
|
225
|
+
message: 'You do not have permissions to update this document.',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
211
229
|
return schema;
|
|
212
230
|
}
|
|
213
231
|
|
|
@@ -321,10 +339,10 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
321
339
|
schema = getSearchSchema(schema, type);
|
|
322
340
|
}
|
|
323
341
|
if (typedef.readAccess && options.requireReadAccess) {
|
|
324
|
-
schema =
|
|
342
|
+
schema = validateAccess('read', schema, typedef.readAccess, options);
|
|
325
343
|
}
|
|
326
344
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
327
|
-
schema =
|
|
345
|
+
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
328
346
|
}
|
|
329
347
|
return schema;
|
|
330
348
|
}
|
|
@@ -449,15 +467,8 @@ function isExcludedField(field, options) {
|
|
|
449
467
|
return false;
|
|
450
468
|
}
|
|
451
469
|
|
|
452
|
-
function validateReadAccess(schema, allowed, options) {
|
|
453
|
-
return validateAccess('read', schema, allowed, options);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function validateWriteAccess(schema, allowed, options) {
|
|
457
|
-
return validateAccess('write', schema, allowed, options);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
470
|
function validateAccess(type, schema, allowed, options) {
|
|
471
|
+
let { message } = options;
|
|
461
472
|
const { modelName } = options.model;
|
|
462
473
|
return schema.custom((val, options) => {
|
|
463
474
|
const document = options[lowerFirst(modelName)] || options['document'];
|
|
@@ -476,9 +487,9 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
476
487
|
// against, so continue on to throw a normal permissions error
|
|
477
488
|
// here instead of raising a problem with the implementation.
|
|
478
489
|
isAllowed = false;
|
|
479
|
-
} else
|
|
490
|
+
} else {
|
|
480
491
|
throw new Error(
|
|
481
|
-
`
|
|
492
|
+
`Access validation "${error.name}" requires passing { document, authUser } to the validator.`
|
|
482
493
|
);
|
|
483
494
|
}
|
|
484
495
|
} else {
|
|
@@ -487,10 +498,17 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
487
498
|
}
|
|
488
499
|
|
|
489
500
|
if (!isAllowed) {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
501
|
+
const { path } = options;
|
|
502
|
+
if (path) {
|
|
503
|
+
if (get(document, path) === val) {
|
|
504
|
+
// If there is a path being accessed and the current
|
|
505
|
+
// value is the same as what is being set then do not
|
|
506
|
+
// throw the error.
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
message ||= `Field "${path.join('.')}" requires ${type} permissions.`;
|
|
493
510
|
}
|
|
511
|
+
throw new PermissionsError(message);
|
|
494
512
|
}
|
|
495
513
|
});
|
|
496
514
|
}
|
package/types/include.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function applyInclude(schema: any): void;
|
|
2
2
|
export function checkSelects(doc: any, ret: any): void;
|
|
3
3
|
export function getParams(modelName: any, arg: any): any;
|
|
4
|
-
export function getDocumentParams(doc: any, arg: any): any;
|
|
4
|
+
export function getDocumentParams(doc: any, arg: any, options?: {}): any;
|
|
5
5
|
export const INCLUDE_FIELD_SCHEMA: {
|
|
6
6
|
setup: any;
|
|
7
7
|
getFields: 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;AAGD,
|
|
1
|
+
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,yDAIC;AAGD,yEAUC;AApID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKG"}
|
|
@@ -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,oEA8FC;AAsBD,wEA2BC;AAgSD;;;EAEC;AAED;;;EAOC;AA9fD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQK"}
|