import { Injectable, type PipeTransform } from '@nestjs/common'; import type { ZodType } from 'zod'; import type { ApiFieldErrors } from '@sport/types'; import { AppException } from '../errors/app.exception'; /** * Validates a request payload with a Zod schema from @sport/validation and * returns the *parsed* value (with coercions and defaults applied). * * Chosen over class-validator so that exactly one schema governs both the API * and the frontend forms. See ADR-0006. * * @Post() * create(@Body(new ZodValidationPipe(createProductSchema)) body: CreateProductInput) {} */ @Injectable() export class ZodValidationPipe implements PipeTransform { constructor(private readonly schema: TSchema) {} transform(value: unknown): unknown { const result = this.schema.safeParse(value); if (!result.success) { throw AppException.validation(toFieldErrors(result.error.issues)); } return result.data; } } interface ZodIssueLike { path: PropertyKey[]; message: string; } /** `items.0.quantity` → ["Must be at least 1"] — directly consumable by forms. */ function toFieldErrors(issues: readonly ZodIssueLike[]): ApiFieldErrors { const fields: Record = {}; for (const issue of issues) { const key = issue.path.map(String).join('.') || '_'; (fields[key] ??= []).push(issue.message); } return fields; }