Skip to content

Commit

Permalink
feat: add in i18n support for validator (#161)
Browse files Browse the repository at this point in the history
* feat: add in i18n support for validator

Also includes the ability to customize error format

* fix: add tests to cover i18n and errorFormat

- revert how error was returned to pass tests
- lint code

* fix: pr change requests

* fix: remove errorFormat

* chore: lint and update tests

* Proposed alternative implementation that assumes you are using this in combination with the content-negotiation middleware

* Updated documentation

* version bump
  • Loading branch information
willfarrell authored and lmammino committed Apr 30, 2018
1 parent c24ee61 commit 1874cfa
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 9 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,9 @@ Middy factory function. Use it to wrap your existing handler to enable middlewar
## middlewareFunction ⇒ <code>void</code> \| <code>Promise</code>
**Kind**: global typedef
**Returns**: <code>void</code> \| <code>Promise</code> - - A middleware can return a Promise instead of using the `next` function as a callback. In this case middy will wait for the promise to resolve (or reject) and it will automatically propagate the result to the next middleware.
**Returns**: <code>void</code> \| <code>Promise</code> - - A middleware can return a Promise instead of using the `next` function as a callback.
In this case middy will wait for the promise to resolve (or reject) and it will automatically
propagate the result to the next middleware.
| Param | Type | Description |
| --- | --- | --- |
Expand Down
6 changes: 4 additions & 2 deletions docs/middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ handler

Fetches parameters from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-paramstore.html).

Parameters to fetch can be defined by path and by name (not mutually exclusive). See AWS docs [here](https://aws.amazon.com/blogs/mt/organize-parameters-by-hierarchy-tags-or-amazon-cloudwatch-events-with-amazon-ec2-systems-manager-parameter-store/).
Parameters to fetch can be defined by path and by name (not mutually exclusive). See AWS docs [here](https://aws.amazon.com/blogs/mt/organize-parameters-by-hierarchy-tags-or-amazon-cloudwatch-events-with-amazon-ec2-systems-manager-parameter-store/).

By default parameters are assigned to the Node.js `process.env` object. They can instead be assigned to the function handler's `context` object by setting the `setToContext` flag to `true`. By default all parameters are added with uppercase names.

Expand Down Expand Up @@ -529,14 +529,16 @@ This middleware can be used in combination with
[`httpErrorHandler`](#httperrorhandler) to automatically return the right
response to the user.

It can also be used in combination with [`httpcontentnegotiation`](#httpContentNegotiation) to load localised translations for the error messages (based on the currently requested language). This feature uses internally [`ajv-i18n`](http://npm.im/ajv-i18n) module, so reference to this module for options and more advanced use cases. By default the language used will be English (`en`), but you can redefine the default language by passing it in the `ajvOptions` options with the key `defaultLanguage` and specifying as value one of the [supported locales](https://www.npmjs.com/package/ajv-i18n#supported-locales).

### Options

- `inputSchema` (object) (optional): The JSON schema object that will be used
to validate the input (`handler.event`) of the Lambda handler.
- `outputSchema` (object) (optional): The JSON schema object that will be used
to validate the output (`handler.response`) of the Lambda handler.
- `ajvOptions` (object) (optional): Options to pass to [ajv](https://epoberezkin.github.io/ajv/)
class constructor. Defaults are `{v5: true, $data: true, allErrors: true}`
class constructor. Defaults are `{v5: true, coerceTypes: 'array', $data: true, allErrors: true, useDefaults: true, defaultLanguage: 'en'}`

### Sample Usage

Expand Down
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "middy",
"version": "0.12.1",
"version": "0.13.0",
"description": "🛵 The stylish Node.js middleware engine for AWS Lambda",
"main": "./index.js",
"files": [
Expand Down Expand Up @@ -67,6 +67,7 @@
"@types/aws-lambda": "^8.10.1",
"@types/http-errors": "^1.6.1",
"ajv": "^6.0.0",
"ajv-i18n": "^3.1.0",
"ajv-keywords": "^3.0.0",
"content-type": "^1.0.4",
"http-errors": "^1.6.2",
Expand Down
69 changes: 69 additions & 0 deletions src/middlewares/__tests__/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,75 @@ describe('📦 Middleware Validator', () => {
}
handler(event, {}, (err, res) => {
expect(err.message).toEqual('Event object failed validation')
expect(err.details).toEqual([{'dataPath': '', 'keyword': 'required', 'message': 'should have required property foo', 'params': {'missingProperty': 'foo'}, 'schemaPath': '#/required'}])
})
})

test('It should handle invalid schema as a BadRequest in a different language', () => {
const handler = middy((event, context, cb) => {
cb(null, event.body) // propagates the body as a response
})

const schema = {
required: ['body', 'foo'],
properties: {
// this will pass validation
body: {
type: 'string'
},
// this won't as it won't be in the event
foo: {
type: 'string'
}
}
}

handler.use(validator({
inputSchema: schema
}))

// invokes the handler, note that property foo is missing
const event = {
preferredLanguage: 'fr',
body: JSON.stringify({something: 'somethingelse'})
}
handler(event, {}, (err, res) => {
expect(err.message).toEqual('Event object failed validation')
expect(err.details).toEqual([{'dataPath': '', 'keyword': 'required', 'message': 'requiert la propriété foo', 'params': {'missingProperty': 'foo'}, 'schemaPath': '#/required'}])
})
})

test('It should handle invalid schema as a BadRequest in a different language (with normalization)', () => {
const handler = middy((event, context, cb) => {
cb(null, event.body) // propagates the body as a response
})

const schema = {
required: ['body', 'foo'],
properties: {
// this will pass validation
body: {
type: 'string'
},
// this won't as it won't be in the event
foo: {
type: 'string'
}
}
}

handler.use(validator({
inputSchema: schema
}))

// invokes the handler, note that property foo is missing
const event = {
preferredLanguage: 'pt',
body: JSON.stringify({something: 'somethingelse'})
}
handler(event, {}, (err, res) => {
expect(err.message).toEqual('Event object failed validation')
expect(err.details).toEqual([{'dataPath': '', 'keyword': 'required', 'message': 'deve ter a propriedade requerida foo', 'params': {'missingProperty': 'foo'}, 'schemaPath': '#/required'}])
})
})

Expand Down
43 changes: 39 additions & 4 deletions src/middlewares/validator.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
const createError = require('http-errors')
const Ajv = require('ajv')
const ajvKeywords = require('ajv-keywords')
const {deepEqual} = require('assert')
const ajvLocalize = require('ajv-i18n')
const { deepEqual } = require('assert')

let ajv
let previousConstructorOptions
const defaults = {v5: true, $data: true, allErrors: true}
const defaults = {
v5: true,
coerceTypes: 'array', // important for query string params
allErrors: true,
useDefaults: true,
$data: true, // required for ajv-keywords
defaultLanguage: 'en'
}

const availableLanguages = Object.keys(ajvLocalize)

/* in ajv-i18n Portuguese is represented as pt-BR */
const languageNormalizationMap = {
'pt': 'pt-BR',
'pt-br': 'pt-BR',
'pt_BR': 'pt-BR',
'pt_br': 'pt-BR'
}

const normalizePreferredLanguage = (lang) => languageNormalizationMap[lang] || lang

module.exports = ({inputSchema, outputSchema, ajvOptions}) => {
const chooseLanguage = ({ preferredLanguage }, defaultLanguage) => {
if (preferredLanguage) {
const lang = normalizePreferredLanguage(preferredLanguage)
if (availableLanguages.includes(lang)) {
return lang
}
}

return defaultLanguage
}

module.exports = ({ inputSchema, outputSchema, ajvOptions }) => {
const options = Object.assign({}, defaults, ajvOptions)
lazyLoadAjv(options)

Expand All @@ -24,14 +55,18 @@ module.exports = ({inputSchema, outputSchema, ajvOptions}) => {

if (!valid) {
const error = new createError.BadRequest('Event object failed validation')
handler.event.headers = Object.assign({}, handler.event.headers)
const language = chooseLanguage(handler.event, options.defaultLanguage)
ajvLocalize[language](validateInput.errors)

error.details = validateInput.errors
throw error
}

return next()
},
after (handler, next) {
if (!outputSchema) {
if (!outputSchema || (!handler.response && handler.error)) {
return next()
}

Expand Down

0 comments on commit 1874cfa

Please sign in to comment.