model-validator-ts
Version:
[](https://www.npmjs.com/package/model-validator-ts)
223 lines • 6.8 kB
JavaScript
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