koa-req-validation
Version:
A request validation middleware for Koa.
1,005 lines • 28 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const validator_1 = __importDefault(require("validator"));
const types_1 = require("./types");
const ValidationResult_1 = __importDefault(require("./ValidationResult"));
/**
* The validation chain object.
*/
class ValidationChain {
/**
* Create a new ValidationChain.
*
* @param parameter Name of the parameter to validate
* @param location Location of the parameter in request
*/
constructor(parameter, location) {
/**
* Default error message when validation fails.
*/
this.defaultErrorMessage = "Invalid value";
/**
* Validations and sanitations to be executed.
*/
this.operations = [];
/**
* Is this parameter optional?
*/
this.isOptional = { value: false };
this.hasNonStringSanitizer = false;
/**
* Build the validation chain. This method has to be called at the end of each
* validation.
*
* ```typescript
* router.post(
* '/auth/login',
* body('username').equals('user').build(),
* body('password').equals('pass').build(),
* handler
* );
* ```
*/
this.build = () => (ctx, next) => __awaiter(this, void 0, void 0, function* () {
const results = yield this.checkResults(ctx);
if (results) {
if (Array.isArray(ctx.state.validationResults)) {
ctx.state.validationResults.push(results);
}
else {
ctx.state.validationResults = [results];
}
}
yield next();
});
/**
* @deprecated Use `build()` instead
*/
this.run = () => {
// eslint-disable-next-line no-console
console.warn("ValidationChain.run() is deprecated. Please use .build() instead.");
return this.build();
};
this.parameter = parameter.split(".");
if (!Object.values(types_1.ParamLocation).includes(location)) {
throw new TypeError(`Param location has to be one of ` +
Object.values(types_1.ParamLocation).join(", ") +
` but received ${location}`);
}
this.location = location;
}
/**
* Pass a custom message to the validation.
*
* @param message Custom message
*
* @throws {Error} No validation has been set before `withMessage()` has been called
*/
withMessage(message) {
if (this.operations.length < 1) {
throw new Error(`Can't set a validation error message using withMessage() when ` +
`no validations have been defined`);
}
const validationDefinition = this.operations[this.operations.length - 1];
if (validationDefinition.type === "sanitation") {
throw new Error(`Can't set a validation error message using withMessage() ` +
`to a sanitizer definition! Please call withMessage() immediately after ` +
`the validation definition.`);
}
validationDefinition.message = message;
return this;
}
/**
* Set this property as optional.
*/
optional(options) {
this.isOptional = {
value: true,
options,
};
return this;
}
/**
* Custom async validation function to execute. The function
* must throw when the validation fails.
*
* @param func The validation function
*/
custom(func) {
if (typeof func !== "function") {
throw new TypeError(`Expected to receive a custom validation function but received: ${func}`);
}
return this.addValidation({
type: "validation",
validation: "custom",
func,
});
}
/**
* Check if the request property contains the given seed.
*/
contains(seed) {
return this.addValidation({
type: "validation",
validation: "contains",
options: seed,
});
}
/**
* Check if the request property equals the given comparison.
*/
equals(comparison) {
return this.addValidation({
type: "validation",
validation: "equals",
options: comparison,
});
}
/**
* Check if the parameter is an integer.
*/
isInt(options) {
return this.addValidation({
type: "validation",
validation: "isInt",
options,
});
}
/**
* Check if the string is in given length.
*
* @param options Min and max length
*/
isLength(options) {
return this.addValidation({
type: "validation",
validation: "isLength",
options,
});
}
/**
* Check if the parameter is an email.
*/
isEmail(options) {
return this.addValidation({
type: "validation",
validation: "isEmail",
options,
});
}
/**
* Check if the parameter is a boolean value.
*/
isBoolean() {
return this.addValidation({
type: "validation",
validation: "isBoolean",
});
}
/**
* Check if the parameter is a zero length string.
*/
isEmpty(options) {
return this.addValidation({
type: "validation",
validation: "isEmpty",
options,
});
}
/**
* Check if the parameter is a float.
*/
isFloat(options) {
return this.addValidation({
type: "validation",
validation: "isFloat",
options,
});
}
/**
* Check if the parameter is an algorithm.
*
* @param algorithm The algorithm
*/
isHash(algorithm) {
return this.addValidation({
type: "validation",
validation: "isHash",
options: algorithm,
});
}
/**
* Check if the parameter is a valid JWT token.
*/
isJWT() {
return this.addValidation({
type: "validation",
validation: "isJWT",
});
}
/**
* Check if the parameter is a valid JSON. Uses
* `JSON.parse`.
*/
isJSON() {
return this.addValidation({
type: "validation",
validation: "isJSON",
});
}
/**
* Check if the parameter is a latitude-lognitude coordinate
* in the format `lat,long` or `lat, long`.
*/
isLatLong() {
return this.addValidation({
type: "validation",
validation: "isLatLong",
});
}
/**
* Check if the paramter contains only lowercase characters.
*/
isLowercase() {
return this.addValidation({
type: "validation",
validation: "isLowercase",
});
}
/**
* Check if the parameter is a MAC address.
*/
isMACAddress(options) {
return this.addValidation({
type: "validation",
validation: "isMACAddress",
options,
});
}
/**
* Check if the parameter is a valid MongoDB ObjectId.
*/
isMongoId() {
return this.addValidation({
type: "validation",
validation: "isMongoId",
});
}
/**
* Check if the parameter contains only numbers.
*/
isNumeric(options) {
return this.addValidation({
type: "validation",
validation: "isNumeric",
options,
});
}
/**
* Check if the parameter is a valid port number.
*/
isPort() {
return this.addValidation({
type: "validation",
validation: "isPort",
});
}
/**
* Check if the parameter is valid UUID (v3, v4 or v5).
*/
isUUID(version) {
return this.addValidation({
type: "validation",
validation: "isUUID",
options: version,
});
}
/**
* Check if the parameter contains only uppercase characters.
*/
isUppercase() {
return this.addValidation({
type: "validation",
validation: "isUppercase",
});
}
/**
* Check if the parameter matches given regular expression.
*
* @param regExp The regular expression
*/
matches(regExp) {
return this.addValidation({
type: "validation",
validation: "matches",
options: regExp,
});
}
/**
* Check if the parameter is some of the allowed
* values.
*
* @param values Options containing at least `values`
* property with allowed values
*/
isIn(values) {
return this.addValidation({
type: "validation",
validation: "isIn",
options: values,
});
}
/**
* Check if the string is a date that's after the specified
* date (defaults to now).
*
* @param date The date (defaults to now)
*/
isAfter(date) {
return this.addValidation({
type: "validation",
validation: "isAfter",
options: date,
});
}
/**
* Check if the string contains only letters. Locale
* defaults to en-US.
*
* @param locale The locale
*/
isAlpha(locale) {
return this.addValidation({
type: "validation",
validation: "isAlpha",
options: locale,
});
}
/**
* Check if the string contains only letters and numbers.
* Locale defaults to en-US.
*
* @param locale The locale
*/
isAlphanumeric(locale) {
return this.addValidation({
type: "validation",
validation: "isAlphanumeric",
options: locale,
});
}
/**
* Check if the string contains ASCII characters only.
*/
isAscii() {
return this.addValidation({
type: "validation",
validation: "isAscii",
});
}
/**
* Check if the string is base64 encoded.
*/
isBase64() {
return this.addValidation({
type: "validation",
validation: "isBase64",
});
}
/**
* Check if the string is a date that's before
* the given date, which defaults to now.
*
* @param date The date (defaults to now)
*/
isBefore(date) {
return this.addValidation({
type: "validation",
validation: "isBefore",
options: date,
});
}
/**
* Check if the strin's length (in UTF-8 bytes)
* falls in range.
*
* @param options The range
*/
isByteLength(options = { min: 0 }) {
return this.addValidation({
type: "validation",
validation: "isByteLength",
options,
});
}
/**
* Check if the string is a credit card.
*/
isCreditCard() {
return this.addValidation({
type: "validation",
validation: "isCreditCard",
});
}
/**
* Check if the string is a valid currency amount.
*
* @param options The options
*/
isCurrency(options) {
return this.addValidation({
type: "validation",
validation: "isCurrency",
options,
});
}
/**
* Check if the string is a data uri format.
*/
isDataURI() {
return this.addValidation({
type: "validation",
validation: "isDataURI",
});
}
/**
* Check if the string represents a decimal number.
*
* @param options The options
*/
isDecimal(options) {
return this.addValidation({
type: "validation",
validation: "isDecimal",
options,
});
}
/**
* Check if the string is a number divisible by
* given number.
*
* @param division The division number
*/
isDivisibleBy(division) {
return this.addValidation({
type: "validation",
validation: "isDivisibleBy",
options: division,
});
}
/**
* Check if the string is fully qualified
* domain name.
*
* @param options The options
*/
isFQDN(options) {
return this.addValidation({
type: "validation",
validation: "isFQDN",
options,
});
}
/**
* Check if the string contains any full-width
* chars.
*/
isFullWidth() {
return this.addValidation({
type: "validation",
validation: "isFullWidth",
});
}
/**
* Check if the string contains any half-width
* chars.
*/
isHalfWidth() {
return this.addValidation({
type: "validation",
validation: "isHalfWidth",
});
}
/**
* Check if the string is a hexadecimal
* color.
*/
isHexColor() {
return this.addValidation({
type: "validation",
validation: "isHexColor",
});
}
/**
* Check if the string is a hexadecimal
* number.
*/
isHexadecimal() {
return this.addValidation({
type: "validation",
validation: "isHexadecimal",
});
}
/**
* Check if the string is an IP (ver 4 or 6).
*/
isIP(version) {
return this.addValidation({
type: "validation",
validation: "isIP",
options: version,
});
}
/**
* Check if the string is an IP range (ver 4 only).
*/
isIPRange() {
return this.addValidation({
type: "validation",
validation: "isIPRange",
});
}
/**
* Check if the string is an ISBN.
*
* @param version The version
*/
isISBN(version) {
return this.addValidation({
type: "validation",
validation: "isISBN",
options: version,
});
}
/**
* Check if the string is an ISSN.
*
* @param options The options
*/
isISSN(options) {
return this.addValidation({
type: "validation",
validation: "isISSN",
options,
});
}
/**
* Check if the string is an ISIN.
*/
isISIN() {
return this.addValidation({
type: "validation",
validation: "isISIN",
});
}
/**
* Check if the string is valid ISO8601 date.
*/
isISO8601(options) {
return this.addValidation({
type: "validation",
validation: "isISO8601",
options,
});
}
/**
* Check if the string is valid RFC3339 date.
*/
isRFC3339() {
return this.addValidation({
type: "validation",
validation: "isRFC3339",
});
}
/**
* Check if the string is a valid ISO 3166-1 alpha-2
* officially assigned country code.
*/
isISO31661Alpha2() {
return this.addValidation({
type: "validation",
validation: "isISO31661Alpha2",
});
}
/**
* Check if the string is a valid ISO 3166-1 alpha-3
* officially assigned country code.
*/
isISO31661Alpha3() {
return this.addValidation({
type: "validation",
validation: "isISO31661Alpha3",
});
}
/**
* Check if the string is a ISRC.
*/
isISRC() {
return this.addValidation({
type: "validation",
validation: "isISRC",
});
}
/**
* Check if the string is a MD5 hash.
*/
isMD5() {
return this.addValidation({
type: "validation",
validation: "isMD5",
});
}
/**
* Check if the string is a valid MIME type format.
*/
isMimeType() {
return this.addValidation({
type: "validation",
validation: "isMimeType",
});
}
/**
* Check if the string is a mobile phone number.
*
* @param locale The locale, defaults to any
*/
isMobilePhone(locale = "any") {
return this.addValidation({
type: "validation",
validation: "isMobilePhone",
options: locale,
});
}
/**
* Check if the string contains one or more multibyte chars.
*/
isMultibyte() {
return this.addValidation({
type: "validation",
validation: "isMultibyte",
});
}
/**
* Check if the string is a postal code.
*
* @param locale The locale to use
*/
isPostalCode(locale = "any") {
return this.addValidation({
type: "validation",
validation: "isPostalCode",
options: locale,
});
}
/**
* Check if the string contains any surrogate pairs chars.
*/
isSurrogatePair() {
return this.addValidation({
type: "validation",
validation: "isSurrogatePair",
});
}
/**
* Check if the string is an URL.
*
* @param options Possible options
*/
isURL(options) {
return this.addValidation({
type: "validation",
validation: "isURL",
options,
});
}
/**
* Check if the string contains a mixture of full and half-width chars.
*/
isVariableWidth() {
return this.addValidation({
type: "validation",
validation: "isVariableWidth",
});
}
/**
* Checks characters if they appear in the whitelist.
*
* @param chars The characters
*/
isWhitelisted(chars) {
return this.addValidation({
type: "validation",
validation: "isWhitelisted",
options: chars,
});
}
/**
* Remove characters that appear in the blacklist. The characters are used in a RegExp
* and so you will need to escape some chars, e.g. blacklist(input, '\\[\\]').
*
* @param chars Characters to blacklist
*/
blacklist(chars) {
return this.addSanitation({
type: "sanitation",
sanitation: "blacklist",
options: chars,
});
}
/**
* Replace <, >, &, ', ' and / with HTML entities.
*/
escape() {
return this.addSanitation({
type: "sanitation",
sanitation: "escape",
});
}
/**
* Replaces HTML encoded entities with <, >, &, ", ' and /.
*/
unescape() {
return this.addSanitation({
type: "sanitation",
sanitation: "unescape",
});
}
/**
* Trim characters from the left-side of the input.
*
* @param chars The characters to trim
*/
ltrim(chars) {
return this.addSanitation({
type: "sanitation",
sanitation: "ltrim",
options: chars,
});
}
/**
* Trim characters from the right-side of the input.
*
* @param chars The characters to trim
*/
rtrim(chars) {
return this.addSanitation({
type: "sanitation",
sanitation: "rtrim",
options: chars,
});
}
/**
* Normalize email address.
*
* @param options The options
*
* @see https://github.com/chriso/validator.js For details
*/
normalizeEmail(options) {
return this.addSanitation({
type: "sanitation",
sanitation: "normalizeEmail",
options,
});
}
/**
* Remove characters with a numerical value < 32 and 127, mostly control characters.
* If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA
* and 0xD). Unicode-safe in JavaScript.
*
* @param keepNewLines
*/
stripLow(keepNewLines = false) {
return this.addSanitation({
type: "sanitation",
sanitation: "stripLow",
options: keepNewLines,
});
}
/**
* convert the input string to a boolean. Everything except for '0', 'false' and ''
* returns true. In strict mode only '1' and 'true' return true.
*/
toBoolean(strict = false) {
return this.addSanitation({
type: "sanitation",
sanitation: "toBoolean",
options: strict,
}, true);
}
/**
* Convert the input string to a date.
*/
toDate() {
return this.addSanitation({
type: "sanitation",
sanitation: "toDate",
}, true);
}
/**
* Convert the input string to a float.
*/
toFloat() {
return this.addSanitation({
type: "sanitation",
sanitation: "toFloat",
}, true);
}
/**
* Convert the input string to an integer, or NaN if the input is not an integer.
*/
toInt(radix = 10) {
return this.addSanitation({
type: "sanitation",
sanitation: "toInt",
options: radix,
}, true);
}
/**
* Trim characters (whitespace by default) from both sides of the input.
*
* @param chars The characters to trim
*/
trim(chars) {
return this.addSanitation({
type: "sanitation",
sanitation: "trim",
options: chars,
});
}
/**
* Remove characters that do not appear in the whitelist. The characters are used in a
* RegExp and so you will need to escape some chars, e.g. whitelist(input, '\\[\\]').
*
* @param chars Characters to whitelist
*/
whitelist(chars) {
return this.addSanitation({
type: "sanitation",
sanitation: "whitelist",
options: chars,
});
}
/**
* Run the validations and return the results.
* @param ctx The context
*/
checkResults(ctx) {
return __awaiter(this, void 0, void 0, function* () {
const originalInput = this.getOriginalInput(ctx);
const param = this.parameter.join(".");
if (typeof originalInput === "undefined") {
if (this.isOptional.value) {
return null;
}
return new ValidationResult_1.default(param, undefined, [
{
param,
location: this.location,
msg: "Missing value",
value: "",
},
]);
}
else if (originalInput === null) {
if (this.isOptional.options && this.isOptional.options.allowNull) {
return null;
}
return new ValidationResult_1.default(param, undefined, [
{ param, location: this.location, msg: "Invalid value", value: null },
]);
}
let input = originalInput + "";
const errors = yield this.operations.reduce((arrP, current) => __awaiter(this, void 0, void 0, function* () {
const arr = yield arrP;
if (isSanitation(current)) {
// If some of the validations have failed, we can't do any sanitations
if (arr.length) {
return arr;
}
input = this.sanitize(input, current);
return arr;
}
const { message } = current;
const finalMessage = typeof message === "function" ? message(ctx, input) : message;
if (current.validation === "custom") {
try {
yield current.func(input, ctx);
}
catch (e) {
arr.push({
msg: finalMessage !== null && finalMessage !== void 0 ? finalMessage : (e instanceof Error && e.message
? e.message
: this.defaultErrorMessage),
location: this.location,
param,
value: originalInput + "",
});
}
}
else if (!validator_1.default[current.validation](input, current.options)) {
arr.push({
msg: finalMessage || this.defaultErrorMessage,
location: this.location,
param,
value: originalInput + "",
});
}
return arr;
}), Promise.resolve([]));
return new ValidationResult_1.default(param, errors.length ? undefined : input, errors);
});
}
/**
* Get original input as it is from the request body.
*
* @param ctx The context
*/
getOriginalInput(ctx) {
let obj;
switch (this.location) {
case types_1.ParamLocation.BODY:
obj = ctx.request.body;
break;
case types_1.ParamLocation.PARAM:
obj = ctx.params;
break;
case types_1.ParamLocation.QUERY:
obj = ctx.query;
break;
}
return this.getParamFromObject(obj);
}
/**
* Get parameter from object.
*
* @param object Object to look the property from
*/
getParamFromObject(object) {
return this.parameter.reduce((prev, current) => {
if (typeof prev === "object" && prev) {
return prev[current];
}
return undefined;
}, object);
}
/**
* Sanitize the given input value with given sanitation definition.
*
* @param input The input as string
*/
sanitize(input, sanitationDefinition) {
const { sanitation, options } = sanitationDefinition;
const fn = validator_1.default[sanitation];
return fn(input, options);
}
addValidation(definition) {
if (this.hasNonStringSanitizer) {
throw new Error("Validations cannot be done after using sanitizers that convert the type of input to non-string value");
}
this.operations.push(definition);
return this;
}
addSanitation(definition, nonStringOutput) {
if (nonStringOutput) {
this.hasNonStringSanitizer = true;
}
this.operations.push(definition);
return this;
}
}
exports.default = ValidationChain;
const isSanitation = (definition) => definition.type === "sanitation";
//# sourceMappingURL=ValidationChain.js.map