Skip to content

Commit 83f94e9

Browse files
Merge pull request #11 from dadi/feature/access-matrix
Add access matrix validation
2 parents c6da45c + c09f957 commit 83f94e9

File tree

3 files changed

+400
-12
lines changed

3 files changed

+400
-12
lines changed

README.md

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,47 @@ const myValidator = new Validator({
3636

3737
The `Validator` class contains the following methods.
3838

39-
### `validateDocument({document, schema})`
39+
### `validateAccessMatrix(matrix, fieldName)`
40+
41+
Validates an access matrix. An optional `fieldName` can be supplied, which will be used in the error objects as the path of the field being validated.
42+
43+
```js
44+
// Rejected Promise:
45+
// > [
46+
// > {"field": "foo.invalidType", "code": "ERROR_INVALID_ACCESS_TYPE"},
47+
// > {"field": "foo.update.invalidKey", "code": "ERROR_INVALID_ACCESS_VALUE"}
48+
// > ]
49+
myValidator
50+
.validateAccessMatrix(
51+
{
52+
create: true,
53+
invalidType: true,
54+
update: {
55+
invalidKey: true
56+
}
57+
},
58+
'foo'
59+
)
60+
.catch(console.log)
61+
62+
// Resolved Promise:
63+
// > undefined
64+
myValidator.validateAccessMatrix({
65+
create: true,
66+
delete: {
67+
filter: {
68+
someField: 'someValue'
69+
}
70+
}
71+
})
72+
```
73+
74+
### `validateDocument({document, isUpdate, schema})`
4075

4176
Validates a document against a collection schema. It returns a Promise that is resolved with `undefined` if no validation errors occur, or rejected with an array of errors if validation fails.
4277

78+
If `isUpdate` is set to `true`, the method does not throw a validation error if a required field is missing from the candidate document, because it is inferred that a partial update, as opposed to a full document, is being evaluated.
79+
4380
```js
4481
const mySchema = {
4582
title: {
@@ -51,7 +88,7 @@ const mySchema = {
5188
}
5289

5390
// Rejected Promise:
54-
// > [{"field": "title", "code": "ERROR_MIN_LENGTH", "message": "is too short"}]
91+
// > [{"field": "title", "code": "ERROR_MIN_LENGTH"}]
5592
myValidator
5693
.validateDocument({
5794
document: {
@@ -75,6 +112,86 @@ myValidator.validateDocument({
75112

76113
Same as `validateDocument` but expects an array of documents (as the `documents` property) and performs validation on each one of them, aborting the process once one of the documents fails validation.
77114

115+
### `validateSchemaField(name, schema)`
116+
117+
Validates a field schema, evaluating whether `name` is a valid field name and whether `schema` is a valid schema object.
118+
119+
```js
120+
// Rejected Promise:
121+
// > [{"field": "fields.title", "code": "ERROR_MISSING_FIELD_TYPE"}]
122+
myValidator
123+
.validateSchemaField('title', {
124+
validation: {
125+
minLength: 10
126+
}
127+
})
128+
.catch(console.log)
129+
130+
// Resolved Promise:
131+
// > undefined
132+
myValidator.validateDocument({
133+
type: 'string',
134+
validation: {
135+
minLength: 10
136+
}
137+
})
138+
```
139+
140+
### `validateSchemaFieldName(name)`
141+
142+
Validates a field name.
143+
144+
```js
145+
// Rejected Promise:
146+
// > [{"field": "fields.$no-good$", "code": "ERROR_INVALID_FIELD_NAME"}]
147+
myValidator.validateSchemaFieldName('$no-good$').catch(console.log)
148+
149+
// Resolved Promise:
150+
// > undefined
151+
myValidator.validateSchemaFieldName('all-good')
152+
```
153+
154+
### `validateSchemaFields(fields)`
155+
156+
Validates an entire `fields` object from a candidate collection schema. It runs `validateSchemaField` on the individual fields.
157+
158+
```js
159+
// Rejected Promise:
160+
// > [{"field": "fields", "code": "ERROR_EMPTY_FIELDS"}]
161+
myValidator.validateSchemaFields({}).catch(console.log)
162+
163+
// Resolved Promise:
164+
// > undefined
165+
myValidator.validateSchemaFieldName({
166+
title: {
167+
type: 'string',
168+
validation: {
169+
minLength: 10
170+
}
171+
}
172+
})
173+
```
174+
175+
### `validateSchemaSettings(settings)`
176+
177+
Validates an entire `settings` object from a candidate collection schema.
178+
179+
```js
180+
// Rejected Promise:
181+
// > [{"field": "fields.displayName", "code": "ERROR_INVALID_SETTING"}]
182+
myValidator
183+
.validateSchemaSettings({
184+
displayName: ['should', 'be', 'a', 'string']
185+
})
186+
.catch(console.log)
187+
188+
// Resolved Promise:
189+
// > undefined
190+
myValidator.validateSchemaSettings({
191+
displayName: 'I am a string'
192+
})
193+
```
194+
78195
### `validateValue({schema, value})`
79196

80197
Validates a candidate value against a field schema. It returns a Promise that is resolved with `undefined` if no validation errors occur, or rejected with an error object if validation fails.
@@ -90,7 +207,7 @@ const mySchema = {
90207
}
91208

92209
// Rejected Promise:
93-
// > {"field": "title", "code": "ERROR_MIN_LENGTH", "message": "is too short"}
210+
// > {"field": "title", "code": "ERROR_MIN_LENGTH"}
94211
myValidator
95212
.validateField({
96213
schema: mySchema,
@@ -106,6 +223,8 @@ myValidator.validateDocument({
106223
})
107224
```
108225

226+
When a `validationCallback` property is found in `schema`, it is used as a callback function that allows the user to supply additional validation logic that will be executed after the normal validation runs. This function should return a Promise that rejects when validation fails, passing down an error object with an optional `code` property that indicates the error code.
227+
109228
## License
110229

111230
DADI is a data centric development and delivery stack, built specifically in support of the principles of API first and COPE.

index.js

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,113 @@ const types = {
1212
}
1313
const ValidationError = require('./lib/validation-error')
1414

15+
const ACCESS_TYPES = [
16+
'delete',
17+
'deleteOwn',
18+
'create',
19+
'read',
20+
'readOwn',
21+
'update',
22+
'updateOwn'
23+
]
24+
1525
class Validator {
1626
constructor({i18nFieldCharacter = ':', internalFieldPrefix = '_'} = {}) {
1727
this.i18nFieldCharacter = i18nFieldCharacter
1828
this.internalFieldPrefix = internalFieldPrefix
1929
}
2030

31+
validateAccessMatrix(matrix, fieldName) {
32+
const fieldPrefix = fieldName ? `${fieldName}.` : ''
33+
const errors = []
34+
35+
if (typeof matrix === 'object') {
36+
Object.keys(matrix).forEach(type => {
37+
if (!ACCESS_TYPES.includes(type)) {
38+
errors.push({
39+
code: 'ERROR_INVALID_ACCESS_TYPE',
40+
field: `${fieldPrefix}${type}`,
41+
message: 'is not a valid access type'
42+
})
43+
}
44+
45+
switch (typeof matrix[type]) {
46+
case 'boolean':
47+
return
48+
49+
case 'object':
50+
Object.keys(matrix[type]).forEach(key => {
51+
if (['fields', 'filter'].includes(key)) {
52+
if (typeof matrix[type][key] !== 'object') {
53+
errors.push({
54+
code: 'ERROR_INVALID_ACCESS_VALUE',
55+
field: `${fieldPrefix}${type}.${key}`,
56+
message:
57+
'is not a valid value for the access type (expected Object)'
58+
})
59+
} else if (key === 'fields') {
60+
const fieldsObj = matrix[type][key]
61+
const fields = Object.keys(fieldsObj)
62+
63+
// A valid fields projection is an object where all fields are
64+
// 0 or 1, never combining the two.
65+
const invalidProjection = fields.some((field, index) => {
66+
if (fieldsObj[field] !== 0 && fieldsObj[field] !== 1) {
67+
return true
68+
}
69+
70+
const nextField = fields[index + 1]
71+
72+
if (
73+
nextField !== undefined &&
74+
fieldsObj[field] !== fieldsObj[nextField]
75+
) {
76+
return true
77+
}
78+
79+
return false
80+
})
81+
82+
if (invalidProjection) {
83+
errors.push({
84+
code: 'ERROR_INVALID_ACCESS_VALUE',
85+
field: `${fieldPrefix}${type}.fields`,
86+
message:
87+
'is not a valid field projection – accepted values for keys are either 0 or 1 and they cannot be combined in the same projection'
88+
})
89+
}
90+
}
91+
} else {
92+
errors.push({
93+
code: 'ERROR_INVALID_ACCESS_VALUE',
94+
field: `${fieldPrefix}${type}.${key}`,
95+
message: 'is not a valid key for an access value'
96+
})
97+
}
98+
})
99+
100+
break
101+
102+
default:
103+
errors.push({
104+
code: 'ERROR_INVALID_ACCESS_VALUE',
105+
field: `${fieldPrefix}${type}`,
106+
message:
107+
'is not a valid access value (expected Boolean or Object)'
108+
})
109+
}
110+
})
111+
} else {
112+
errors.push({
113+
code: 'ERROR_INVALID_ACCESS_MATRIX',
114+
field: fieldName,
115+
message: 'is not a valid access matrix object'
116+
})
117+
}
118+
119+
return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
120+
}
121+
21122
validateDocument({document = {}, isUpdate = false, schema = {}}) {
22123
const errors = []
23124
let chain = Promise.resolve()
@@ -45,17 +146,23 @@ class Validator {
45146
return this.validateValue({
46147
schema: fieldSchema,
47148
value
48-
}).catch(error => {
49-
const errorData = {
50-
field,
51-
message: error.message
52-
}
149+
}).catch(valueError => {
150+
const valueErrors = Array.isArray(valueError)
151+
? valueError
152+
: [valueError]
153+
154+
valueErrors.forEach(error => {
155+
const errorData = {
156+
field: error.field || field,
157+
message: error.message
158+
}
53159

54-
if (typeof error.code === 'string') {
55-
errorData.code = error.code
56-
}
160+
if (typeof error.code === 'string') {
161+
errorData.code = error.code
162+
}
57163

58-
errors.push(errorData)
164+
errors.push(errorData)
165+
})
59166
})
60167
})
61168
})
@@ -383,6 +490,10 @@ class Validator {
383490
return typeHandler({
384491
schema,
385492
value
493+
}).then(() => {
494+
if (typeof schema.validationCallback === 'function') {
495+
return Promise.resolve(schema.validationCallback(value))
496+
}
386497
})
387498
}
388499
}

0 commit comments

Comments
 (0)