Skip to content

Commit

Permalink
fix(rest): honor options for AJV validator caching
Browse files Browse the repository at this point in the history
Fixes #3234
  • Loading branch information
raymondfeng committed Jun 27, 2019
1 parent e64698e commit 1fd52a3
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 13 deletions.
38 changes: 38 additions & 0 deletions packages/rest/src/__tests__/unit/request-body.validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand Down
13 changes: 9 additions & 4 deletions packages/rest/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ajv.ValidateFunction> // Second level keyed by stringified AJV options
>;

/**
* Options for request body validation using AJV
*/
Expand All @@ -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 */
Expand Down
39 changes: 30 additions & 9 deletions packages/rest/src/validation/request-body.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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<string, unknown> = {};
// 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.
Expand All @@ -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<string, AJV.ValidateFunction> | 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)) {
Expand Down

0 comments on commit 1fd52a3

Please sign in to comment.