UNPKG

model-validator-ts

Version:

[![npm version](https://img.shields.io/npm/v/model-validator-ts.svg)](https://www.npmjs.com/package/model-validator-ts)

254 lines 7.68 kB
function invariant(condition, message) { if (!condition) { throw new Error(message); } } export class ErrorBag { #issues = []; addError(key, message) { this.#issues.push({ key, message }); } firstError(key) { return this.#issues.find(issue => issue.key === key)?.message; } hasErrors() { return this.#issues.length > 0; } toFlattenObject() { const result = {}; for (const issue of this.#issues) { result[issue.key] = issue.message; } return result; } get toObject() { const result = {}; for (const issue of this.#issues) { if (!result[issue.key]) { result[issue.key] = []; } result[issue.key].push(issue.message); } return result; } toString() { return JSON.stringify(this.toObject); } toHtml() { let result = ""; if (!this.hasErrors()) { return result; } result += "<ul>"; for (const issue of this.#issues) { let strKey; if (typeof issue.key === "symbol" || typeof issue.key === "number") { strKey = issue.key.toString(); } else { strKey = issue.key; } result += `<li>${strKey}: ${issue.message}</li>`; } result += "</ul>"; return result; } toText() { if (!this.hasErrors()) { return ""; } const result = []; let currentKey = null; for (const issue of this.#issues) { if (currentKey !== issue.key) { currentKey = issue.key; result.push(currentKey); } result.push(`- ${issue.message}`); } return result.join("\n"); } } // Separate class for validator definition export class ValidatorDefinition { schema; rules = []; ['~deps']; ['~preBuildDeps']; constructor(args) { this.schema = args.schema; this.rules = args.rules ?? []; this['~deps'] = args.deps ?? {}; this['~preBuildDeps'] = args.preBuildDeps ?? {}; } addRule(ruledef) { this.rules.push(ruledef); return this; } addRules(ruledefs) { this.rules.push(...ruledefs); return this; } build(...dependencies) { const [deps] = dependencies; return new ValidatorModel(this.schema, this.rules, { ...this['~preBuildDeps'], ...(deps ?? {}), }); } } // Update ValidationBuilder to accept rules and dependencies export class ValidatorModel { schema; #rules; #deps; errors = new ErrorBag(); constructor(schema, rules, deps) { this.schema = schema; this.#rules = rules; this.#deps = deps; } addError(key, message) { this.errors.addError(key, message); return this.errors; } mergeErrors(errors) { for (const [key, message] of Object.entries(errors)) { if (!message) { continue; } if (typeof key !== "string") { throw new Error("Invalid error key, expected string"); } if (typeof message !== "string") { throw new Error("Invalid error message, expected string"); } this.addError(key, message); } return this.errors; } mapErrors(args) { for (const [key, message] of Object.entries(args.src)) { let targetKey = null; if ("mappings" in args) { // @ts-ignore targetKey = args.mappings[key] ?? null; } targetKey = targetKey ?? key; this.addError(targetKey, message); } return this.errors; } /* * Method to validate does the same as validate but because the input is already typed * we can use it to validate the shape of the input at compile time * @param input - the input to validate */ async validateShape(input, opts) { return this.validate(input, opts); } /* * Method to validate the input against the schema and business rules * @param input - the input to validate * @return a ValidationResult object */ async validate(input, opts) { const override = opts?.override; if (override && typeof input === "object" && input !== null) { Object.assign(input, override); } let presult = this.schema['~standard'].validate(input); if (presult instanceof Promise) { presult = await presult; } this.errors = new ErrorBag(); if (presult.issues) { for (const issue of presult.issues) { if (typeof issue.message !== "string") { throw new Error("Unexpected error format, expected string"); } let path; if (Array.isArray(issue.path)) { path = issue.path.join("."); } else if (typeof issue.path === "string") { path = issue.path; } else { throw new Error(`Unsupported issue path type ${typeof issue.path}: issue: ${JSON.stringify(issue)}`); } this.addError(path, issue.message); } return { success: false, errors: this.errors, }; } for (const ruleDef of this.#rules) { const params = { property: ruleDef.attribute, data: presult.value, builder: this, deps: this.#deps, }; if (ruleDef.fn.constructor.name === "AsyncFunction") { await ruleDef.fn(params); } else { ruleDef.fn(params); } } if (this.errors.hasErrors()) { return { success: false, errors: this.errors, }; } else { return { success: true, value: presult.value, }; } } toClientFields() { throw new Error("Not implemented"); } } export function createCommand(args) { const validator = args.validator.build(args.deps); return { runShape(input, opts) { return this.run(input, opts); }, async run(input, opts) { const validation = await validator.validate(input, opts); if (!validation.success) { return { validated: false, errors: validation.errors }; } const result = await args.execute({ data: validation.value, deps: args.deps, builder: validator, }); if (result instanceof ErrorBag) { return { validated: false, errors: result }; } return { validated: true, result: result }; }, execute: args.execute, validator, }; } export function createValidatorBuilder(config) { return function createValidator(args) { const outerDeps = typeof config.deps === "function" ? config.deps() : config.deps ?? {}; return new ValidatorDefinition({ schema: args.schema, rules: args.rules ?? [], deps: args.deps, preBuildDeps: outerDeps, }); }; } //# sourceMappingURL=index.js.map