Skip to content

Commit

Permalink
Add adapter validation
Browse files Browse the repository at this point in the history
  • Loading branch information
jstayton committed Aug 21, 2019
1 parent ba33293 commit a0960ef
Show file tree
Hide file tree
Showing 10 changed files with 684 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/adapters/base.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const is = require('is')

const AdapterValidator = require('../validators/adapter')
const NotImplementedError = require('../errors/not_implemented')

class BaseAdapter {
constructor() {
this.validator = new AdapterValidator(this.defineValidation.bind(this))
}

static get FILTER_OPERATORS() {
return ['=']
}
Expand All @@ -23,6 +28,10 @@ class BaseAdapter {
throw new NotImplementedError()
}

defineValidation(/* schema */) {
return undefined
}

filter(builder, filter) {
const { operator } = filter
const operatorMethod = `filter:${operator}`
Expand Down
34 changes: 34 additions & 0 deletions src/adapters/knex.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ class KnexAdapter extends BaseAdapter {
]
}

defineValidation(schema) {
return {
'filter:=': schema
.alternatives()
.try([schema.boolean(), schema.number(), schema.string()]),
'filter:!=': schema
.alternatives()
.try([schema.boolean(), schema.number(), schema.string()]),
'filter:<>': schema
.alternatives()
.try([schema.boolean(), schema.number(), schema.string()]),
'filter:>': schema.number(),
'filter:>=': schema.number(),
'filter:<': schema.number(),
'filter:<=': schema.number(),
'filter:is': schema.any().valid(null),
'filter:is not': schema.any().valid(null),
'filter:in': schema.array().items(schema.number(), schema.string()),
'filter:not in': schema.array().items(schema.number(), schema.string()),
'filter:like': schema.string(),
'filter:not like': schema.string(),
'filter:ilike': schema.string(),
'filter:not ilike': schema.string(),
'filter:between': schema
.array()
.length(2)
.items(schema.number()),
'filter:not between': schema
.array()
.length(2)
.items(schema.number()),
}
}

'filter:*'(builder, { field, operator, value }) {
return builder.where(field, operator, value)
}
Expand Down
2 changes: 2 additions & 0 deletions src/orchestrators/filterer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Filterer extends BaseOrchestrator {
return this.querier
}

this.querier.adapter.validator.validateFilters(filters, this.queryKey)

const keys = this.schema.keys()
let filter

Expand Down
2 changes: 2 additions & 0 deletions src/orchestrators/pager.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class Pager extends BaseOrchestrator {
const page = this.parse()

if (page) {
this.querier.adapter.validator.validatePage(page, this.queryKey)

this.apply(page)
}

Expand Down
2 changes: 2 additions & 0 deletions src/orchestrators/sorter.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Sorter extends BaseOrchestrator {
return this.querier
}

this.querier.adapter.validator.validateSorts(sorts, this.queryKey)

for (const [key, sort] of sorts) {
this.apply(sort, key)
}
Expand Down
73 changes: 73 additions & 0 deletions src/validators/adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const Joi = require('@hapi/joi')

const joiValidationErrorConverter = require('../services/joi_validation_error_converter')

class AdapterValidator {
constructor(defineSchema) {
this.schema = defineSchema(Joi)
}

buildError(error, key) {
return joiValidationErrorConverter(error, key)
}

validateValue(schemaKey, key, value) {
if (!this.schema || !this.schema[schemaKey]) {
return true
}

const { error } = this.schema[schemaKey].validate(value)

if (error) {
throw this.buildError(error, key)
}

return true
}

validateFilters(filters, queryKey) {
if (!this.schema) {
return true
}

for (const [key, filter] of filters) {
this.validateValue(
`filter:${filter.operator}`,
`${queryKey}:${key}`,
filter.value
)
}

return true
}

validateSorts(sorts, queryKey) {
const schemaKey = 'sort'

if (!this.schema || !this.schema[schemaKey]) {
return true
}

for (const [key, sort] of sorts) {
this.validateValue(schemaKey, `${queryKey}:${key}`, sort.order)
}

return true
}

validatePage(page, queryKey) {
if (!this.schema) {
return true
}

const entries = Object.entries(page)

for (const [field, value] of entries) {
this.validateValue(`page:${field}`, `${queryKey}:${field}`, value)
}

return true
}
}

module.exports = AdapterValidator
22 changes: 22 additions & 0 deletions test/src/adapters/base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
const BaseAdapter = require('../../../src/adapters/base')
const NotImplementedError = require('../../../src/errors/not_implemented')

describe('constructor', () => {
test('creates an instance of the validator, calls `defineValidation`', () => {
const defineValidation = jest.spyOn(
BaseAdapter.prototype,
'defineValidation'
)

expect(new BaseAdapter().validator.constructor.name).toBe(
'AdapterValidator'
)
expect(defineValidation).toHaveBeenCalled()

defineValidation.mockRestore()
})
})

describe('FILTER_OPERATORS', () => {
test('defaults to requiring `=` operator support', () => {
expect(BaseAdapter.FILTER_OPERATORS).toEqual(['='])
Expand Down Expand Up @@ -31,6 +47,12 @@ describe('page', () => {
})
})

describe('defineValidation', () => {
test('is not defined by default', () => {
expect(new BaseAdapter().defineValidation()).toBeUndefined()
})
})

describe('filter', () => {
test('calls/returns `filter:{operator}` if defined', () => {
const adapter = new BaseAdapter()
Expand Down
Loading

0 comments on commit a0960ef

Please sign in to comment.