@bedrockio/model 0.1.4 → 0.1.6
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/dist/cjs/access.js +30 -16
- package/dist/cjs/errors.js +6 -1
- package/dist/cjs/search.js +2 -2
- package/dist/cjs/validation.js +36 -10
- package/package.json +3 -3
- package/src/access.js +35 -17
- package/src/errors.js +7 -1
- package/src/search.js +2 -2
- package/src/validation.js +36 -9
- package/types/access.d.ts.map +1 -1
- package/types/errors.d.ts +2 -0
- package/types/errors.d.ts.map +1 -1
- package/types/search.d.ts +37 -298
- package/types/search.d.ts.map +1 -1
- package/types/validation.d.ts.map +1 -1
- package/yarn-error.log +6270 -0
package/dist/cjs/access.js
CHANGED
|
@@ -29,19 +29,17 @@ function hasAccess(type, allowed = 'all', options = {}) {
|
|
|
29
29
|
document,
|
|
30
30
|
authUser
|
|
31
31
|
} = options;
|
|
32
|
-
if (!Array.isArray(allowed)) {
|
|
33
|
-
allowed = [allowed];
|
|
34
|
-
}
|
|
35
32
|
const scopes = resolveScopes(options);
|
|
33
|
+
allowed = resolveAllowed(allowed);
|
|
36
34
|
return allowed.some(token => {
|
|
37
35
|
if (token === 'self') {
|
|
38
|
-
assertOptions(
|
|
36
|
+
assertOptions(token, options);
|
|
39
37
|
return document.id == authUser.id;
|
|
40
38
|
} else if (token === 'user') {
|
|
41
|
-
assertOptions(
|
|
39
|
+
assertOptions(token, options);
|
|
42
40
|
return document.user?.id == authUser.id;
|
|
43
41
|
} else if (token === 'owner') {
|
|
44
|
-
assertOptions(
|
|
42
|
+
assertOptions(token, options);
|
|
45
43
|
return document.owner?.id == authUser.id;
|
|
46
44
|
} else {
|
|
47
45
|
return scopes?.includes(token);
|
|
@@ -53,18 +51,34 @@ function resolveScopes(options) {
|
|
|
53
51
|
if (!options.scope && !options.scopes) {
|
|
54
52
|
(0, _warn.default)('Scopes were requested but not provided.');
|
|
55
53
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
scopes =
|
|
59
|
-
}
|
|
60
|
-
|
|
54
|
+
let scopes;
|
|
55
|
+
if (options.scopes) {
|
|
56
|
+
scopes = options.scopes;
|
|
57
|
+
} else if (options.scope) {
|
|
58
|
+
scopes = [options.scope];
|
|
59
|
+
} else {
|
|
60
|
+
scopes = [];
|
|
61
|
+
}
|
|
62
|
+
return scopes;
|
|
61
63
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
function resolveAllowed(arg) {
|
|
65
|
+
const allowed = Array.isArray(arg) ? arg : [arg];
|
|
66
|
+
|
|
67
|
+
// Sort allowed scopes to put "self" last allowing
|
|
68
|
+
// role based scopes to be fulfilled first.
|
|
69
|
+
allowed.sort((a, b) => {
|
|
70
|
+
if (a === b) {
|
|
71
|
+
return 0;
|
|
72
|
+
} else if (a === 'self') {
|
|
73
|
+
return 1;
|
|
66
74
|
} else {
|
|
67
|
-
|
|
75
|
+
return -1;
|
|
68
76
|
}
|
|
77
|
+
});
|
|
78
|
+
return allowed;
|
|
79
|
+
}
|
|
80
|
+
function assertOptions(token, options) {
|
|
81
|
+
if (!options.authUser || !options.document) {
|
|
82
|
+
throw new _errors.ImplementationError(token);
|
|
69
83
|
}
|
|
70
84
|
}
|
package/dist/cjs/errors.js
CHANGED
|
@@ -6,7 +6,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.ReferenceError = exports.PermissionsError = exports.ImplementationError = void 0;
|
|
7
7
|
class PermissionsError extends Error {}
|
|
8
8
|
exports.PermissionsError = PermissionsError;
|
|
9
|
-
class ImplementationError extends Error {
|
|
9
|
+
class ImplementationError extends Error {
|
|
10
|
+
constructor(name) {
|
|
11
|
+
super();
|
|
12
|
+
this.name = name;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
10
15
|
exports.ImplementationError = ImplementationError;
|
|
11
16
|
class ReferenceError extends Error {
|
|
12
17
|
constructor(message, references) {
|
package/dist/cjs/search.js
CHANGED
|
@@ -84,7 +84,7 @@ function searchValidation(definition, options = {}) {
|
|
|
84
84
|
sort,
|
|
85
85
|
...rest
|
|
86
86
|
} = options;
|
|
87
|
-
return {
|
|
87
|
+
return _yada.default.object({
|
|
88
88
|
ids: _yada.default.array(_validation.OBJECT_ID_SCHEMA),
|
|
89
89
|
keyword: _yada.default.string().description('A keyword to perform a text search against.'),
|
|
90
90
|
include: _yada.default.string().description('Fields to be selected or populated.'),
|
|
@@ -92,7 +92,7 @@ function searchValidation(definition, options = {}) {
|
|
|
92
92
|
sort: _yada.default.allow(SORT_SCHEMA, _yada.default.array(SORT_SCHEMA)).default(sort),
|
|
93
93
|
limit: _yada.default.number().positive().default(limit).description('Limits the number of results.'),
|
|
94
94
|
...rest
|
|
95
|
-
};
|
|
95
|
+
});
|
|
96
96
|
}
|
|
97
97
|
function validateDefinition(definition) {
|
|
98
98
|
if (Array.isArray(definition.search)) {
|
package/dist/cjs/validation.js
CHANGED
|
@@ -104,6 +104,7 @@ function applyValidation(schema, definition) {
|
|
|
104
104
|
return getSchemaFromMongoose(schema, {
|
|
105
105
|
allowSearch: true,
|
|
106
106
|
skipRequired: true,
|
|
107
|
+
expandDotSyntax: true,
|
|
107
108
|
unwindArrayFields: true,
|
|
108
109
|
requireReadAccess: true,
|
|
109
110
|
appendSchema: (0, _search.searchValidation)(definition, searchOptions),
|
|
@@ -135,8 +136,10 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
135
136
|
} = options;
|
|
136
137
|
let schema = getObjectSchema(attributes, options);
|
|
137
138
|
if (assertUniqueOptions) {
|
|
138
|
-
schema =
|
|
139
|
-
|
|
139
|
+
schema = schema.custom(async (obj, {
|
|
140
|
+
root
|
|
141
|
+
}) => {
|
|
142
|
+
await (0, _softDelete.assertUnique)(root, {
|
|
140
143
|
model: options.model,
|
|
141
144
|
...assertUniqueOptions
|
|
142
145
|
});
|
|
@@ -148,9 +151,6 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
148
151
|
return schema;
|
|
149
152
|
}
|
|
150
153
|
function getObjectSchema(arg, options) {
|
|
151
|
-
const {
|
|
152
|
-
stripUnknown
|
|
153
|
-
} = options;
|
|
154
154
|
if ((0, _utils.isSchemaTypedef)(arg)) {
|
|
155
155
|
return getSchemaForTypedef(arg, options);
|
|
156
156
|
} else if (arg instanceof _mongoose.default.Schema) {
|
|
@@ -158,6 +158,10 @@ function getObjectSchema(arg, options) {
|
|
|
158
158
|
} else if (Array.isArray(arg)) {
|
|
159
159
|
return getArraySchema(arg, options);
|
|
160
160
|
} else if (typeof arg === 'object') {
|
|
161
|
+
const {
|
|
162
|
+
stripUnknown,
|
|
163
|
+
expandDotSyntax
|
|
164
|
+
} = options;
|
|
161
165
|
const map = {};
|
|
162
166
|
for (let [key, field] of Object.entries(arg)) {
|
|
163
167
|
if (!isExcludedField(field, options)) {
|
|
@@ -170,6 +174,11 @@ function getObjectSchema(arg, options) {
|
|
|
170
174
|
stripUnknown: true
|
|
171
175
|
});
|
|
172
176
|
}
|
|
177
|
+
if (expandDotSyntax) {
|
|
178
|
+
schema = schema.options({
|
|
179
|
+
expandDotSyntax: true
|
|
180
|
+
});
|
|
181
|
+
}
|
|
173
182
|
return schema;
|
|
174
183
|
} else {
|
|
175
184
|
return getSchemaForType(arg);
|
|
@@ -324,14 +333,31 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
324
333
|
} = options.model;
|
|
325
334
|
return schema.custom((val, options) => {
|
|
326
335
|
const document = options[(0, _lodash.lowerFirst)(modelName)] || options['document'];
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
336
|
+
let isAllowed;
|
|
337
|
+
try {
|
|
338
|
+
isAllowed = (0, _access.hasAccess)(type, allowed, {
|
|
339
|
+
...options,
|
|
340
|
+
document
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof _errors.ImplementationError) {
|
|
344
|
+
if (type === 'read') {
|
|
345
|
+
// Read access validation for search means that "self" access
|
|
346
|
+
// cannot be fulfilled as there is no single document to test
|
|
347
|
+
// against, so continue on to throw a normal permissions error
|
|
348
|
+
// here instead of raising a problem with the implementation.
|
|
349
|
+
isAllowed = false;
|
|
350
|
+
} else if (type === 'write') {
|
|
351
|
+
throw new Error(`Write access "${error.name}" requires passing { document, authUser } to the validator.`);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
331
357
|
if (!isAllowed) {
|
|
332
358
|
const currentValue = (0, _lodash.get)(document, options.path);
|
|
333
359
|
if (val !== currentValue) {
|
|
334
|
-
throw new _errors.PermissionsError(
|
|
360
|
+
throw new _errors.PermissionsError(`requires ${type} permissions.`);
|
|
335
361
|
}
|
|
336
362
|
}
|
|
337
363
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrockio/model",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"lodash": "^4.17.21"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@bedrockio/yada": "^1.0.
|
|
32
|
+
"@bedrockio/yada": "^1.0.25",
|
|
33
33
|
"mongoose": "^6.9.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@babel/core": "^7.20.12",
|
|
38
38
|
"@babel/preset-env": "^7.20.2",
|
|
39
39
|
"@bedrockio/prettier-config": "^1.0.2",
|
|
40
|
-
"@bedrockio/yada": "^1.0.
|
|
40
|
+
"@bedrockio/yada": "^1.0.25",
|
|
41
41
|
"@shelf/jest-mongodb": "^4.1.6",
|
|
42
42
|
"babel-plugin-import-replacement": "^1.0.1",
|
|
43
43
|
"eslint": "^8.33.0",
|
package/src/access.js
CHANGED
|
@@ -19,19 +19,18 @@ export function hasAccess(type, allowed = 'all', options = {}) {
|
|
|
19
19
|
return false;
|
|
20
20
|
} else {
|
|
21
21
|
const { document, authUser } = options;
|
|
22
|
-
if (!Array.isArray(allowed)) {
|
|
23
|
-
allowed = [allowed];
|
|
24
|
-
}
|
|
25
22
|
const scopes = resolveScopes(options);
|
|
23
|
+
|
|
24
|
+
allowed = resolveAllowed(allowed);
|
|
26
25
|
return allowed.some((token) => {
|
|
27
26
|
if (token === 'self') {
|
|
28
|
-
assertOptions(
|
|
27
|
+
assertOptions(token, options);
|
|
29
28
|
return document.id == authUser.id;
|
|
30
29
|
} else if (token === 'user') {
|
|
31
|
-
assertOptions(
|
|
30
|
+
assertOptions(token, options);
|
|
32
31
|
return document.user?.id == authUser.id;
|
|
33
32
|
} else if (token === 'owner') {
|
|
34
|
-
assertOptions(
|
|
33
|
+
assertOptions(token, options);
|
|
35
34
|
return document.owner?.id == authUser.id;
|
|
36
35
|
} else {
|
|
37
36
|
return scopes?.includes(token);
|
|
@@ -44,20 +43,39 @@ function resolveScopes(options) {
|
|
|
44
43
|
if (!options.scope && !options.scopes) {
|
|
45
44
|
warn('Scopes were requested but not provided.');
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
|
|
47
|
+
let scopes;
|
|
48
|
+
if (options.scopes) {
|
|
49
|
+
scopes = options.scopes;
|
|
50
|
+
} else if (options.scope) {
|
|
51
|
+
scopes = [options.scope];
|
|
52
|
+
} else {
|
|
53
|
+
scopes = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return scopes;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
function resolveAllowed(arg) {
|
|
60
|
+
const allowed = Array.isArray(arg) ? arg : [arg];
|
|
61
|
+
|
|
62
|
+
// Sort allowed scopes to put "self" last allowing
|
|
63
|
+
// role based scopes to be fulfilled first.
|
|
64
|
+
allowed.sort((a, b) => {
|
|
65
|
+
if (a === b) {
|
|
66
|
+
return 0;
|
|
67
|
+
} else if (a === 'self') {
|
|
68
|
+
return 1;
|
|
57
69
|
} else {
|
|
58
|
-
|
|
59
|
-
`Write access "${token}" requires passing { document, authUser } to the validator.`
|
|
60
|
-
);
|
|
70
|
+
return -1;
|
|
61
71
|
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return allowed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertOptions(token, options) {
|
|
78
|
+
if (!options.authUser || !options.document) {
|
|
79
|
+
throw new ImplementationError(token);
|
|
62
80
|
}
|
|
63
81
|
}
|
package/src/errors.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export class PermissionsError extends Error {}
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export class ImplementationError extends Error {
|
|
4
|
+
constructor(name) {
|
|
5
|
+
super();
|
|
6
|
+
this.name = name;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export class ReferenceError extends Error {
|
|
5
11
|
constructor(message, references) {
|
package/src/search.js
CHANGED
|
@@ -85,7 +85,7 @@ export function searchValidation(definition, options = {}) {
|
|
|
85
85
|
|
|
86
86
|
const { limit, sort, ...rest } = options;
|
|
87
87
|
|
|
88
|
-
return {
|
|
88
|
+
return yd.object({
|
|
89
89
|
ids: yd.array(OBJECT_ID_SCHEMA),
|
|
90
90
|
keyword: yd
|
|
91
91
|
.string()
|
|
@@ -99,7 +99,7 @@ export function searchValidation(definition, options = {}) {
|
|
|
99
99
|
.default(limit)
|
|
100
100
|
.description('Limits the number of results.'),
|
|
101
101
|
...rest,
|
|
102
|
-
};
|
|
102
|
+
});
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function validateDefinition(definition) {
|
package/src/validation.js
CHANGED
|
@@ -5,7 +5,7 @@ import { get, omit, lowerFirst } from 'lodash';
|
|
|
5
5
|
|
|
6
6
|
import { hasAccess } from './access';
|
|
7
7
|
import { searchValidation } from './search';
|
|
8
|
-
import { PermissionsError } from './errors';
|
|
8
|
+
import { PermissionsError, ImplementationError } from './errors';
|
|
9
9
|
import { hasUniqueConstraints, assertUnique } from './soft-delete';
|
|
10
10
|
import { isMongooseSchema, isSchemaTypedef } from './utils';
|
|
11
11
|
import { RESERVED_FIELDS } from './schema';
|
|
@@ -114,6 +114,7 @@ export function applyValidation(schema, definition) {
|
|
|
114
114
|
return getSchemaFromMongoose(schema, {
|
|
115
115
|
allowSearch: true,
|
|
116
116
|
skipRequired: true,
|
|
117
|
+
expandDotSyntax: true,
|
|
117
118
|
unwindArrayFields: true,
|
|
118
119
|
requireReadAccess: true,
|
|
119
120
|
appendSchema: searchValidation(definition, searchOptions),
|
|
@@ -142,8 +143,8 @@ export function getValidationSchema(attributes, options = {}) {
|
|
|
142
143
|
const { appendSchema, assertUniqueOptions } = options;
|
|
143
144
|
let schema = getObjectSchema(attributes, options);
|
|
144
145
|
if (assertUniqueOptions) {
|
|
145
|
-
schema =
|
|
146
|
-
await assertUnique(
|
|
146
|
+
schema = schema.custom(async (obj, { root }) => {
|
|
147
|
+
await assertUnique(root, {
|
|
147
148
|
model: options.model,
|
|
148
149
|
...assertUniqueOptions,
|
|
149
150
|
});
|
|
@@ -156,7 +157,6 @@ export function getValidationSchema(attributes, options = {}) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
function getObjectSchema(arg, options) {
|
|
159
|
-
const { stripUnknown } = options;
|
|
160
160
|
if (isSchemaTypedef(arg)) {
|
|
161
161
|
return getSchemaForTypedef(arg, options);
|
|
162
162
|
} else if (arg instanceof mongoose.Schema) {
|
|
@@ -164,6 +164,7 @@ function getObjectSchema(arg, options) {
|
|
|
164
164
|
} else if (Array.isArray(arg)) {
|
|
165
165
|
return getArraySchema(arg, options);
|
|
166
166
|
} else if (typeof arg === 'object') {
|
|
167
|
+
const { stripUnknown, expandDotSyntax } = options;
|
|
167
168
|
const map = {};
|
|
168
169
|
for (let [key, field] of Object.entries(arg)) {
|
|
169
170
|
if (!isExcludedField(field, options)) {
|
|
@@ -178,6 +179,11 @@ function getObjectSchema(arg, options) {
|
|
|
178
179
|
stripUnknown: true,
|
|
179
180
|
});
|
|
180
181
|
}
|
|
182
|
+
if (expandDotSyntax) {
|
|
183
|
+
schema = schema.options({
|
|
184
|
+
expandDotSyntax: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
181
187
|
|
|
182
188
|
return schema;
|
|
183
189
|
} else {
|
|
@@ -362,14 +368,35 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
362
368
|
const { modelName } = options.model;
|
|
363
369
|
return schema.custom((val, options) => {
|
|
364
370
|
const document = options[lowerFirst(modelName)] || options['document'];
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
371
|
+
|
|
372
|
+
let isAllowed;
|
|
373
|
+
try {
|
|
374
|
+
isAllowed = hasAccess(type, allowed, {
|
|
375
|
+
...options,
|
|
376
|
+
document,
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error instanceof ImplementationError) {
|
|
380
|
+
if (type === 'read') {
|
|
381
|
+
// Read access validation for search means that "self" access
|
|
382
|
+
// cannot be fulfilled as there is no single document to test
|
|
383
|
+
// against, so continue on to throw a normal permissions error
|
|
384
|
+
// here instead of raising a problem with the implementation.
|
|
385
|
+
isAllowed = false;
|
|
386
|
+
} else if (type === 'write') {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Write access "${error.name}" requires passing { document, authUser } to the validator.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
369
396
|
if (!isAllowed) {
|
|
370
397
|
const currentValue = get(document, options.path);
|
|
371
398
|
if (val !== currentValue) {
|
|
372
|
-
throw new PermissionsError(
|
|
399
|
+
throw new PermissionsError(`requires ${type} permissions.`);
|
|
373
400
|
}
|
|
374
401
|
}
|
|
375
402
|
});
|
package/types/access.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA,mEAEC;AAED,oEAEC;AAED;;GAEG;AACH,+CAFW,MAAM,GAAC,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA,mEAEC;AAED,oEAEC;AAED;;GAEG;AACH,+CAFW,MAAM,GAAC,MAAM,EAAE,yBA2BzB"}
|
package/types/errors.d.ts
CHANGED
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;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAE9C;IACE,uBAGC;IADC,UAAgB;CAEnB;AAED;IACE,2CAGC;IADC,gBAA4B;CAE/B"}
|