release: WRNexusJS 0.4.0
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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 Record<string, unknown> = Record<string, unknown>> {
|
||||
readonly #refinements: AsyncRefinement<T>[] = [];
|
||||
constructor(readonly base: ObjectSchema) {}
|
||||
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 Record<string, unknown> = Record<string, unknown>>(
|
||||
schema: ObjectSchema,
|
||||
): AsyncObjectSchema<T> {
|
||||
return new AsyncObjectSchema<T>(schema);
|
||||
}
|
||||
|
||||
export async function parseBodyAsync<T extends Record<string, unknown>>(
|
||||
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)),
|
||||
};
|
||||
}
|
||||
@@ -390,3 +390,11 @@ async function readBody(req: Request): Promise<Record<string, unknown>> {
|
||||
}
|
||||
|
||||
export { renderSchemasScript, VALIDATE_RUNTIME } from "./runtime.ts";
|
||||
export {
|
||||
AsyncObjectSchema,
|
||||
asyncSchema,
|
||||
parseBodyAsync,
|
||||
schemaToOpenApi,
|
||||
mergeValidationResults,
|
||||
} from "./advanced.ts";
|
||||
export type { AsyncValidationContext, AsyncRefinement, OpenApiSchema } from "./advanced.ts";
|
||||
|
||||
Reference in New Issue
Block a user