-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial commit. validate ALL the things
- Loading branch information
0 parents
commit 91c40c9
Showing
11 changed files
with
721 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Tells the .editorconfg plugin to stop searching once it finds this file | ||
root = true | ||
|
||
[*] | ||
indent_size = 2 | ||
indent_style = space | ||
charset = utf-8 | ||
end_of_line = lf | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true | ||
|
||
[*.md] | ||
trim_trailing_whitespace = false | ||
|
||
[*.py] | ||
indent_size = 4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"env": { | ||
"node": true, | ||
"mocha": true, | ||
"es6": true | ||
}, | ||
"globals": { | ||
"chai": true, | ||
"expect": true, | ||
"sinon": true | ||
}, | ||
"rules": { | ||
"array-bracket-spacing": [2, "never"], | ||
"arrow-body-style": 0, | ||
"arrow-parens": 1, | ||
"brace-style": [2, "1tbs"], | ||
"camelcase": 2, | ||
"comma-dangle": 2, | ||
"comma-spacing": 2, | ||
"computed-property-spacing": [2, "never"], | ||
"curly": 2, | ||
"dot-notation": 2, | ||
"eol-last": 0, | ||
"eqeqeq": [2, "smart"], | ||
"no-extra-parens": 1, | ||
"no-floating-decimal": 2, | ||
"no-debugger": 2, | ||
"no-redeclare": 2, | ||
"no-spaced-func": 2, | ||
"no-undef": 2, | ||
"no-underscore-dangle": 0, | ||
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}], | ||
"no-use-before-define": [1, "nofunc"], | ||
"no-warning-comments": 0, | ||
"object-curly-spacing": [2, "never"], | ||
"quotes": [2, "single"], | ||
"semi": [2, "always"], | ||
"keyword-spacing": [2, { "after": true }], | ||
"space-infix-ops": 2, | ||
"strict": 0, | ||
"valid-jsdoc": 0 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.DS_Store | ||
node_modules | ||
*.log | ||
coverage | ||
.nyc_output |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
language: node_js | ||
|
||
node_js: | ||
- "7" | ||
- "6" | ||
- "5" | ||
- "4" | ||
|
||
after_success: | ||
- npm run coveralls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
# express-joi-validation | ||
|
||
 [](https://badge.fury.io/js/express-joi-validation) [](https://coveralls.io/github/evanshortiss/express-joi-validation?branch=master) | ||
|
||
A middleware for validating express inputs using Joi schemas. Fills some of the | ||
voids I found that other Joi middleware miss: | ||
|
||
* Allows the developers to specify the order in which request inputs are validated in a clear manner. | ||
* Replaces the `req.body` and others with converted Joi values. The same applies for headers, query, and params, but... | ||
* Retains the original `req.body` inside a new property named `req.originalBody`. The same applies for headers, query, and params using the `original` prefix. | ||
* Passes sensible default options to Joi for headers, params, query, and body. These are detailed below. | ||
* Uses `peerDependencies` to get a Joi instance of your choosing instead of | ||
using a fixed version. | ||
|
||
|
||
|
||
## Install | ||
|
||
``` | ||
# remember, you need to install joi too | ||
npm i express-joi-validation joi --save | ||
``` | ||
|
||
|
||
## Usage | ||
|
||
```js | ||
const Joi = require('joi'); | ||
const validator = require('express-joi-validation')({}); | ||
|
||
const app = require('express')(); | ||
const orders = require('lib/orders'); | ||
|
||
const querySchema = Joi.object({ | ||
type: Joi.string().required().valid('food', 'drinks', 'entertainment'), | ||
from: Joi.date().iso().required(), | ||
to: Joi.date().iso().min(Joi.ref('from')).required() | ||
}); | ||
|
||
app.get('/orders', validator.query(querySchema), (req, res, next) => { | ||
console.log( | ||
`Compare the incoming query ${JSON.stringify(req.originalQuery)} vs. the sanatised query ${JSON.stringify(req.query)}` | ||
); | ||
// if we're in here then the query was valid! | ||
orders.getForQuery(req.query) | ||
.then((listOfOrders) => res.json(listOfOrders)) | ||
.catch(next); | ||
}); | ||
``` | ||
|
||
|
||
## Behaviours | ||
|
||
### Joi Versioning | ||
This module uses `peerDependencies` for the Joi version being used. This means | ||
whatever Joi version is in the `dependencies` of your `package.json` will be used by this module. | ||
|
||
### Validation Ordering | ||
If you'd like to validate different request inputs in differing orders it's | ||
simple, just define the the middleware in the order desired. | ||
|
||
Here's an example where we do headers, body, and finally the query: | ||
|
||
```js | ||
// verify headers, then body, then query | ||
route.get( | ||
'/tickets', | ||
validator.headers(headerSchema), | ||
validator.body(bodySchema), | ||
validator.query(querySchema), | ||
routeHandler | ||
); | ||
``` | ||
|
||
### Error Handling | ||
When validation fails, this module will default to returning a HTTP 400 with | ||
the Joi validation error as a `text/plain` response type. | ||
|
||
A `passError` option is supported to override this behaviour, and force the | ||
middleware to pass the error to the express error handler you've defined. | ||
|
||
### Joi Options | ||
It is possible to pass specific Joi options to each validator like so: | ||
|
||
```js | ||
route.get( | ||
'/tickets', | ||
validator.headers( | ||
headerSchema, | ||
{ | ||
joi: {convert: true, allowUnknown: true} | ||
} | ||
), | ||
validator.body( | ||
bodySchema, | ||
{ | ||
joi: {convert: true, allowUnknown: false} | ||
} | ||
) | ||
routeHandler | ||
); | ||
``` | ||
|
||
The following sensible defaults are applied if you pass none: | ||
|
||
#### Query | ||
* convert: true | ||
* allowUnknown: false | ||
* abortEarly: false | ||
|
||
#### Body | ||
* convert: true | ||
* allowUnknown: false | ||
* abortEarly: false | ||
|
||
#### Headers | ||
* convert: true | ||
* allowUnknown: true | ||
* stripUnknown: false | ||
* abortEarly: false | ||
|
||
#### Route Params | ||
* convert: true | ||
* allowUnknown: false | ||
* abortEarly: false | ||
|
||
|
||
## API | ||
|
||
### module(config) | ||
|
||
A factory function an instance of the module for use. Can pass the following | ||
options: | ||
|
||
* passError - Set this to true if you'd like validation errors to get passed | ||
to the express error handler so you can handle them manually vs. the default behaviour that returns a 400. | ||
* statusCode - The status code to use when validation fails and _passError_ | ||
is false. Default is 400. | ||
|
||
### Instance Functions | ||
|
||
Each instance function can be passed an options Object with the following: | ||
|
||
* joi - Custom options to pass to `Joi.validate`. | ||
* passError - Same as above. | ||
* statusCode - Same as above. | ||
|
||
#### instance.query(schema, [options]) | ||
Create a middleware instance that will validate the query for an incoming | ||
request. Can be passed `options` that override the options passed when the | ||
instance was created. | ||
|
||
#### instance.body(schema, [options]) | ||
Create a middleware instance that will validate the body for an incoming | ||
request. Can be passed `options` that override the options passed when the | ||
instance was created. | ||
|
||
#### instance.headers(schema, [options]) | ||
Create a middleware instance that will validate the headers for an incoming | ||
request. Can be passed `options` that override the options passed when the | ||
instance was created. | ||
|
||
#### instance.params(schema, [options]) | ||
Create a middleware instance that will validate the params for an incoming | ||
request. Can be passed `options` that override the options passed when the | ||
instance was created. | ||
|
||
The `instance.params` middleware is a little different to the others. It _must_ | ||
be attached directly to the route it is related to. Here's a sample: | ||
|
||
```js | ||
|
||
const schema = Joi.object({ | ||
id: Joi.number().integer().required() | ||
}); | ||
|
||
// INCORRECT | ||
app.use(validator.params(schema)); | ||
app.get('/orders/:id', (req, res, next) => { | ||
// The "id" parameter will NOT have been validated here! | ||
}); | ||
|
||
// CORRECT | ||
app.get('/orders/:id', validator.params(schema), (req, res, next) => { | ||
// This WILL have a validated "id" | ||
}) | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
'use strict'; | ||
|
||
|
||
const route = module.exports = require('express').Router(); | ||
const users = require('./users'); | ||
const Joi = require('joi'); | ||
const _ = require('lodash'); | ||
const validator = require('../index.js')({}); | ||
|
||
|
||
|
||
/** | ||
* This "GET /:id" endpoint is used to query users by their ID | ||
* Try accessing http://localhost:8080/users/1001 to see it in action. | ||
* Now try http://localhost:8080/users/bananas - it will fail since the ID must be an integer | ||
*/ | ||
const paramsSchema = Joi.object({ | ||
id: Joi.number().required() | ||
}); | ||
|
||
route.get('/:id', validator.params(paramsSchema), (req, res) => { | ||
console.log(`\nGetting user by ID ${req.params.id}.`); | ||
console.log(`req.params was ${JSON.stringify(req.originalParams)} before validation`); | ||
console.log(`req.params is ${JSON.stringify(req.params)} after validation`); | ||
console.log('note that the ID was correctly cast to an integer'); | ||
|
||
const u = _.find(users, {id: req.params.id}); | ||
|
||
if (u) { | ||
res.json(u); | ||
} else { | ||
res.status(404).json({ | ||
message: `no user exists with id "${req.params.id}"` | ||
}); | ||
} | ||
}); | ||
|
||
|
||
|
||
/** | ||
* This "GET /" endpoint is used to query users by a querystring | ||
* Try accessing http://localhost:8080/users?name=j&age=25 to get users that are 25 or with a name containing a "j". | ||
* Now try http://localhost:8080/users - it will fail since name is required | ||
*/ | ||
const querySchema = Joi.object({ | ||
name: Joi.string().required().min(1).max(10), | ||
age: Joi.number().integer().min(1).max(120) | ||
}); | ||
|
||
route.get('/', validator.query(querySchema), (req, res) => { | ||
console.log(`\nGetting users for query ${JSON.stringify(req.query)}.`); | ||
console.log(`req.query was ${JSON.stringify(req.originalQuery)} before validation`); | ||
console.log(`req.query is ${JSON.stringify(req.query)} after validation`); | ||
console.log('note that the age was correctly cast to an integer if provided\n'); | ||
|
||
res.json( | ||
_.filter(users, (u) => { | ||
return _.includes(u.name, req.query.name) || req.query.age && u.age === req.query.age; | ||
}) | ||
); | ||
}); | ||
|
||
|
||
/** | ||
* This "POST /" endpoint is used to create new users | ||
* POST to http://localhost:8080/users with '{"name": "jane", "age": "26"}' to see it work | ||
* Now try posting '{"name": "jane", "age": 1000}' - it will fail since the age is above 120 | ||
*/ | ||
const bodySchema = Joi.object({ | ||
name: Joi.string().required().min(1).max(10), | ||
age: Joi.number().integer().required().min(1).max(120) | ||
}); | ||
|
||
route.post('/', require('body-parser').json(), validator.body(bodySchema), (req, res) => { | ||
console.log(`\Creating user with data ${JSON.stringify(req.body)}.`); | ||
console.log(`req.body was ${JSON.stringify(req.originalBody)} before validation`); | ||
console.log(`req.body is ${JSON.stringify(req.body)} after validation`); | ||
console.log('note that the age was correctly cast to an integer if it was a string\n'); | ||
|
||
// Generate data required for insert (new id is incremented from previous max) | ||
const prevMaxId = _.maxBy(users, (u) => u.id).id; | ||
const data = Object.assign({}, req.body, {id: prevMaxId + 1}); | ||
|
||
users.push(data); | ||
|
||
res.json({message: 'created user', data: data}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
'use strict'; | ||
|
||
const port = 8080; | ||
|
||
const app = require('express')(); | ||
const Joi = require('joi'); | ||
const validator = require('../index.js')({}); | ||
|
||
const headerSchema = Joi.object({ | ||
'host': Joi.string().required(), | ||
'user-agent': Joi.string().required() | ||
}); | ||
|
||
|
||
app.use(validator.headers(headerSchema)); | ||
|
||
app.use('/users', require('./router')); | ||
|
||
app.listen(port, (err) => { | ||
if (err) { | ||
throw err; | ||
} | ||
|
||
console.log(`\napp started on ${port}\n`); | ||
console.log(`Try accessing http://localhost:${port}/users/1001 or http://localhost:${port}/users?name=dean to get some data.\n`); | ||
console.log(`Now try access http://localhost:${port}/users?age=50. You should get an error complaining that your querystring is invalid.`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
'use strict'; | ||
|
||
module.exports = [ | ||
{id: 1000, name: 'anne, a.', age: 25}, | ||
{id: 1001, name: 'barry, a.', age: 52}, | ||
{id: 1002, name: 'clare, a.', age: 25}, | ||
{id: 1003, name: 'joe, a.', age: 67}, | ||
{id: 1004, name: 'anne, b.', age: 47}, | ||
{id: 1005, name: 'barry, b.', age: 80}, | ||
{id: 1006, name: 'clare, b.', age: 28}, | ||
{id: 1007, name: 'joe, b.', age: 15} | ||
]; |
Oops, something went wrong.