diff --git a/src/db/Validator.ts b/src/db/Validator.ts index 85ca6b8..cc79eca 100644 --- a/src/db/Validator.ts +++ b/src/db/Validator.ts @@ -1,6 +1,7 @@ import Model from "./Model"; import Query from "./Query"; import {Connection} from "mysql"; +import {Type} from "../Utils"; export default class Validator { private readonly steps: ValidationStep[] = []; @@ -48,11 +49,11 @@ export default class Validator { return this; } - public acceptUndefined(): Validator { + public acceptUndefined(alsoAcceptEmptyString: boolean = false): Validator { this.addStep({ verifyStep: () => true, throw: null, - interrupt: val => val === undefined || val === null, + interrupt: val => val === undefined || val === null || (alsoAcceptEmptyString && typeof val === 'string' && val.length === 0), isFormat: true, }); return this; @@ -67,6 +68,15 @@ export default class Validator { return this; } + public sameAs(otherName?: string, other?: T): Validator { + this.addStep({ + verifyStep: val => val === other, + throw: () => new DifferentThanError(otherName), + isFormat: true, + }); + return this; + } + public regexp(regexp: RegExp): Validator { this.validationAttributes.push(`pattern="${regexp}"`); this.addStep({ @@ -92,7 +102,7 @@ export default class Validator { public minLength(minLength: number): Validator { this.addStep({ verifyStep: val => (val).length >= minLength, - throw: () => new BadLengthValidationError(minLength, 1000000), + throw: () => new TooShortError(minLength), isFormat: true, }); return this; @@ -104,7 +114,7 @@ export default class Validator { public maxLength(maxLength: number): Validator { this.addStep({ verifyStep: val => (val).length <= maxLength, - throw: () => new BadLengthValidationError(0, maxLength), + throw: () => new TooLongError(maxLength), isFormat: true, }); return this; @@ -158,19 +168,20 @@ export default class Validator { return this; } - public unique(model: Model, querySupplier?: () => Query): Validator { + public unique(model: M | Type, foreignKey?: string, querySupplier?: () => Query): Validator { this.addStep({ verifyStep: async (val, thingName, c) => { + if (!foreignKey) foreignKey = thingName; let query: Query; if (querySupplier) { - query = querySupplier().where(thingName, val); + query = querySupplier().where(foreignKey, val); } else { - query = (model.constructor).select('1').where(thingName, val); + query = (model instanceof Model ? model.constructor : model).select('1').where(foreignKey, val); } - if (typeof model.id === 'number') query = query.whereNot('id', model.id); + if (model instanceof Model && typeof model.id === 'number') query = query.whereNot('id', model.id); return (await query.execute(c)).results.length === 0; }, - throw: () => new AlreadyExistsValidationError(model.table), + throw: () => new AlreadyExistsValidationError((model).table), isFormat: false, }); return this; @@ -210,26 +221,37 @@ interface ValidationStep { } export class ValidationBag extends Error { - private readonly messages: { [p: string]: any } = {}; + private readonly errors: ValidationError[] = []; public addMessage(err: ValidationError) { - if (!err.thingName) { - throw new Error('Null thing name'); - } + if (!err.thingName) throw new Error('Null thing name'); + this.errors.push(err); + } - this.messages[err.thingName] = { - name: err.name, - message: err.message, - value: err.value, - }; + public addBag(otherBag: ValidationBag) { + for (const error of otherBag.errors) { + this.errors.push(error); + } } public hasMessages(): boolean { - return Object.keys(this.messages).length > 0; + return this.errors.length > 0; } public getMessages(): { [p: string]: ValidationError } { - return this.messages; + const messages: { [p: string]: ValidationError } = {}; + for (const err of this.errors) { + messages[err.thingName!] = { + name: err.name, + message: err.message, + value: err.value, + }; + } + return messages; + } + + public getErrors(): ValidationError[] { + return this.errors; } } @@ -258,6 +280,32 @@ export class BadLengthValidationError extends ValidationError { } } +export class TooShortError extends ValidationError { + private readonly minLength: number; + + constructor(minLength: number) { + super(); + this.minLength = minLength; + } + + public get message(): string { + return `${this.thingName} must be at least ${this.minLength} characters.`; + } +} + +export class TooLongError extends ValidationError { + private readonly maxLength: number; + + constructor(maxLength: number) { + super(); + this.maxLength = maxLength; + } + + public get message(): string { + return `${this.thingName} must be at most ${this.maxLength} characters.`; + } +} + export class BadValueValidationError extends ValidationError { private readonly expectedValue: any; @@ -271,6 +319,19 @@ export class BadValueValidationError extends ValidationError { } } +export class DifferentThanError extends ValidationError { + private readonly otherName: any; + + constructor(otherName: any) { + super(); + this.otherName = otherName; + } + + public get message(): string { + return `This should be the same as ${this.otherName}.` + } +} + export class OutOfRangeValidationError extends ValidationError { private readonly min?: number; private readonly max?: number; @@ -312,7 +373,7 @@ export class AlreadyExistsValidationError extends ValidationError { } public get message(): string { - return `${this.value} already exists in ${this.table}.${this.thingName}.`; + return `${this.thingName} already exists in ${this.table}.`; } }