1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00
unleash.unleash/src/lib/error/bad-data-error.ts
Thomas Heartman b3054c9277
Chore: remove "dataPath" from data OpenAPI data errors. (#5272)
The `dataPath` was present (but not in the type) in previous versions of
the
error library that we use. But with the recent major upgrade, it's
been removed and the `instancePath` property has finally come into use.

This PR removes all the handling for the previous property and
replaces it with `instancePath`. Because the `dataPath` used full
stops and the `instancePath` uses slashes, we need to change a little
bit of the handling too.
2023-11-07 09:26:14 +01:00

199 lines
6.2 KiB
TypeScript

import { ErrorObject } from 'ajv';
import { ValidationError } from 'joi';
import getProp from 'lodash.get';
import { ApiErrorSchema, UnleashError } from './unleash-error';
type ValidationErrorDescription = {
description?: string;
message: string;
path?: string;
};
class BadDataError extends UnleashError {
statusCode = 400;
details: ValidationErrorDescription[];
constructor(
message: string,
errors?: [ValidationErrorDescription, ...ValidationErrorDescription[]],
) {
const topLevelMessage = `Request validation failed: your request body or params contain invalid data${
errors
? '. Refer to the `details` list for more information.'
: `: ${message}`
}`;
super(topLevelMessage);
this.details = errors ?? [{ message: message, description: message }];
}
toJSON(): ApiErrorSchema {
return {
...super.toJSON(),
details: this.details,
};
}
}
export default BadDataError;
const constructPath = (pathToParent: string, propertyName: string) =>
[pathToParent, propertyName].filter(Boolean).join('/');
const missingRequiredPropertyMessage = (
pathToParentObject: string,
missingPropertyName: string,
) => {
const path = constructPath(pathToParentObject, missingPropertyName);
const description = `The \`${path}\` property is required. It was not present on the data you sent.`;
return {
path,
description,
message: description,
};
};
const additionalPropertiesMessage = (
pathToParentObject: string,
additionalPropertyName: string,
) => {
const path = constructPath(pathToParentObject, additionalPropertyName);
const description = `The ${
pathToParentObject ? `\`${pathToParentObject}\`` : 'root'
} object of the request body does not allow additional properties. Your request included the \`${path}\` property.`;
return {
path,
description,
message: description,
};
};
const genericErrorMessage = (
requestBody: object,
propertyName: string,
errorMessage: string = 'is invalid',
) => {
const input = getProp(requestBody, propertyName.split('/'));
const youSent = JSON.stringify(input);
const description = `The \`${propertyName}\` property ${errorMessage}. You sent ${youSent}.`;
return {
description,
message: description,
path: propertyName,
};
};
const oneOfMessage = (
propertyName: string,
errorMessage: string = 'is invalid',
) => {
const errorPosition =
propertyName === '' ? 'root object' : `"${propertyName}" property`;
const description = `The ${errorPosition} ${errorMessage}. The data you provided matches more than one option in the schema. These options are mutually exclusive. Please refer back to the schema and remove any excess properties.`;
return {
description,
message: description,
path: propertyName,
};
};
const enumMessage = (
propertyName: string,
message: string | undefined,
allowedValues: string[],
suppliedValue: string | null | undefined,
) => {
const description = `The \`${propertyName}\` property ${
message ?? 'must match one of the allowed values'
}: ${allowedValues
.map((value) => `"${value}"`)
.join(
', ',
)}. You provided "${suppliedValue}", which is not valid. Please use one of the allowed values instead..`;
return {
description,
message: description,
path: propertyName,
};
};
export const fromOpenApiValidationError =
(requestBody: object) =>
(validationError: ErrorObject): ValidationErrorDescription => {
const { instancePath, params, message } = validationError;
const propertyName = instancePath.substring('/body/'.length);
switch (validationError.keyword) {
case 'required':
return missingRequiredPropertyMessage(
propertyName,
params.missingProperty,
);
case 'additionalProperties':
return additionalPropertiesMessage(
propertyName,
params.additionalProperty,
);
case 'enum':
return enumMessage(
instancePath.substring(instancePath.lastIndexOf('/') + 1),
message,
params.allowedValues,
getProp(
requestBody,
instancePath.substring('/body/'.length).split('/'),
),
);
case 'oneOf':
return oneOfMessage(propertyName, validationError.message);
default:
return genericErrorMessage(requestBody, propertyName, message);
}
};
export const fromOpenApiValidationErrors = (
requestBody: object,
validationErrors: [ErrorObject, ...ErrorObject[]],
): BadDataError => {
const [firstDetail, ...remainingDetails] = validationErrors.map(
fromOpenApiValidationError(requestBody),
);
return new BadDataError(
"Request validation failed: the payload you provided doesn't conform to the schema. Check the `details` property for a list of errors that we found.",
[firstDetail, ...remainingDetails],
);
};
export const fromJoiError = (err: ValidationError): BadDataError => {
const details = err.details.map((detail) => {
const messageEnd = detail.context?.value
? `. You provided ${JSON.stringify(detail.context.value)}.`
: '.';
const description = detail.message + messageEnd;
return {
description,
message: description,
};
});
const [first, ...rest] = details;
if (first) {
return new BadDataError(
'A validation error occurred while processing your request data. Refer to the `details` property for more information.',
[first, ...rest],
);
} else {
return new BadDataError(
'A validation error occurred while processing your request data. Please make sure it conforms to the request data schema.',
);
}
};