Comparing version 10.1.0 to 11.0.0-rc1
@@ -9,3 +9,12 @@ import { ErrorRequestHandler, RequestHandler } from 'express'; | ||
declare namespace Celebrate { | ||
declare enum Segments { | ||
PARAMS="params", | ||
HEADERS="headers", | ||
QUERY="query", | ||
COOKIES="cookies", | ||
SIGNEDCOOKIES="signedCookies", | ||
BODY="body" | ||
} | ||
/** | ||
@@ -62,11 +71,6 @@ * Creates a Celebrate middleware function. | ||
/** | ||
* Format a joi error into a standard object | ||
* The standard error used by Celebrate | ||
*/ | ||
function format(err: ValidationResult<object>, | ||
source: "params" | "headers" | "query" | "cookies" | "signedCookies" | "body", | ||
opts?: { celebrated: boolean }): { | ||
meta: { | ||
source: "params" | "headers" | "query" | "cookies" | "signedCookies" | "body" | ||
}, | ||
joi: ValidationError, | ||
declare class CelebrateError { | ||
constructor(error: ValidationError, segment: Segments, opts?: { celebrated: boolean }) {} | ||
} | ||
@@ -73,0 +77,0 @@ } |
208
lib/index.js
const Assert = require('assert'); | ||
const Joi = require('@hapi/joi'); | ||
const EscapeHtml = require('escape-html'); | ||
const get = require('lodash.get'); | ||
const { | ||
middlewareSchema, celebrateSchema, sourceSchema, optSchema, | ||
middlewareSchema, | ||
celebrateSchema, | ||
sourceSchema, | ||
optSchema, | ||
} = require('./schema'); | ||
const { segments } = require('./constants'); | ||
const CELEBRATED = Symbol('celebrated'); | ||
const DEFAULT_JOI_OPTIONS = { | ||
escapeHtml: true, | ||
const internals = { | ||
CELEBRATED: Symbol('celebrated'), | ||
DEFAULT_FORMAT_OPTIONS: { | ||
celebrated: true, | ||
}, | ||
}; | ||
const DEFAULT_FORMAT_OPTIONS = { | ||
celebrated: true, | ||
}; | ||
const _format = (error, source, opts) => ({ | ||
[CELEBRATED]: opts.celebrated, | ||
joi: error, | ||
meta: { source }, | ||
}); | ||
const format = (err, source, opts = { celebrated: false }) => { | ||
Assert.ok(get(err, 'error.isJoi', false)); | ||
let result = Joi.validate(source, sourceSchema); | ||
Assert.ifError(result.error); | ||
result = Joi.validate(opts, optSchema); | ||
Assert.ifError(result.error); | ||
const { | ||
error, | ||
} = err; | ||
return _format(error, source, opts); | ||
internals.CelebrateError = class extends Error { | ||
constructor(joiError, segment, opts) { | ||
super(joiError.message); | ||
this.joi = joiError; | ||
this.meta = { source: segment }; | ||
this[internals.CELEBRATED] = opts.celebrated; | ||
} | ||
}; | ||
const validateSource = (source) => ({ | ||
celebrateOpts, | ||
joiOpts, | ||
internals.validateSource = (segment, spec) => ({ | ||
config, | ||
req, | ||
rules, | ||
}) => { | ||
const spec = rules.get(source); | ||
if (!spec) { return null; } | ||
const result = Joi.validate(req[source], spec, celebrateOpts.reqContext ? { | ||
...joiOpts, | ||
context: req, | ||
} : joiOpts); | ||
const { | ||
value, | ||
error, | ||
} = result; | ||
if (value !== undefined) { | ||
Object.defineProperty(req, source, { | ||
}) => new Promise((resolve, reject) => { | ||
spec.validateAsync(req[segment], config).then(({ value }) => { | ||
resolve({ | ||
value, | ||
segment, | ||
}); | ||
} | ||
if (error) { | ||
return _format(error, source, DEFAULT_FORMAT_OPTIONS); | ||
} | ||
return null; | ||
}; | ||
}).catch((e) => reject(new internals.CelebrateError( | ||
e, | ||
segment, | ||
internals.DEFAULT_FORMAT_OPTIONS, | ||
))); | ||
}); | ||
const validateHeaders = validateSource('headers'); | ||
const validateParams = validateSource('params'); | ||
const validateQuery = validateSource('query'); | ||
const validateCookies = validateSource('cookies'); | ||
const validateSignedCookies = validateSource('signedCookies'); | ||
const validateBody = validateSource('body'); | ||
const maybeValidateBody = (config, callback) => { | ||
const method = config.req.method.toLowerCase(); | ||
internals.maybeValidateBody = (segment, spec) => (opts) => { | ||
const method = opts.req.method.toLowerCase(); | ||
if (method === 'get' || method === 'head') { | ||
return null; | ||
return Promise.resolve({ | ||
value: null, | ||
segment, | ||
}); | ||
} | ||
return validateBody(config, callback); | ||
return internals.validateSource(segment, spec)(opts); | ||
}; | ||
const REQ_VALIDATIONS = [ | ||
validateHeaders, | ||
validateParams, | ||
validateQuery, | ||
validateCookies, | ||
validateSignedCookies, | ||
maybeValidateBody, | ||
]; | ||
// Map of segments to validation functions. | ||
// The key order is the order validations will run in. | ||
internals.REQ_VALIDATIONS = new Map([ | ||
[segments.HEADERS, internals.validateSource], | ||
[segments.PARAMS, internals.validateSource], | ||
[segments.QUERY, internals.validateSource], | ||
[segments.COOKIES, internals.validateSource], | ||
[segments.SIGNEDCOOKIES, internals.validateSource], | ||
[segments.BODY, internals.maybeValidateBody], | ||
]); | ||
const isCelebrate = (err) => { | ||
if (err != null && typeof err === 'object') { | ||
return err[CELEBRATED] || false; | ||
// ⚠️ steps is mutated in this function | ||
internals.check = (steps, opts) => { | ||
const validateFn = steps.shift(); | ||
if (validateFn) { | ||
return validateFn(opts).then(({ value, segment }) => { | ||
if (value != null) { | ||
Object.defineProperty(opts.req, segment, { | ||
value, | ||
}); | ||
} | ||
return internals.check(steps, opts); | ||
}); | ||
} | ||
return false; | ||
// If we get here, all the promises have resolved | ||
// and so we have a final resolve to end the recursion | ||
return Promise.resolve(null); | ||
}; | ||
const celebrate = (schema, joiOptions = {}, celebrateOptions = {}) => { | ||
let result = Joi.validate(schema, middlewareSchema); | ||
exports.celebrate = (schema, joiOpts = {}, celebrateOptions = {}) => { | ||
let result = middlewareSchema.validate(schema); | ||
Assert.ifError(result.error); | ||
result = Joi.validate(celebrateOptions, celebrateSchema); | ||
result = celebrateSchema.validate(celebrateOptions); | ||
Assert.ifError(result.error); | ||
const rules = new Map(); | ||
const joiOpts = { ...DEFAULT_JOI_OPTIONS, ...joiOptions }; | ||
Object.entries(schema).forEach(([key, value]) => rules.set(key, Joi.compile(value))); | ||
// We want a fresh copy of steps since `internals.check` mutates the array | ||
const steps = []; | ||
internals.REQ_VALIDATIONS.forEach((validateFn, segment) => { | ||
const spec = schema[segment]; | ||
if (spec) { | ||
steps.push(validateFn(segment, Joi.compile(spec))); | ||
} | ||
}); | ||
const middleware = (req, res, next) => { | ||
let stepNumber = 0; | ||
let err = null; | ||
const config = { | ||
const finalConfig = celebrateOptions.reqContext ? { | ||
...joiOpts, | ||
context: req, | ||
warnings: true, | ||
} : { | ||
...joiOpts, | ||
warnings: true, | ||
}; | ||
return internals.check(steps, { | ||
config: finalConfig, | ||
req, | ||
joiOpts, | ||
rules, | ||
celebrateOpts: celebrateOptions, | ||
}; | ||
do { | ||
const step = REQ_VALIDATIONS[stepNumber]; | ||
err = step(config); | ||
stepNumber += 1; | ||
} while (stepNumber <= REQ_VALIDATIONS.length - 1 && err === null); | ||
next(err); | ||
}) | ||
.then(next) | ||
.catch(next); | ||
}; | ||
@@ -131,5 +124,12 @@ | ||
const errors = () => (err, req, res, next) => { | ||
exports.isCelebrate = (err) => { | ||
if (err != null && typeof err === 'object') { | ||
return err[internals.CELEBRATED] || false; | ||
} | ||
return false; | ||
}; | ||
exports.errors = () => (err, req, res, next) => { | ||
// If this isn't a Celebrate error, send it to the next error handler | ||
if (!isCelebrate(err)) { | ||
if (!exports.isCelebrate(err)) { | ||
return next(err); | ||
@@ -162,8 +162,14 @@ } | ||
module.exports = { | ||
celebrate, | ||
errors, | ||
Joi, | ||
isCelebrate, | ||
format, | ||
exports.CelebrateError = (error, segment, opts = { celebrated: false }) => { | ||
Assert.ok(error.isJoi); | ||
let result = sourceSchema.validate(segment); | ||
Assert.ifError(result.error); | ||
result = optSchema.validate(opts); | ||
Assert.ifError(result.error); | ||
return new internals.CelebrateError(error, segment, opts); | ||
}; | ||
exports.Joi = Joi; | ||
exports.Segments = segments; |
const Joi = require('@hapi/joi'); | ||
const { segments } = require('./constants'); | ||
exports.middlewareSchema = Joi.object({ | ||
headers: Joi.any(), | ||
params: Joi.any(), | ||
query: Joi.any(), | ||
cookies: Joi.any(), | ||
signedCookies: Joi.any(), | ||
body: Joi.any(), | ||
[segments.HEADERS]: Joi.any(), | ||
[segments.PARAMS]: Joi.any(), | ||
[segments.QUERY]: Joi.any(), | ||
[segments.COOKIES]: Joi.any(), | ||
[segments.SIGNEDCOOKIES]: Joi.any(), | ||
[segments.BODY]: Joi.any(), | ||
}).required().min(1); | ||
@@ -16,3 +17,10 @@ | ||
exports.sourceSchema = Joi.string().valid(['headers', 'params', 'query', 'cookies', 'signedCookies', 'body']); | ||
exports.sourceSchema = Joi.string().valid( | ||
segments.HEADERS, | ||
segments.PARAMS, | ||
segments.QUERY, | ||
segments.COOKIES, | ||
segments.SIGNEDCOOKIES, | ||
segments.BODY, | ||
); | ||
@@ -19,0 +27,0 @@ exports.optSchema = Joi.object({ |
{ | ||
"name": "celebrate", | ||
"version": "10.1.0", | ||
"version": "11.0.0-rc1", | ||
"description": "A joi validation middleware for Express.", | ||
@@ -32,5 +32,4 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@hapi/joi": "15.x.x", | ||
"escape-html": "1.0.3", | ||
"lodash.get": "4.4.x" | ||
"@hapi/joi": "16.x.x", | ||
"escape-html": "1.0.3" | ||
}, | ||
@@ -40,3 +39,3 @@ "devDependencies": { | ||
"@types/express": "4.x.x", | ||
"@types/hapi__joi": "15.x.x", | ||
"@types/hapi__joi": "16.x.x", | ||
"artificial": "1.x.x", | ||
@@ -43,0 +42,0 @@ "benchmark": "2.1.x", |
@@ -49,4 +49,5 @@ [![celebrate](https://github.com/arb/celebrate/raw/master/images/logo.svg?sanitize=1)](https://www.npmjs.org/package/celebrate) | ||
- [`Joi`](#joi) | ||
- [`Segments`](#segments) | ||
- [`CelebrateError(err, segment, [opts])`](#celebrateerrorerr-segment-opts) | ||
- [`isCelebrate(err)`](#iscelebrateerr) | ||
- [`format(err, source, [opts])`](#formaterr-source-opts) | ||
- [Validation Order](#validation-order) | ||
@@ -73,3 +74,3 @@ - [Issues](#issues) | ||
const BodyParser = require('body-parser'); | ||
const { celebrate, Joi, errors } = require('celebrate'); | ||
const { celebrate, Joi, errors, Segments } = require('celebrate'); | ||
@@ -80,3 +81,3 @@ const app = express(); | ||
app.post('/signup', celebrate({ | ||
body: Joi.object().keys({ | ||
[Segments.BODY]: Joi.object().keys({ | ||
name: Joi.string().required(), | ||
@@ -86,3 +87,3 @@ age: Joi.number().integer(), | ||
}), | ||
query: { | ||
[Segments.QUERY]: { | ||
token: Joi.string().token().required() | ||
@@ -100,3 +101,3 @@ } | ||
const express = require('express'); | ||
const { celebrate, Joi, errors } = require('celebrate'); | ||
const { celebrate, Joi, errors, Segments } = require('celebrate'); | ||
const app = express(); | ||
@@ -107,3 +108,3 @@ | ||
app.use(celebrate({ | ||
headers: Joi.object({ | ||
[Segments.HEADERS]: Joi.object({ | ||
token: Joi.string().required().regex(/abc\d{3}/) | ||
@@ -125,4 +126,4 @@ }).unknown() | ||
- `schema` - an `object` where `key` can be one of `'params'`, `'headers'`, `'query'`, `'cookies'`, `'signedCookies'` and `'body'` and the `value` is a [joi](https://github.com/hapijs/joi/blob/master/API.md) validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the `req` object will not be validated. A schema must contain at least one valid key. | ||
- `[joiOptions]` - optional `object` containing joi [options](https://github.com/hapijs/joi/blob/master/API.md#validatevalue-schema-options-callback) that are passed directly into the `validate` function. Defaults to `{ escapeHtml: true }`. | ||
- `schema` - an `object` where `key` can be one of the values from [`Segments`](#segments) and the `value` is a [joi](https://github.com/hapijs/joi/blob/master/API.md) validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the `req` object will not be validated. A schema must contain at least one valid key. | ||
- `[joiOptions]` - optional `object` containing joi [options](https://github.com/hapijs/joi/blob/master/API.md#anyvalidatevalue-options) that are passed directly into the `validate` function. Defaults to `{ warnings: true }`. | ||
- `[celebrateOptions]` - an optional `object` with the following keys. Defaults to `{}`. | ||
@@ -137,6 +138,3 @@ - `reqContext` - `bool` value that instructs joi to use the incoming `req` object as the `context` value during joi validation. If set, this will trump the value of `joiOptions.context`. This is useful if you want to validate part of the request object against another part of the request object. See the tests for more details. | ||
Errors origintating from `celebrate()` are objects with the following keys: | ||
- `joi` - The full [joi error object](https://github.com/hapijs/joi/blob/master/API.md#errors). | ||
- `meta` - On `object` with the following keys: | ||
- `source` - A `string` indicating the step where the validation failed. Will be one of `'params'`, `'headers'`, `'query'`, `'cookies'`, `'signedCookies'`, or `'body'` | ||
Errors origintating from `celebrate()` are [`CelebrateError`](#celebrateerrorerr-segment-opts)) objects. | ||
@@ -147,16 +145,24 @@ ### `Joi` | ||
### `isCelebrate(err)` | ||
### `Segments` | ||
Returns `true` if the provided `err` object originated from the `celebrate` middleware, and `false` otherwise. Useful if you want to write your own error handler for celebrate errors. | ||
An enum containing all the segments of `req` objects that celebrate *can* valiate against. | ||
- `err` - an error object | ||
```js | ||
{ | ||
BODY: 'body', | ||
COOKIES: 'cookies', | ||
HEADERS: 'headers', | ||
PARAMS: 'params', | ||
QUERY: 'query', | ||
SIGNEDCOOKIES: 'signedCookies', | ||
} | ||
``` | ||
### `format(err, source, [opts])` | ||
### `CelebrateError(err, segment, [opts])` | ||
A factory function for creating celebrate errors. | ||
Formats the incomming values into the shape of celebrate [errors](#errors()) | ||
- `err` - a Joi validation error object | ||
- `source` - A `string` indicating the step where the validation failed. Will be one of `'params'`, `'headers'`, `'query'`, `'cookies'`, `'signedCookies'`, or `'body'` | ||
- `segment` - A [`Segment`](#segments) indicating the step where the validation failed. | ||
- `[opts]` - optional `object` with the following keys | ||
- `celebrated` - `bool` that, when `true`, adds `Symbol('celebrated'): true` to the result object. This indicates this error as originating from `celebrate`. You'd likely want to set this to `true` if you want the celebrate error handler to handle errors originating from the `format` function that you call in user-land code. Defaults to `false`. | ||
- `celebrated` - `bool` that, when `true`, adds `Symbol('celebrated'): true` to the result object. This indicates this error as originating from `celebrate`. You'd likely want to set this to `true` if you want the celebrate error handler to handle errors originating from the `format` function that you call in user-land code. Defaults to `false`. | ||
<details> | ||
@@ -167,6 +173,12 @@ <summary>Sample usage</summary> | ||
const result = Joi.validate(req.params.id, Joi.string().valid('foo'), { abortEarly: false }); | ||
const err = format(result, 'params'); | ||
const err = CelebrateError(result.error, Segments.PARAMS); | ||
``` | ||
</details> | ||
### `isCelebrate(err)` | ||
Returns `true` if the provided `err` object originated from the `celebrate` middleware, and `false` otherwise. Useful if you want to write your own error handler for celebrate errors. | ||
- `err` - an error object | ||
## Validation Order | ||
@@ -173,0 +185,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
19332
2
7
253
190
1
1
+ Added@hapi/formula@1.2.0(transitive)
+ Added@hapi/joi@16.1.8(transitive)
+ Added@hapi/pinpoint@1.0.2(transitive)
- Removedlodash.get@4.4.x
- Removed@hapi/bourne@1.3.2(transitive)
- Removed@hapi/joi@15.1.1(transitive)
- Removedlodash.get@4.4.2(transitive)
Updated@hapi/joi@16.x.x