Skip to content

Commit

Permalink
feat(rest): improve validation errors for invalid parameter value
Browse files Browse the repository at this point in the history
Rework the validation code to use exiting `RestHttpErrors.invalidData`
error instead of a custom `BadRequest` error. This way the error object
includes the parameter name in the error message & properties and has
a machine-readable `code` property.

Signed-off-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
bajtos committed Aug 17, 2020
1 parent b787c6d commit 54f762c
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
Client,
createRestAppClient,
expect,
givenHttpServerConfig,
sinon,
} from '@loopback/testlab';
Expand Down Expand Up @@ -253,6 +254,34 @@ describe('Coercion', () => {
sinon.assert.calledWithExactly(spy, {...inclusionFilter});
});

it('returns AJV validation errors in error details', async () => {
const filter = {
where: 'string-instead-of-object',
};
const response = await client
.get(`/nested-inclusion-from-query`)
.query({filter: JSON.stringify(filter)})
.expect(400);

expect(response.body.error).to.containDeep({
code: 'INVALID_PARAMETER_VALUE',
details: [
{
code: 'type',
info: {
type: 'object',
},
message: 'should be object',
path: '/where',
},
],
});

expect(response.body.error.message).to.match(
/Invalid data.* for parameter "filter"/,
);
});

async function givenAClient() {
app = new RestApplication({rest: givenHttpServerConfig()});
app.controller(MyController);
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/coercion/coerce-parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ async function coerceObject(
data,
schema,
{},
{...options, coerceTypes: true, source: 'parameter'},
{...options, coerceTypes: true, source: 'parameter', name: spec.name},
);
}

Expand Down
8 changes: 6 additions & 2 deletions packages/rest/src/rest-http-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export namespace RestHttpErrors {
name: string,
extraProperties?: Props,
): HttpErrors.HttpError & Props {
const msg = `Invalid data ${JSON.stringify(data)} for parameter ${name}!`;
const msg = `Invalid data ${JSON.stringify(data)} for parameter "${name}".`;
return Object.assign(
new HttpErrors.BadRequest(msg),
{
Expand Down Expand Up @@ -51,11 +51,15 @@ export namespace RestHttpErrors {

export const INVALID_REQUEST_BODY_MESSAGE =
'The request body is invalid. See error object `details` property for more info.';
export function invalidRequestBody(): HttpErrors.HttpError {

export function invalidRequestBody(
details: ValidationErrorDetails[],
): HttpErrors.HttpError & {details: ValidationErrorDetails[]} {
return Object.assign(
new HttpErrors.UnprocessableEntity(INVALID_REQUEST_BODY_MESSAGE),
{
code: 'VALIDATION_FAILED',
details,
},
);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/rest/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ export interface ValueValidationOptions extends ValidationOptions {
* 'query', 'cookie', etc...
*/
source?: string;

/**
* Parameter name, as provided in `ParameterObject#name` property.
*/
name?: string;
}

/**
Expand Down
35 changes: 18 additions & 17 deletions packages/rest/src/validation/request-body.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from '@loopback/openapi-v3';
import ajv, {Ajv} from 'ajv';
import debugModule from 'debug';
import _ from 'lodash';
import util from 'util';
import {HttpErrors, RequestBody, RestHttpErrors} from '..';
import {
Expand Down Expand Up @@ -180,30 +179,32 @@ export async function validateValueAgainstSchema(

// Throw invalid request body error
if (options.source === 'body') {
const error = RestHttpErrors.invalidRequestBody();
addErrorDetails(error, validationErrors);
const error = RestHttpErrors.invalidRequestBody(
buildErrorDetails(validationErrors),
);
throw error;
}

// Throw invalid value error
const error = new HttpErrors.BadRequest('Invalid value.');
addErrorDetails(error, validationErrors);
const error = RestHttpErrors.invalidData(value, options.name ?? '(unknown)', {
details: buildErrorDetails(validationErrors),
});
throw error;
}

function addErrorDetails(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
function buildErrorDetails(
validationErrors: ajv.ErrorObject[],
) {
error.details = _.map(validationErrors, e => {
return {
path: e.dataPath,
code: e.keyword,
message: e.message,
info: e.params,
};
});
): RestHttpErrors.ValidationErrorDetails[] {
return validationErrors.map(
(e: ajv.ErrorObject): RestHttpErrors.ValidationErrorDetails => {
return {
path: e.dataPath,
code: e.keyword,
message: e.message ?? `must pass validation rule ${e.keyword}`,
info: e.params,
};
},
);
}

/**
Expand Down

0 comments on commit 54f762c

Please sign in to comment.