From 1874cfa31030f8debc5fd2b38253a7fd127448c7 Mon Sep 17 00:00:00 2001 From: will Farrell Date: Mon, 30 Apr 2018 13:07:36 -0600 Subject: [PATCH] feat: add in i18n support for validator (#161) * 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 --- README.md | 4 +- docs/middlewares.md | 6 ++- package-lock.json | 7 ++- package.json | 3 +- src/middlewares/__tests__/validator.js | 69 ++++++++++++++++++++++++++ src/middlewares/validator.js | 43 ++++++++++++++-- 6 files changed, 123 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 36c4ab95e..f74597c7f 100644 --- a/README.md +++ b/README.md @@ -602,7 +602,9 @@ Middy factory function. Use it to wrap your existing handler to enable middlewar ## middlewareFunction ⇒ void \| Promise **Kind**: global typedef -**Returns**: void \| Promise - - 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**: void \| Promise - - 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 | | --- | --- | --- | diff --git a/docs/middlewares.md b/docs/middlewares.md index 2b9f88545..a910419a7 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -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. @@ -529,6 +529,8 @@ 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 @@ -536,7 +538,7 @@ response to the user. - `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 diff --git a/package-lock.json b/package-lock.json index d3a17e53f..55e3be41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "middy", - "version": "0.12.1", + "version": "0.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -110,6 +110,11 @@ "json-schema-traverse": "0.3.1" } }, + "ajv-i18n": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-3.1.0.tgz", + "integrity": "sha1-tImXjZediEf8QRxmqzXVwhG2MAw=" + }, "ajv-keywords": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.0.0.tgz", diff --git a/package.json b/package.json index 20100b5ca..7033133a8 100644 --- a/package.json +++ b/package.json @@ -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": [ @@ -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", diff --git a/src/middlewares/__tests__/validator.js b/src/middlewares/__tests__/validator.js index 9cf24e686..449023179 100644 --- a/src/middlewares/__tests__/validator.js +++ b/src/middlewares/__tests__/validator.js @@ -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'}]) }) }) diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js index f1d82df96..9ee7baf15 100644 --- a/src/middlewares/validator.js +++ b/src/middlewares/validator.js @@ -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) @@ -24,6 +55,10 @@ 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 } @@ -31,7 +66,7 @@ module.exports = ({inputSchema, outputSchema, ajvOptions}) => { return next() }, after (handler, next) { - if (!outputSchema) { + if (!outputSchema || (!handler.response && handler.error)) { return next() }