File size: 2,731 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | /**
* zod-backed wire validation. Every procedure in the contract validates its
* input tuple before it goes on the wire (caller bugs) and its output when it
* comes back (server drift). Event payloads are validated on delivery;
* failures there are reported, not thrown, so one bad event cannot kill a
* subscription stream.
*/
import { z } from 'zod';
import type { ProcedureContract, StreamingProcedureContract } from '#/contract/types';
export type ValidationPhase = 'input' | 'output' | 'event' | 'chunk';
export class KlientValidationError extends Error {
constructor(
readonly phase: ValidationPhase,
/** `service.method` for calls, the klient event name for events. */
readonly procedure: string,
readonly issues: z.ZodError['issues'],
/** The offending raw payload (input args, output data, or event data). */
readonly payload: unknown,
) {
super(
`${phase} validation failed for ${procedure}: ${issues
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
.join('; ')}`,
);
this.name = 'KlientValidationError';
}
}
/** Parse the positional-args tuple; returns the normalized args to send. */
export function parseInput(
procedure: string,
contract: ProcedureContract | StreamingProcedureContract,
args: unknown[],
): unknown[] {
const result = contract.input.safeParse(args);
if (!result.success) {
throw new KlientValidationError('input', procedure, result.error.issues, args);
}
return result.data as unknown[];
}
/** Parse a wire result; returns the normalized data to hand to the caller. */
export function parseOutput(
procedure: string,
contract: ProcedureContract,
data: unknown,
): unknown {
const result = contract.output.safeParse(data);
if (!result.success) {
throw new KlientValidationError('output', procedure, result.error.issues, data);
}
return result.data;
}
/** Validate one streamed chunk; throws on mismatch. */
export function parseChunk(
procedure: string,
contract: StreamingProcedureContract,
data: unknown,
): unknown {
const result = contract.chunk.safeParse(data);
if (!result.success) {
throw new KlientValidationError('chunk', procedure, result.error.issues, data);
}
return result.data;
}
/** Parse an event payload without throwing; `undefined` on failure. */
export function parseEvent(
event: string,
schema: z.ZodType,
data: unknown,
): { ok: true; data: unknown } | { ok: false; error: KlientValidationError } {
const result = schema.safeParse(data);
if (!result.success) {
return { ok: false, error: new KlientValidationError('event', event, result.error.issues, data) };
}
return { ok: true, data: result.data };
}
|