diff --git a/packages/rest/src/coercion/coerce-parameter.ts b/packages/rest/src/coercion/coerce-parameter.ts index 151ddb3aea83..450091a25a82 100644 --- a/packages/rest/src/coercion/coerce-parameter.ts +++ b/packages/rest/src/coercion/coerce-parameter.ts @@ -14,6 +14,7 @@ import { RequestBodyValidationOptions, RestHttpErrors, validateValueAgainstSchema, + ValueValidationOptions, } from '../'; import {parseJson} from '../parse-json'; import { @@ -40,7 +41,7 @@ const debug = debugModule('loopback:rest:coercion'); export async function coerceParameter( data: string | undefined | object, spec: ParameterObject, - options?: RequestBodyValidationOptions, + options?: ValueValidationOptions, ) { const schema = extractSchemaFromSpec(spec); @@ -184,7 +185,7 @@ async function coerceObject( data, schema, {}, - {...options, coerceTypes: true}, + {...options, coerceTypes: true, position: 'parameter'}, ); } diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 0a7f695dc7b5..d5438913f63b 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -108,6 +108,17 @@ export type AjvKeyword = KeywordDefinition & {name: string}; */ export type AjvFormat = FormatDefinition & {name: string}; +/** + * Options for any value validation using AJV + */ +export interface ValueValidationOptions extends RequestBodyValidationOptions { + /** + * Where the data comes from. It can be 'body', 'path', 'header', + * 'query', 'cookie', etc... + */ + position?: string; +} + /** * Options for request body validation using AJV */ diff --git a/packages/rest/src/validation/request-body.validator.ts b/packages/rest/src/validation/request-body.validator.ts index e6a172a2cbde..8a32d5395c98 100644 --- a/packages/rest/src/validation/request-body.validator.ts +++ b/packages/rest/src/validation/request-body.validator.ts @@ -15,7 +15,11 @@ import debugModule from 'debug'; import _ from 'lodash'; import util from 'util'; import {HttpErrors, RequestBody, RestHttpErrors} from '..'; -import {RequestBodyValidationOptions, SchemaValidatorCache} from '../types'; +import { + RequestBodyValidationOptions, + SchemaValidatorCache, + ValueValidationOptions, +} from '../types'; import {AjvFactoryProvider} from './ajv-factory.provider'; const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema'); @@ -66,7 +70,10 @@ export async function validateRequestBody( if (!schema) return; options = {coerceTypes: !!body.coercionRequired, ...options}; - await validateValueAgainstSchema(body.value, schema, globalSchemas, options); + await validateValueAgainstSchema(body.value, schema, globalSchemas, { + ...options, + position: 'body', + }); } /** @@ -117,10 +124,10 @@ function getKeyForOptions(options: RequestBodyValidationOptions) { */ export async function validateValueAgainstSchema( // eslint-disable-next-line @typescript-eslint/no-explicit-any - body: any, + value: any, schema: SchemaObject | ReferenceObject, globalSchemas: SchemasObject = {}, - options: RequestBodyValidationOptions = {}, + options: ValueValidationOptions = {}, ) { let validate: ajv.ValidateFunction | undefined; @@ -145,10 +152,10 @@ export async function validateValueAgainstSchema( let validationErrors: ajv.ErrorObject[] = []; try { - const validationResult = await validate(body); - // When body is optional & values is empty / null, ajv returns null + const validationResult = await validate(value); + // When value is optional & values is empty / null, ajv returns null if (validationResult || validationResult === null) { - debug('Request body passed AJV validation.'); + debug('Value passed AJV validation.'); return; } } catch (error) { @@ -158,8 +165,8 @@ export async function validateValueAgainstSchema( /* istanbul ignore if */ if (debug.enabled) { debug( - 'Invalid request body: %s. Errors: %s', - util.inspect(body, {depth: null}), + 'Invalid value: %s. Errors: %s', + util.inspect(value, {depth: null}), util.inspect(validationErrors), ); } @@ -168,7 +175,24 @@ export async function validateValueAgainstSchema( validationErrors = options.ajvErrorTransformer(validationErrors); } - const error = RestHttpErrors.invalidRequestBody(); + // Throw invalid request body error + if (options.position === 'body') { + const error = RestHttpErrors.invalidRequestBody(); + addErrorDetails(error, validationErrors); + throw error; + } + + // Throw invalid value error + const error = new HttpErrors.BadRequest('Invalid value.'); + addErrorDetails(error, validationErrors); + throw error; +} + +function addErrorDetails( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: any, + validationErrors: ajv.ErrorObject[], +) { error.details = _.map(validationErrors, e => { return { path: e.dataPath, @@ -177,7 +201,6 @@ export async function validateValueAgainstSchema( info: e.params, }; }); - throw error; } /**