Skip to content

Commit

Permalink
initial commit. validate ALL the things
Browse files Browse the repository at this point in the history
  • Loading branch information
evanshortiss committed Apr 13, 2017
0 parents commit 91c40c9
Show file tree
Hide file tree
Showing 11 changed files with 721 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
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
43 changes: 43 additions & 0 deletions .eslintrc
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
}
}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
node_modules
*.log
coverage
.nyc_output
10 changes: 10 additions & 0 deletions .travis.yml
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
188 changes: 188 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# express-joi-validation

![TravisCI](https://travis-ci.org/evanshortiss/express-joi-validation.svg) [![npm version](https://badge.fury.io/js/express-joi-validation.svg)](https://badge.fury.io/js/express-joi-validation) [![Coverage Status](https://coveralls.io/repos/github/evanshortiss/express-joi-validation/badge.svg?branch=master)](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"
})

```
87 changes: 87 additions & 0 deletions example/router.js
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});
});
27 changes: 27 additions & 0 deletions example/server.js
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.`);
});
12 changes: 12 additions & 0 deletions example/users.js
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}
];
Loading

0 comments on commit 91c40c9

Please sign in to comment.