Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 122 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,47 @@ const myValidator = new Validator({

The `Validator` class contains the following methods.

### `validateDocument({document, schema})`
### `validateAccessMatrix(matrix, fieldName)`

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.

```js
// Rejected Promise:
// > [
// > {"field": "foo.invalidType", "code": "ERROR_INVALID_ACCESS_TYPE"},
// > {"field": "foo.update.invalidKey", "code": "ERROR_INVALID_ACCESS_VALUE"}
// > ]
myValidator
.validateAccessMatrix(
{
create: true,
invalidType: true,
update: {
invalidKey: true
}
},
'foo'
)
.catch(console.log)

// Resolved Promise:
// > undefined
myValidator.validateAccessMatrix({
create: true,
delete: {
filter: {
someField: 'someValue'
}
}
})
```

### `validateDocument({document, isUpdate, schema})`

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.

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.

```js
const mySchema = {
title: {
Expand All @@ -51,7 +88,7 @@ const mySchema = {
}

// Rejected Promise:
// > [{"field": "title", "code": "ERROR_MIN_LENGTH", "message": "is too short"}]
// > [{"field": "title", "code": "ERROR_MIN_LENGTH"}]
myValidator
.validateDocument({
document: {
Expand All @@ -75,6 +112,86 @@ myValidator.validateDocument({

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.

### `validateSchemaField(name, schema)`

Validates a field schema, evaluating whether `name` is a valid field name and whether `schema` is a valid schema object.

```js
// Rejected Promise:
// > [{"field": "fields.title", "code": "ERROR_MISSING_FIELD_TYPE"}]
myValidator
.validateSchemaField('title', {
validation: {
minLength: 10
}
})
.catch(console.log)

// Resolved Promise:
// > undefined
myValidator.validateDocument({
type: 'string',
validation: {
minLength: 10
}
})
```

### `validateSchemaFieldName(name)`

Validates a field name.

```js
// Rejected Promise:
// > [{"field": "fields.$no-good$", "code": "ERROR_INVALID_FIELD_NAME"}]
myValidator.validateSchemaFieldName('$no-good$').catch(console.log)

// Resolved Promise:
// > undefined
myValidator.validateSchemaFieldName('all-good')
```

### `validateSchemaFields(fields)`

Validates an entire `fields` object from a candidate collection schema. It runs `validateSchemaField` on the individual fields.

```js
// Rejected Promise:
// > [{"field": "fields", "code": "ERROR_EMPTY_FIELDS"}]
myValidator.validateSchemaFields({}).catch(console.log)

// Resolved Promise:
// > undefined
myValidator.validateSchemaFieldName({
title: {
type: 'string',
validation: {
minLength: 10
}
}
})
```

### `validateSchemaSettings(settings)`

Validates an entire `settings` object from a candidate collection schema.

```js
// Rejected Promise:
// > [{"field": "fields.displayName", "code": "ERROR_INVALID_SETTING"}]
myValidator
.validateSchemaSettings({
displayName: ['should', 'be', 'a', 'string']
})
.catch(console.log)

// Resolved Promise:
// > undefined
myValidator.validateSchemaSettings({
displayName: 'I am a string'
})
```

### `validateValue({schema, value})`

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.
Expand All @@ -90,7 +207,7 @@ const mySchema = {
}

// Rejected Promise:
// > {"field": "title", "code": "ERROR_MIN_LENGTH", "message": "is too short"}
// > {"field": "title", "code": "ERROR_MIN_LENGTH"}
myValidator
.validateField({
schema: mySchema,
Expand All @@ -106,6 +223,8 @@ myValidator.validateDocument({
})
```

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.

## License

DADI is a data centric development and delivery stack, built specifically in support of the principles of API first and COPE.
Expand Down
129 changes: 120 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,113 @@ const types = {
}
const ValidationError = require('./lib/validation-error')

const ACCESS_TYPES = [
'delete',
'deleteOwn',
'create',
'read',
'readOwn',
'update',
'updateOwn'
]

class Validator {
constructor({i18nFieldCharacter = ':', internalFieldPrefix = '_'} = {}) {
this.i18nFieldCharacter = i18nFieldCharacter
this.internalFieldPrefix = internalFieldPrefix
}

validateAccessMatrix(matrix, fieldName) {
const fieldPrefix = fieldName ? `${fieldName}.` : ''
const errors = []

if (typeof matrix === 'object') {
Object.keys(matrix).forEach(type => {
if (!ACCESS_TYPES.includes(type)) {
errors.push({
code: 'ERROR_INVALID_ACCESS_TYPE',
field: `${fieldPrefix}${type}`,
message: 'is not a valid access type'
})
}

switch (typeof matrix[type]) {
case 'boolean':
return

case 'object':
Object.keys(matrix[type]).forEach(key => {
if (['fields', 'filter'].includes(key)) {
if (typeof matrix[type][key] !== 'object') {
errors.push({
code: 'ERROR_INVALID_ACCESS_VALUE',
field: `${fieldPrefix}${type}.${key}`,
message:
'is not a valid value for the access type (expected Object)'
})
} else if (key === 'fields') {
const fieldsObj = matrix[type][key]
const fields = Object.keys(fieldsObj)

// A valid fields projection is an object where all fields are
// 0 or 1, never combining the two.
const invalidProjection = fields.some((field, index) => {
if (fieldsObj[field] !== 0 && fieldsObj[field] !== 1) {
return true
}

const nextField = fields[index + 1]

if (
nextField !== undefined &&
fieldsObj[field] !== fieldsObj[nextField]
) {
return true
}

return false
})

if (invalidProjection) {
errors.push({
code: 'ERROR_INVALID_ACCESS_VALUE',
field: `${fieldPrefix}${type}.fields`,
message:
'is not a valid field projection – accepted values for keys are either 0 or 1 and they cannot be combined in the same projection'
})
}
}
} else {
errors.push({
code: 'ERROR_INVALID_ACCESS_VALUE',
field: `${fieldPrefix}${type}.${key}`,
message: 'is not a valid key for an access value'
})
}
})

break

default:
errors.push({
code: 'ERROR_INVALID_ACCESS_VALUE',
field: `${fieldPrefix}${type}`,
message:
'is not a valid access value (expected Boolean or Object)'
})
}
})
} else {
errors.push({
code: 'ERROR_INVALID_ACCESS_MATRIX',
field: fieldName,
message: 'is not a valid access matrix object'
})
}

return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
}

validateDocument({document = {}, isUpdate = false, schema = {}}) {
const errors = []
let chain = Promise.resolve()
Expand Down Expand Up @@ -45,17 +146,23 @@ class Validator {
return this.validateValue({
schema: fieldSchema,
value
}).catch(error => {
const errorData = {
field,
message: error.message
}
}).catch(valueError => {
const valueErrors = Array.isArray(valueError)
? valueError
: [valueError]

valueErrors.forEach(error => {
const errorData = {
field: error.field || field,
message: error.message
}

if (typeof error.code === 'string') {
errorData.code = error.code
}
if (typeof error.code === 'string') {
errorData.code = error.code
}

errors.push(errorData)
errors.push(errorData)
})
})
})
})
Expand Down Expand Up @@ -383,6 +490,10 @@ class Validator {
return typeHandler({
schema,
value
}).then(() => {
if (typeof schema.validationCallback === 'function') {
return Promise.resolve(schema.validationCallback(value))
}
})
}
}
Expand Down
Loading