import { SMDate } from "./datetime"; import { bytesReadable } from "../helpers/types"; export interface ValidationObject { validate: (value: string) => ValidationResult; } export interface ValidationResult { valid: boolean; invalidMessages: Array; } export const defaultValidationResult: ValidationResult = { valid: true, invalidMessages: [], }; export const createValidationResult = ( valid: boolean, message: string | Array = "" ) => { if (typeof message == "string") { message = [message]; } return { valid: valid, invalidMessages: message, }; }; /** * Validation Min */ const VALIDATION_MIN_TYPE = ["String", "Number"]; type ValidationMinType = (typeof VALIDATION_MIN_TYPE)[number]; interface ValidationMinOptions { min: number; type?: ValidationMinType; invalidMessage?: string | ((options: ValidationMinOptions) => string); } interface ValidationMinObject extends ValidationMinOptions { validate: (value: string) => ValidationResult; } const defaultValidationMinOptions: ValidationMinOptions = { min: 1, type: "String", invalidMessage: (options: ValidationMinOptions) => { return options.type == "String" ? `Required to be at least ${options.min} characters.` : `Required to be at least ${options.min}.`; }, }; export function Min( minOrOptions: number | ValidationMinOptions, options?: ValidationMinOptions ); export function Min(options: ValidationMinOptions): ValidationMinObject; /** * Validate field length or number is at minimum or higher/larger * * @param minOrOptions minimum number or options data * @param options options data * @returns ValidationMinObject */ export function Min( minOrOptions: number | ValidationMinOptions, options?: ValidationMinOptions ): ValidationMinObject { if (typeof minOrOptions === "number") { options = { ...defaultValidationMinOptions, ...(options || {}) }; options.min = minOrOptions; } else { options = { ...defaultValidationMinOptions, ...(minOrOptions || {}) }; } return { ...options, validate: function (value: string): ValidationResult { return { valid: this.type == "String" ? value.toString().length >= this.min : parseInt(value) >= this.min, invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * Validation Max */ const VALIDATION_MAX_TYPE = ["String", "Number"]; type ValidationMaxType = (typeof VALIDATION_MAX_TYPE)[number]; interface ValidationMaxOptions { max: number; type?: ValidationMaxType; invalidMessage?: string | ((options: ValidationMaxOptions) => string); } interface ValidationMaxObject extends ValidationMaxOptions { validate: (value: string) => ValidationResult; } const defaultValidationMaxOptions: ValidationMaxOptions = { max: 1, type: "String", invalidMessage: (options: ValidationMaxOptions) => { return options.type == "String" ? `Required to be less than ${options.max + 1} characters.` : `Required to be less than ${options.max + 1}.`; }, }; export function Max( maxOrOptions: number | ValidationMaxOptions, options?: ValidationMaxOptions ): ValidationMaxObject; export function Max(options: ValidationMaxOptions): ValidationMaxObject; /** * Validate field length or number is at maximum or smaller * * @param maxOrOptions maximum number or options data * @param options options data * @returns ValidationMaxObject */ export function Max( maxOrOptions: number | ValidationMaxOptions, options?: ValidationMaxOptions ): ValidationMaxObject { if (typeof maxOrOptions === "number") { options = { ...defaultValidationMaxOptions, ...(options || {}) }; options.max = maxOrOptions; } else { options = { ...defaultValidationMaxOptions, ...(maxOrOptions || {}) }; } return { ...options, validate: function (value: string): ValidationResult { return { valid: this.type == "String" ? value.toString().length <= this.max : parseInt(value) <= this.max, invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * PASSWORD */ interface ValidationPasswordOptions { invalidMessage?: string | ((options: ValidationPasswordOptions) => string); } interface ValidationPasswordObject extends ValidationPasswordOptions { validate: (value: string) => ValidationResult; } const defaultValidationPasswordOptions: ValidationPasswordOptions = { invalidMessage: "Your password needs to have at least a letter, a number and a special character.", }; /** * Validate field is in a valid password format * * @param options options data * @returns ValidationPasswordObject */ export function Password( options?: ValidationPasswordOptions ): ValidationPasswordObject { options = { ...defaultValidationPasswordOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: /(?=.*[A-Za-z])(?=.*\d)(?=.*[.@$!%*#?&])[A-Za-z\d.@$!%*#?&]{1,}$/.test( value ), invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * EMAIL */ interface ValidationEmailOptions { invalidMessage?: string | ((options: ValidationEmailOptions) => string); } interface ValidationEmailObject extends ValidationEmailOptions { validate: (value: string) => ValidationResult; } const defaultValidationEmailOptions: ValidationEmailOptions = { invalidMessage: "Your email is not in a supported format.", }; /** * Validate field is in a valid Email format * * @param options options data * @returns ValidationEmailObject */ export function Email(options?: ValidationEmailOptions): ValidationEmailObject { options = { ...defaultValidationEmailOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: value.length == 0 || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value), invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * PHONE */ interface ValidationPhoneOptions { invalidMessage?: string | ((options: ValidationPhoneOptions) => string); } interface ValidationPhoneObject extends ValidationPhoneOptions { validate: (value: string) => ValidationResult; } const defaultValidationPhoneOptions: ValidationPhoneOptions = { invalidMessage: "Your Phone number is not in a supported format.", }; /** * Validate field is in a valid Phone format * * @param options options data * @returns ValidationPhoneObject */ export function Phone(options?: ValidationPhoneOptions): ValidationPhoneObject { options = { ...defaultValidationPhoneOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: value.length == 0 || /^(\+|00)?[0-9][0-9 \-().]{7,32}$/.test(value), invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * NUMBER */ interface ValidationNumberOptions { invalidMessage?: string | ((options: ValidationNumberOptions) => string); } interface ValidationNumberObject extends ValidationNumberOptions { validate: (value: string) => ValidationResult; } const defaultValidationNumberOptions: ValidationNumberOptions = { invalidMessage: "Must be a number.", }; /** * Validate field is in a valid Whole number format * * @param options options data * @returns ValidationNumberObject */ export function Number( options?: ValidationNumberOptions ): ValidationNumberObject { options = { ...defaultValidationNumberOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: value.length == 0 || /^0?\d+$/.test(value), invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * DATE */ interface ValidationDateOptions { before?: string | ((value: string) => string); after?: string | ((value: string) => string); invalidMessage?: string | ((options: ValidationDateOptions) => string); invalidBeforeMessage?: | string | ((options: ValidationDateOptions) => string); invalidAfterMessage?: string | ((options: ValidationDateOptions) => string); } interface ValidationDateObject extends ValidationDateOptions { validate: (value: string) => ValidationResult; } const defaultValidationDateOptions: ValidationDateOptions = { before: "", after: "", invalidMessage: "Must be a valid date.", invalidBeforeMessage: (options: ValidationDateOptions) => { return `Must be a date before ${options.before}.`; }, invalidAfterMessage: (options: ValidationDateOptions) => { return `Must be a date after ${options.after}.`; }, }; /** * Validate field is in a valid Date format * * @param options options data * @returns ValidationDateObject */ export function Date(options?: ValidationDateOptions): ValidationDateObject { options = { ...defaultValidationDateOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { let valid = true; let invalidMessageType = "invalidMessage"; const parsedDate = new SMDate(value); if (parsedDate.isValid() == true) { const beforeDate = new SMDate( typeof (options["before"] = options?.before || "") === "function" ? options.before(value) : options.before ); const afterDate = new SMDate( typeof (options["after"] = options?.after || "") === "function" ? options.after(value) : options.after ); if ( beforeDate.isValid() == true && parsedDate.isBefore(beforeDate) == false ) { valid = false; invalidMessageType = "invalidBeforeMessage"; } if ( afterDate.isValid() == true && parsedDate.isAfter(afterDate) == false ) { valid = false; invalidMessageType = "invalidAfterMessage"; } } else { valid = false; } return { valid: valid, invalidMessages: [ typeof this[invalidMessageType] === "string" ? this[invalidMessageType] : this[invalidMessageType](this), ], }; }, }; } /** * TIME */ interface ValidationTimeOptions { before?: string | ((value: string) => string); after?: string | ((value: string) => string); invalidMessage?: string | ((options: ValidationTimeOptions) => string); invalidBeforeMessage?: | string | ((options: ValidationTimeOptions) => string); invalidAfterMessage?: string | ((options: ValidationTimeOptions) => string); } interface ValidationTimeObject extends ValidationTimeOptions { validate: (value: string) => ValidationResult; } const defaultValidationTimeOptions: ValidationTimeOptions = { before: "", after: "", invalidMessage: "Must be a valid time.", invalidBeforeMessage: (options: ValidationTimeOptions) => { return `Must be a time before ${options.before}.`; }, invalidAfterMessage: (options: ValidationTimeOptions) => { return `Must be a time after ${options.after}.`; }, }; /** * Validate field is in a valid Time format * * @param options options data * @returns ValidationTimeObject */ export function Time(options?: ValidationTimeOptions): ValidationTimeObject { options = { ...defaultValidationTimeOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { let valid = true; let invalidMessageType = "invalidMessage"; const parsedTime = new SMDate(value); if (parsedTime.isValid() == true) { const beforeTime = new SMDate( typeof (options["before"] = options?.before || "") === "function" ? options.before(value) : options.before ); const afterTime = new SMDate( typeof (options["after"] = options?.after || "") === "function" ? options.after(value) : options.after ); if ( beforeTime.isValid() == true && parsedTime.isBefore(beforeTime) == false ) { valid = false; invalidMessageType = "invalidBeforeMessage"; } if ( afterTime.isValid() == true && parsedTime.isAfter(afterTime) == false ) { valid = false; invalidMessageType = "invalidAfterMessage"; } } else { valid = false; } return { valid: valid, invalidMessages: [ typeof this[invalidMessageType] === "string" ? this[invalidMessageType] : this[invalidMessageType](this), ], }; }, }; } /** * DATETIME */ interface ValidationDateTimeOptions { before?: string | ((value: string) => string); after?: string | ((value: string) => string); invalidMessage?: string | ((options: ValidationDateTimeOptions) => string); invalidBeforeMessage?: | string | ((options: ValidationDateTimeOptions) => string); invalidAfterMessage?: | string | ((options: ValidationDateTimeOptions) => string); } interface ValidationDateTimeObject extends ValidationDateTimeOptions { validate: (value: string) => ValidationResult; } const defaultValidationDateTimeOptions: ValidationDateTimeOptions = { before: "", after: "", invalidMessage: "Must be a valid date and time.", invalidBeforeMessage: (options: ValidationDateTimeOptions) => { return `Must be a date/time before ${options.before}.`; }, invalidAfterMessage: (options: ValidationDateTimeOptions) => { return `Must be a date/time after ${options.after}.`; }, }; /** * Validate field is in a valid Date format * * @param options options data * @returns ValidationDateObject */ export function DateTime( options?: ValidationDateTimeOptions ): ValidationDateTimeObject { options = { ...defaultValidationDateTimeOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { let valid = true; let invalidMessageType = "invalidMessage"; const parsedDate = new SMDate(value); if (parsedDate.isValid() == true) { const beforeDate = new SMDate( typeof (options["before"] = options?.before || "") === "function" ? options.before(value) : options.before ); const afterDate = new SMDate( typeof (options["after"] = options?.after || "") === "function" ? options.after(value) : options.after ); if ( beforeDate.isValid() == true && parsedDate.isBefore(beforeDate) == false ) { valid = false; invalidMessageType = "invalidBeforeMessage"; } if ( afterDate.isValid() == true && parsedDate.isAfter(afterDate) == false ) { valid = false; invalidMessageType = "invalidAfterMessage"; } } else { valid = false; } return { valid: valid, invalidMessages: [ typeof this[invalidMessageType] === "string" ? this[invalidMessageType] : this[invalidMessageType](this), ], }; }, }; } /** * CUSTOM */ type ValidationCustomCallback = (value: string) => boolean | string; interface ValidationCustomOptions { callback: ValidationCustomCallback; invalidMessage?: string | ((options: ValidationCustomOptions) => string); } interface ValidationCustomObject extends ValidationCustomOptions { validate: (value: string) => Promise; } const defaultValidationCustomOptions: ValidationCustomOptions = { callback: () => { return true; }, invalidMessage: "This field is invalid.", }; export function Custom( callbackOrOptions: ValidationCustomCallback | ValidationCustomOptions, options?: ValidationCustomOptions ); export function Custom( options: ValidationCustomOptions ): ValidationCustomObject; /** * Validate field is in a valid Custom format * * @param callbackOrOptions * @param options options data * @returns ValidationCustomObject */ export function Custom( callbackOrOptions: ValidationCustomCallback | ValidationCustomOptions, options?: ValidationCustomOptions ): ValidationCustomObject { if (typeof callbackOrOptions === "function") { options = { ...defaultValidationCustomOptions, ...(options || {}) }; options.callback = callbackOrOptions; } else { options = { ...defaultValidationCustomOptions, ...(callbackOrOptions || {}), }; } return { ...options, validate: async function (value: string): Promise { const validateResult = { valid: true, invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; const callbackResult = typeof this.callback === "function" ? await this.callback(value) : true; if (typeof callbackResult === "string") { if (callbackResult.length > 0) { validateResult.valid = false; validateResult.invalidMessages = [callbackResult]; } } else if (callbackResult !== true) { validateResult.valid = false; } return validateResult; }, }; } /** * And * * @param list */ export const And = (list: Array) => { return { list: list, validate: function (value: string) { const validationResult: ValidationResult = { valid: true, invalidMessages: [], }; this.list.every((item: ValidationObject) => { const validationItemResult = item.validate(value); if (validationItemResult.valid == false) { validationResult.valid = false; validationResult.invalidMessages = validationResult.invalidMessages.concat( validationItemResult.invalidMessages ); } return true; }); return validationResult; }, }; }; /** * Required */ interface ValidationRequiredOptions { invalidMessage?: string | ((options: ValidationRequiredOptions) => string); } interface ValidationRequiredObject extends ValidationRequiredOptions { validate: (value: string) => ValidationResult; } const defaultValidationRequiredOptions: ValidationRequiredOptions = { invalidMessage: "This field is required.", }; /** * Validate field contains value * * @param options options data * @returns ValidationRequiredObject */ export function Required( options?: ValidationRequiredOptions ): ValidationRequiredObject { options = { ...defaultValidationRequiredOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: value.length > 0, invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * Url */ interface ValidationUrlOptions { invalidMessage?: string | ((options: ValidationUrlOptions) => string); } interface ValidationUrlObject extends ValidationUrlOptions { validate: (value: string) => ValidationResult; } const defaultValidationUrlOptions: ValidationUrlOptions = { invalidMessage: "Not a supported Url format.", }; /** * Validate field is in a valid Email format * * @param options options data * @returns ValidationEmailObject */ export function Url(options?: ValidationUrlOptions): ValidationUrlObject { options = { ...defaultValidationUrlOptions, ...(options || {}) }; return { ...options, validate: function (value: string): ValidationResult { return { valid: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*(:\d+)?([/?#][^\s]*)?$/.test( value ), invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; } /** * FileSize */ interface ValidationFileSizeOptions { size: number; invalidMessage?: string | ((options: ValidationFileSizeOptions) => string); } interface ValidationFileSizeObject extends ValidationFileSizeOptions { validate: (value: File) => ValidationResult; } const defaultValidationFileSizeOptions: ValidationFileSizeOptions = { size: 1024 * 1024 * 1024, // 1 Mb invalidMessage: (options) => { return `The file size must be less than ${bytesReadable(options.size)}`; }, }; /** * Validate field is in a valid Email format * * @param options options data * @returns ValidationEmailObject */ export function FileSize( options?: ValidationFileSizeOptions ): ValidationFileSizeObject { options = { ...defaultValidationFileSizeOptions, ...(options || {}) }; return { ...options, validate: function (value: File): ValidationResult { return { valid: value.size < options.size, invalidMessages: [ typeof this.invalidMessage === "string" ? this.invalidMessage : this.invalidMessage(this), ], }; }, }; }