import {z, ZodType} from 'zod' import {InjectionAware} from '../di' import {ErrorWithContext, TypeTag} from '../util' import {ZodifyRecipient} from './ZodifyRecipient' import {ZodifyRegistrar} from './ZodifyRegistrar' import {Logging} from '../service/Logging' /** Type tag for a validated runtime type. */ export type Valid = TypeTag<'@extollo/lib:Valid'> & T export type ValidatorFactory = Validator | (() => Validator) /** * Error thrown if the schema for a validator cannot be located. */ export class InvalidSchemaMappingError extends Error { constructor(message = 'Unable to resolve schema for validator.') { super(message) } } /** * Interface defining validation error messages. */ export interface ValidationFormErrors { formErrors: string[], fieldErrors: { [k in keyof T]: string[] } } /** * Error thrown when data validation has failed. */ export class ValidationError extends ErrorWithContext { constructor( public readonly formErrors: ValidationFormErrors, message = 'Invalid form data', ) { super(message) } } /** * Validates input data against a schema at runtime. */ export class Validator extends InjectionAware implements ZodifyRecipient { public static fromSchema(type: ZodType): Validator { const inst = new Validator() inst.schema = type return inst } __exZodifiedSchemata: number[] = [] protected schema?: ZodType /** * Parse the input data against the schema. * @throws ValidationError * @param from */ public parse(from: unknown): Valid { try { return this.getZod().parse(from) as Valid } catch (e: unknown) { if ( e instanceof z.ZodError ) { throw new ValidationError(e.formErrors as ValidationFormErrors) } throw e } } /** * Typeguard for the input schema. * @param what */ public is(what: unknown): what is Valid { try { this.parse(what) return true } catch (e: unknown) { this.log().verbose(`Error during validation: ${e}`) if ( e instanceof ValidationError ) { return false } throw e } } /** Get the logging service. */ protected log(): Logging { return this.make(Logging) } /** Get the Zod schema. */ protected getZod(): z.ZodType { if ( this.schema ) { return this.schema } // eslint-disable-next-line no-underscore-dangle if ( this.__exZodifiedSchemata.length < 1 ) { throw new InvalidSchemaMappingError() } // eslint-disable-next-line no-underscore-dangle const id = this.__exZodifiedSchemata[0] const type = this.make(ZodifyRegistrar).get(id) if ( !type ) { throw new InvalidSchemaMappingError() } return type } }