Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validator: break out compileSchema #912

Merged
merged 5 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions packages/validator/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'ava'
import middy from '../../core/index.js'
import validator from '../index.js'
import validator, { transpileSchema } from '../index.js'

const event = {}
const context = {
Expand Down Expand Up @@ -147,7 +147,7 @@ test('It should validate an event object', async (t) => {

handler.use(
validator({
eventSchema: schema
eventSchema: transpileSchema(schema)
})
)

Expand Down Expand Up @@ -226,7 +226,7 @@ test('It should validate an event object with formats', async (t) => {

handler.use(
validator({
eventSchema: schema
eventSchema: transpileSchema(schema)
})
)

Expand Down Expand Up @@ -281,7 +281,7 @@ test('It should handle invalid schema as a BadRequest', async (t) => {

handler.use(
validator({
eventSchema: schema
eventSchema: transpileSchema(schema)
})
)

Expand Down Expand Up @@ -328,7 +328,7 @@ test('It should handle invalid schema as a BadRequest in a different language',

handler.use(
validator({
eventSchema: schema
eventSchema: transpileSchema(schema)
})
)

Expand Down Expand Up @@ -384,13 +384,13 @@ test('It should handle invalid schema as a BadRequest in a different language (w

handler.use(
validator({
eventSchema: schema
eventSchema: transpileSchema(schema)
})
)

// invokes the handler, note that property foo is missing
const event = {
preferredLanguage: 'pt',
preferredLanguage: 'pt-BR',
body: JSON.stringify({ something: 'somethingelse' })
}

Expand Down Expand Up @@ -432,14 +432,14 @@ test('It should handle invalid schema as a BadRequest without i18n', async (t) =

handler.use(
validator({
eventSchema: schema,
eventSchema: transpileSchema(schema),
i18nEnabled: false
})
)

// invokes the handler, note that property foo is missing
const event = {
preferredLanguage: 'pt',
preferredLanguage: 'pt-BR',
body: JSON.stringify({ something: 'somethingelse' })
}

Expand All @@ -451,6 +451,7 @@ test('It should handle invalid schema as a BadRequest without i18n', async (t) =
{
instancePath: '',
keyword: 'required',
message: "must have required property 'foo'",
params: { missingProperty: 'foo' },
schemaPath: '#/required'
}
Expand All @@ -468,7 +469,7 @@ test('It should validate context object', async (t) => {
return expectedResponse
})

handler.use(validator({ contextSchema }))
handler.use(validator({ contextSchema: transpileSchema(contextSchema) }))

const response = await handler(event, context)

Expand All @@ -484,7 +485,7 @@ test('It should make requests with invalid context fails with an Internal Server
.before((request) => {
request.context.callbackWaitsForEmptyEventLoop = 'fail'
})
.use(validator({ contextSchema }))
.use(validator({ contextSchema: transpileSchema(contextSchema) }))

let response

Expand Down Expand Up @@ -520,7 +521,7 @@ test('It should validate response object', async (t) => {
}
}

handler.use(validator({ responseSchema: schema }))
handler.use(validator({ responseSchema: transpileSchema(schema) }))

const response = await handler(event, context)

Expand All @@ -545,7 +546,7 @@ test('It should make requests with invalid responses fail with an Internal Serve
}
}

handler.use(validator({ responseSchema: schema }))
handler.use(validator({ responseSchema: transpileSchema(schema) }))

let response

Expand All @@ -568,7 +569,7 @@ test('It should not allow bad email format', async (t) => {
return {}
})

handler.use(validator({ eventSchema: schema }))
handler.use(validator({ eventSchema: transpileSchema(schema) }))

const event = { email: 'abc@abc' }
try {
Expand All @@ -591,7 +592,7 @@ test('It should error when unsupported keywords used (input)', async (t) => {

const event = { foo: 'a' }
try {
handler.use(validator({ eventSchema: schema }))
handler.use(validator({ eventSchema: transpileSchema(schema) }))
await handler(event, context)
} catch (e) {
t.is(e.message, 'strict mode: unknown keyword: "somethingnew"')
Expand All @@ -610,7 +611,7 @@ test('It should error when unsupported keywords used (output)', async (t) => {

const event = { foo: 'a' }
try {
handler.use(validator({ responseSchema: schema }))
handler.use(validator({ responseSchema: transpileSchema(schema) }))
await handler(event.context)
} catch (e) {
t.is(e.message, 'strict mode: unknown keyword: "somethingnew"')
Expand Down
90 changes: 27 additions & 63 deletions packages/validator/index.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,25 @@
import { createError } from '@middy/util'
import _ajv from 'ajv/dist/2020.js'
import localize from 'ajv-i18n'
import formats from 'ajv-formats'
import formatsDraft2019 from 'ajv-formats-draft2019'
import typeofKeyword from 'ajv-keywords/dist/definitions/typeof.js'
import uriResolver from 'fast-uri'

const Ajv = _ajv.default // esm workaround for linting

let ajv
const ajvDefaults = {
strict: true,
coerceTypes: 'array', // important for query string params
allErrors: true,
useDefaults: 'empty',
messages: false, // allow i18n,
uriResolver,
keywords: [
// allow `typeof` for identifying functions in `context`
typeofKeyword()
]
}
import { compile, ftl } from 'ajv-cmd'
import localize from 'ajv-ftl-i18n'

const defaults = {
eventSchema: undefined,
contextSchema: undefined,
responseSchema: undefined,
ajvOptions: {},
ajvInstance: undefined,
i18nEnabled: true,
defaultLanguage: 'en',
i18nEnabled: true
languages: {}
}

const validatorMiddleware = (opts = {}) => {
let {
const {
eventSchema,
contextSchema,
responseSchema,
ajvOptions,
ajvInstance,
i18nEnabled,
defaultLanguage,
i18nEnabled
languages
} = { ...defaults, ...opts }
eventSchema = compileSchema(eventSchema, ajvOptions, ajvInstance)
contextSchema = compileSchema(contextSchema, ajvOptions, ajvInstance)
responseSchema = compileSchema(responseSchema, ajvOptions, ajvInstance)

const validatorMiddlewareBefore = async (request) => {
if (eventSchema) {
Expand All @@ -53,7 +28,11 @@ const validatorMiddleware = (opts = {}) => {
if (!validEvent) {
if (i18nEnabled) {
const language = chooseLanguage(request.event, defaultLanguage)
localize[language](eventSchema.errors)
if (languages[language]) {
languages[language](eventSchema.errors)
} else {
localize[language](eventSchema.errors)
}
}

// Bad Request
Expand All @@ -76,9 +55,9 @@ const validatorMiddleware = (opts = {}) => {
}

const validatorMiddlewareAfter = async (request) => {
const valid = await responseSchema(request.response)
const validResponse = await responseSchema(request.response)

if (!valid) {
if (!validResponse) {
// Internal Server Error
throw createError(500, 'Response object failed validation', {
cause: responseSchema.errors
Expand All @@ -92,43 +71,28 @@ const validatorMiddleware = (opts = {}) => {
}
}

const ajvDefaults = {
strict: true,
coerceTypes: 'array', // important for query string params
allErrors: true,
useDefaults: 'empty',
messages: true // needs to be true to allow errorMessage to work
}

// This is pulled out due to it's performance cost (50-100ms on cold start)
// Precompile your schema during a build step is recommended.
export const compileSchema = (schema, ajvOptions, ajvInstance = null) => {
// Check if already compiled
if (typeof schema === 'function' || !schema) return schema
export const transpileSchema = (schema, ajvOptions) => {
const options = { ...ajvDefaults, ...ajvOptions }
if (!ajv) {
ajv = ajvInstance ?? new Ajv(options)
formats(ajv)
formatsDraft2019(ajv)
} else if (!ajvInstance) {
// Update options when initializing the middleware multiple times
ajv.opts = { ...ajv.opts, ...options }
}
return ajv.compile(schema)
}

/* 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',
'zh-tw': 'zh-TW',
zh_TW: 'zh-TW',
zh_tw: 'zh-TW'
return compile(schema, options)
}

const normalizePreferredLanguage = (lang) =>
languageNormalizationMap[lang] ?? lang
export const transpileLocale = ftl

const availableLanguages = Object.keys(localize)
const chooseLanguage = ({ preferredLanguage }, defaultLanguage) => {
if (preferredLanguage) {
const lang = normalizePreferredLanguage(preferredLanguage)
if (availableLanguages.includes(lang)) {
return lang
if (availableLanguages.includes(preferredLanguage)) {
return preferredLanguage
}
}

Expand Down
Loading