swaf/src/db/Validator.ts

308 lines
9.1 KiB
TypeScript

import Model from "./Model";
import Query from "./Query";
import {Connection} from "mysql";
export default class Validator<T> {
private readonly steps: ValidationStep<T>[] = [];
private readonly validationAttributes: string[] = [];
private _min?: number;
private _max?: number;
/**
* @param thingName The name of the thing to validate.
* @param value The value to verify.
* @param onlyFormat {@code true} to only validate format properties, {@code false} otherwise.
* @param connection A connection to use in case of wrapped transactions.
*/
async execute(thingName: string, value: T | undefined, onlyFormat: boolean, connection?: Connection): Promise<void> {
const bag = new ValidationBag();
for (const step of this.steps) {
if (onlyFormat && !step.isFormat) continue;
const result = step.verifyStep(value, thingName, connection);
if ((result === false || result instanceof Promise && (await result) === false) && step.throw) {
const error: ValidationError = step.throw();
error.thingName = thingName;
error.value = value;
bag.addMessage(error);
} else if (step.interrupt !== undefined && step.interrupt(value)) {
break;
}
}
if (bag.hasMessages()) {
throw bag;
}
}
public defined(): Validator<T> {
this.validationAttributes.push('required');
this.addStep({
verifyStep: val => val !== undefined,
throw: () => new UndefinedValueValidationError(),
isFormat: true,
});
return this;
}
public acceptUndefined(): Validator<T> {
this.addStep({
verifyStep: () => true,
throw: null,
interrupt: val => val === undefined || val === null,
isFormat: true,
});
return this;
}
public equals(other?: T): Validator<T> {
this.addStep({
verifyStep: val => val === other,
throw: () => new BadValueValidationError(other),
isFormat: true,
});
return this;
}
public regexp(regexp: RegExp): Validator<T> {
this.validationAttributes.push(`pattern="${regexp}"`);
this.addStep({
verifyStep: val => regexp.test(<string><unknown>val),
throw: () => new InvalidFormatValidationError(),
isFormat: true,
});
return this;
}
public length(length: number): Validator<T> {
this.addStep({
verifyStep: val => (<any>val).length === length,
throw: () => new BadLengthValidationError(length),
isFormat: true,
});
return this;
}
/**
* @param minLength included
* @param maxLength included
*/
public between(minLength: number, maxLength: number): Validator<T> {
this.addStep({
verifyStep: val => {
const length = (<any>val).length;
return length >= minLength && length <= maxLength;
},
throw: () => new BadLengthValidationError(minLength, maxLength),
isFormat: true,
});
return this;
}
/**
* @param min included
*/
public min(min: number): Validator<T> {
this.validationAttributes.push(`min="${min}"`);
this._min = min;
this.addStep({
verifyStep: val => {
return (<any>val) >= min;
},
throw: () => new OutOfRangeValidationError(this._min, this._max),
isFormat: true,
});
return this;
}
/**
* @param max included
*/
public max(max: number): Validator<T> {
this.validationAttributes.push(`max="${max}"`);
this._max = max;
this.addStep({
verifyStep: val => {
return (<any>val) <= max;
},
throw: () => new OutOfRangeValidationError(this._min, this._max),
isFormat: true,
});
return this;
}
public unique(model: Model, querySupplier?: () => Query): Validator<T> {
this.addStep({
verifyStep: async (val, thingName, c) => {
let query: Query;
if (querySupplier) {
query = querySupplier().where(thingName, val);
} else {
query = (<any>model.constructor).select('1').where(thingName, val);
}
if (typeof model.id === 'number') query = query.whereNot('id', model.id);
return (await query.execute(c)).results.length === 0;
},
throw: () => new AlreadyExistsValidationError(model.table),
isFormat: false,
});
return this;
}
public exists(modelClass: Function, foreignKey?: string): Validator<T> {
this.addStep({
verifyStep: async (val, thingName, c) => (await (<any>modelClass).select('1').where(foreignKey !== undefined ? foreignKey : thingName, val).execute(c)).results.length >= 1,
throw: () => new UnknownRelationValidationError((<any>modelClass).table, foreignKey),
isFormat: false,
});
return this;
}
private addStep(step: ValidationStep<T>) {
this.steps.push(step);
}
public getValidationAttributes(): string[] {
return this.validationAttributes;
}
public step(step: number): Validator<T> {
this.validationAttributes.push(`step="${step}"`);
return this;
}
}
interface ValidationStep<T> {
interrupt?: (val?: T) => boolean;
verifyStep(val: T | undefined, thingName: string, connection?: Connection): boolean | Promise<boolean>;
throw: ((val?: T) => ValidationError) | null;
readonly isFormat: boolean;
}
export class ValidationBag extends Error {
private readonly messages: { [p: string]: any } = {};
public addMessage(err: ValidationError) {
if (!err.thingName) {
throw new Error('Null thing name');
}
this.messages[err.thingName] = {
name: err.name,
message: err.message,
value: err.value,
};
}
public hasMessages(): boolean {
return Object.keys(this.messages).length > 0;
}
public getMessages(): { [p: string]: ValidationError } {
return this.messages;
}
}
export abstract class ValidationError extends Error {
public thingName?: string;
public value?: any;
public get name(): string {
return this.constructor.name;
}
}
export class BadLengthValidationError extends ValidationError {
private readonly expectedLength: number;
private readonly maxLength?: number;
constructor(expectedLength: number, maxLength?: number) {
super();
this.expectedLength = expectedLength;
this.maxLength = maxLength;
}
public get message(): string {
return `${this.thingName} expected length: ${this.expectedLength}${this.maxLength !== undefined ? ` to ${this.maxLength}` : ''}; ` +
`actual length: ${this.value.length}.`;
}
}
export class BadValueValidationError extends ValidationError {
private readonly expectedValue: any;
constructor(expectedValue: any) {
super();
this.expectedValue = expectedValue;
}
public get message(): string {
return `Expected: ${this.expectedValue}; got: ${this.value}.`
}
}
export class OutOfRangeValidationError extends ValidationError {
private readonly min?: number;
private readonly max?: number;
constructor(min?: number, max?: number) {
super();
this.min = min;
this.max = max;
}
public get message(): string {
if (this.min === undefined) {
return `${this.thingName} must be at most ${this.max}`;
} else if (this.max === undefined) {
return `${this.thingName} must be at least ${this.min}`;
}
return `${this.thingName} must be between ${this.min} and ${this.max}.`;
}
}
export class InvalidFormatValidationError extends ValidationError {
public get message(): string {
return `"${this.value}" is not a valid ${this.thingName}.`;
}
}
export class UndefinedValueValidationError extends ValidationError {
public get message(): string {
return `${this.thingName} is required.`;
}
}
export class AlreadyExistsValidationError extends ValidationError {
private readonly table: string;
constructor(table: string) {
super();
this.table = table;
}
public get message(): string {
return `${this.value} already exists in ${this.table}.${this.thingName}.`;
}
}
export class UnknownRelationValidationError extends ValidationError {
private readonly table: string;
private readonly foreignKey?: string;
constructor(table: string, foreignKey?: string) {
super();
this.table = table;
this.foreignKey = foreignKey;
}
public get message(): string {
return `${this.thingName}=${this.value} relation was not found in ${this.table}${this.foreignKey !== undefined ? `.${this.foreignKey}` : ''}.`;
}
}