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)

223 lines 6.8 kB
function invariant(condition, message) { if (!condition) { throw new Error(message); } } export class ErrorBag { #issues = []; #global; addGlobalError(message) { this.#global = message; return this; } addError(key, message) { this.#issues.push({ key, message }); return this; } get global() { return this.#global; } firstError(key) { return this.#issues.find((issue) => issue.key === key)?.message; } hasErrors() { return this.#issues.length > 0 || this.#global !== undefined; } toObject() { const issuesObj = {}; for (const issue of this.#issues) { if (!issuesObj[issue.key]) { issuesObj[issue.key] = []; } issuesObj[issue.key].push(issue.message); } return { global: this.#global, issues: issuesObj, }; } } async function validate(args) { const override = args.opts?.override; if (override && typeof args.input === "object" && args.input !== null) { Object.assign(args.input, override); } let presult = args.schema["~standard"].validate(args.input); if (presult instanceof Promise) { presult = await presult; } const bag = 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)}`); } bag.addError(path, issue.message); } return { success: false, errors: bag, rule: undefined, }; } // Now time to evaluate the rules let context = {}; for (const rule of args.rules) { const result = await rule.fn({ data: args.input, bag, deps: args.deps, context, }); if (bag.hasErrors()) { return { success: false, errors: bag, rule: { id: rule.id, description: rule.description }, }; } if (result && typeof result === "object") { if (result !== undefined && result !== null && "context" in result && typeof result.context === "object" && result.context !== null) { context = { ...context, ...result.context }; } } } return { success: true, context, value: presult.value, }; } export class Command { #validatorBuilder; #execute; constructor(validatorBuilder, execute) { this.#validatorBuilder = validatorBuilder; this.#execute = execute; } provide(deps) { const newBuilder = this.#validatorBuilder.provide(deps); return new Command(newBuilder, this.#execute); } run = (async (input, opts) => { const internals = this.#validatorBuilder["~unsafeInternals"]; invariant(internals.depsStatus !== "required", "Deps should be provided before calling run"); invariant(internals.schema, "Schema must be defined before calling command"); const validation = await this.#validatorBuilder.validate(input, opts); if (!validation.success) { return { success: false, errors: validation.errors, step: "validation", rule: validation.rule, }; } // Create a new error bag for the command execution const executionBag = new ErrorBag(); const executeResult = await this.#execute({ data: validation.value, deps: internals.deps, context: validation.context, bag: executionBag, }); // Check if errors were added to the bag during execution if (executionBag.hasErrors()) { return { success: false, errors: executionBag, step: "execution", rule: undefined, }; } // Check if the execute function returned an ErrorBag if (executeResult instanceof ErrorBag) { return { success: false, errors: executeResult, step: "execution", rule: undefined, }; } return { success: true, result: executeResult, context: validation.context, }; }); runShape = ((input, opts) => { const internals = this.#validatorBuilder["~unsafeInternals"]; invariant(internals.depsStatus !== "required", "Deps should be provided before calling runShape"); return this.run(input, opts); }); } export class FluentValidatorBuilder { #state; constructor(state) { this.#state = state || { contextRules: [], schema: undefined, deps: undefined, depsStatus: "not-required", }; } #setState(updates) { // Update the state object Object.assign(this.#state, updates); // Return this instance but cast to the new type return this; } input(schema) { return this.#setState({ schema }); } $deps() { return this.#setState({ depsStatus: "required", }); } get ["~unsafeInternals"]() { return this.#state; } validate = ((input, opts) => { invariant(this.#state.depsStatus !== "required", "Deps should be provided before calling validate"); invariant(this.#state.schema, "Schema must be defined before calling validate"); return validate({ schema: this.#state.schema, input, rules: this.#state.contextRules, opts, deps: this.#state.deps ?? {}, }); }); rule(rule) { return this.#setState({ contextRules: [...this.#state.contextRules, rule], }); } provide(deps) { return this.#setState({ deps, depsStatus: "passed", }); } command(args) { return new Command(this, args.execute); } } export function buildValidator() { return new FluentValidatorBuilder(); } //# sourceMappingURL=index.js.map