diff --git a/packages/rest/src/__tests__/unit/request-body.validator.test.ts b/packages/rest/src/__tests__/unit/request-body.validator.test.ts index 079d21c117c7..cc0c2f7e8d5e 100644 --- a/packages/rest/src/__tests__/unit/request-body.validator.test.ts +++ b/packages/rest/src/__tests__/unit/request-body.validator.test.ts @@ -58,6 +58,44 @@ describe('validateRequestBody', () => { ); }); + // Test for https://github.com/strongloop/loopback-next/issues/3234 + it('honors options for AJV validator caching', () => { + // 1. Trigger a validation with `{coerceTypes: false}` + validateRequestBody( + { + value: {city: 'San Jose', unit: 123, isOwner: true}, + schema: ADDRESS_SCHEMA, + }, + aBodySpec(ADDRESS_SCHEMA), + {}, + {coerceTypes: false}, + ); + + // 2. Trigger a validation with `{coerceTypes: true}` + validateRequestBody( + { + value: {city: 'San Jose', unit: '123', isOwner: 'true'}, + schema: ADDRESS_SCHEMA, + }, + aBodySpec(ADDRESS_SCHEMA), + {}, + {coerceTypes: true}, + ); + + // 3. Trigger a validation with `{coerceTypes: false}` with invalid data + expect(() => + validateRequestBody( + { + value: {city: 'San Jose', unit: '123', isOwner: true}, + schema: ADDRESS_SCHEMA, + }, + aBodySpec(ADDRESS_SCHEMA), + {}, + {coerceTypes: false}, + ), + ).to.throw(/The request body is invalid/); + }); + it('rejects data missing a required property', () => { const details: RestHttpErrors.ValidationErrorDetails[] = [ { diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 7b1db3f5299d..fd394d288ade 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -84,6 +84,14 @@ export type LogError = ( request: Request, ) => void; +/** + * Cache for AJV schema validators + */ +export type SchemaValidatorCache = WeakMap< + SchemaObject | ReferenceObject, // First keyed by schema object + Map // Second level keyed by stringified AJV options +>; + /** * Options for request body validation using AJV */ @@ -92,10 +100,7 @@ export interface RequestBodyValidationOptions extends ajv.Options { * Custom cache for compiled schemas by AJV. This setting makes it possible * to skip the default cache. */ - compiledSchemaCache?: WeakMap< - SchemaObject | ReferenceObject, - ajv.ValidateFunction - >; + compiledSchemaCache?: SchemaValidatorCache; } /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/packages/rest/src/validation/request-body.validator.ts b/packages/rest/src/validation/request-body.validator.ts index 21941ad7c0a6..63f3841b3c60 100644 --- a/packages/rest/src/validation/request-body.validator.ts +++ b/packages/rest/src/validation/request-body.validator.ts @@ -14,7 +14,7 @@ import * as debugModule from 'debug'; import * as _ from 'lodash'; import * as util from 'util'; import {HttpErrors, RequestBody, RestHttpErrors} from '..'; -import {RequestBodyValidationOptions} from '../types'; +import {RequestBodyValidationOptions, SchemaValidatorCache} from '../types'; const toJsonSchema = require('openapi-schema-to-json-schema'); const debug = debugModule('loopback:rest:validation'); @@ -79,10 +79,24 @@ function convertToJsonSchema(openapiSchema: SchemaObject) { /** * Built-in cache for complied schemas by AJV */ -const DEFAULT_COMPILED_SCHEMA_CACHE = new WeakMap< - SchemaObject | ReferenceObject, - AJV.ValidateFunction ->(); +const DEFAULT_COMPILED_SCHEMA_CACHE: SchemaValidatorCache = new WeakMap(); + +/** + * Build a cache key for AJV options + * @param options - Request body validation options + */ +function getKeyForOptions(options: RequestBodyValidationOptions) { + const ajvOptions: Record = {}; + // Sort keys for options + const keys = Object.keys( + options, + ).sort() as (keyof RequestBodyValidationOptions)[]; + for (const k of keys) { + if (k === 'compiledSchemaCache') continue; + ajvOptions[k] = options[k]; + } + return JSON.stringify(ajvOptions); +} /** * Validate the request body data against JSON schema. @@ -98,15 +112,22 @@ function validateValueAgainstSchema( globalSchemas: SchemasObject = {}, options: RequestBodyValidationOptions = {}, ) { - let validate: AJV.ValidateFunction; + let validate: AJV.ValidateFunction | undefined; const cache = options.compiledSchemaCache || DEFAULT_COMPILED_SCHEMA_CACHE; + const key = getKeyForOptions(options); + let validatorMap: Map | undefined; if (cache.has(schema)) { - validate = DEFAULT_COMPILED_SCHEMA_CACHE.get(schema)!; - } else { + validatorMap = cache.get(schema)!; + validate = validatorMap.get(key); + } + + if (!validate) { validate = createValidator(schema, globalSchemas, options); - cache.set(schema, validate); + validatorMap = validatorMap || new Map(); + validatorMap.set(key, validate); + cache.set(schema, validatorMap); } if (validate(body)) {