Files
WRNexusJS/packages/validation/src/advanced.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

103 lines
4.0 KiB
TypeScript

import { invalid, type ObjectSchema, type ParseResult, type SchemaDescriptor } from "./index.ts";
export interface AsyncValidationContext<T> {
value: T;
addIssue(field: keyof T | string, message: string): void;
signal?: AbortSignal;
}
export type AsyncRefinement<T> = (context: AsyncValidationContext<T>) => void | Promise<void>;
export class AsyncObjectSchema<T extends object = Record<string, unknown>> {
readonly #refinements: AsyncRefinement<T>[] = [];
constructor(readonly base: ObjectSchema<T>) {}
refine(refinement: AsyncRefinement<T>): this {
this.#refinements.push(refinement);
return this;
}
describe(): SchemaDescriptor {
return this.base.describe();
}
async parse(input: unknown, signal?: AbortSignal): Promise<ParseResult<T>> {
const result = this.base.parse(input) as ParseResult<T>;
if (!result.ok) return result;
const errors: Record<string, string> = {};
const context: AsyncValidationContext<T> = {
value: result.value,
signal,
addIssue(field, message) {
if (!(String(field) in errors)) errors[String(field)] = message;
},
};
for (const refinement of this.#refinements) {
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
await refinement(context);
}
return { ok: Object.keys(errors).length === 0, value: result.value, errors };
}
}
export function asyncSchema<T extends object>(schema: ObjectSchema<T>): AsyncObjectSchema<T> {
return new AsyncObjectSchema<T>(schema);
}
export async function parseBodyAsync<T extends object>(
schema: AsyncObjectSchema<T>,
request: Request,
signal?: AbortSignal,
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
let body: Record<string, unknown> = {};
const contentType = request.headers.get("content-type") ?? "";
try {
if (contentType.includes("json")) body = (await request.json()) as Record<string, unknown>;
else {
const data = await request.formData();
for (const [key, value] of data) body[key] = value;
}
} catch {
/* handled by schema */
}
const result = await schema.parse(body, signal);
return result.ok
? { ok: true, value: result.value }
: { ok: false, response: invalid(result.errors) };
}
export interface OpenApiSchema {
type: "object";
properties: Record<string, Record<string, unknown>>;
required?: string[];
}
export function schemaToOpenApi(schema: ObjectSchema | AsyncObjectSchema): OpenApiSchema {
const descriptor = schema.describe();
const required: string[] = [];
const properties: Record<string, Record<string, unknown>> = {};
for (const [name, field] of Object.entries(descriptor.fields)) {
const property: Record<string, unknown> = {
type: field.type === "boolean" ? "boolean" : field.type,
};
if (field.label) property.title = field.label;
for (const rule of field.rules) {
if (rule.kind === "min") property[field.type === "string" ? "minLength" : "minimum"] = rule.n;
else if (rule.kind === "max")
property[field.type === "string" ? "maxLength" : "maximum"] = rule.n;
else if (rule.kind === "length") property.minLength = property.maxLength = rule.n;
else if (["email", "url", "uuid", "date"].includes(rule.kind))
property.format = rule.kind === "url" ? "uri" : rule.kind;
else if (rule.kind === "oneOf") property.enum = rule.values;
else if (rule.kind === "pattern") property.pattern = rule.source;
else if (rule.kind === "integer") property.type = "integer";
}
properties[name] = property;
if (!field.optional) required.push(name);
}
return { type: "object", properties, ...(required.length ? { required } : {}) };
}
export function mergeValidationResults<T>(...results: ParseResult<T>[]): ParseResult<T> {
return {
ok: results.every((result) => result.ok),
value: Object.assign({}, ...results.map((result) => result.value)) as T,
errors: Object.assign({}, ...results.map((result) => result.errors)),
};
}