import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common"; import { z, type ZodTypeAny } from "zod"; import { ApiError } from "../errors.js"; /** * Zod validation pipe. * * @Body(ZodValidate(schema)) body * @Query(ZodValidate(querySchema)) query * @Params(ZodValidate(paramsSchema)) params * * Parses the raw input against the schema (with `.parse`) and returns the * transformed/validated object; failures throw VALIDATION_ERROR with * field-level details — handled by the global filter into the canonical * {error:{code:"VALIDATION_ERROR",details:[...]}} envelope. * * NOTE: constructed manually by the `ZodValidate` factory below — it is * `@Injectable()`-marked but NEVER provided to Nest DI, so its ctor takes * no `@Inject` (the schema is a value, not a DI token; TASK-0100 S0). */ @Injectable() // Declared as `Promise` because `transform` is `async` (it returns a // promise). NestJS awaits pipe results at runtime (pipes-consumer), so the // downstream handler still receives the resolved `T` — type-accurate, no // behavioral change. export class ZodValidationPipe implements PipeTransform> { constructor(private readonly schema: ZodTypeAny) {} async transform(value: unknown, _metadata: ArgumentMetadata): Promise { const result = this.schema.safeParse(value); if (!result.success) { const details = result.error.issues.slice(0, 20).map((i) => ({ field: i.path.join("."), issue: i.message })); throw ApiError.validation("Request validation failed.", details); } return result.data as T; } } /** Convenience factory: `ZodValidate(schema)`. */ export function ZodValidate(schema: ZodTypeAny): ZodValidationPipe { return new ZodValidationPipe(schema); } /** * Validate a plain query object (e.g. the result of parsePagination inputs) * against a schema — used by services for non-route validation. */ export function parseOrThrow(schema: z.ZodType, value: unknown): T { const r = schema.safeParse(value); if (!r.success) { const details = r.error.issues.slice(0, 20).map((i) => ({ field: i.path.join("."), issue: i.message })); throw ApiError.validation("Request validation failed.", details); } return r.data; }