49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
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<TSchema extends ZodType> 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<string, string[]> = {};
|
|
|
|
for (const issue of issues) {
|
|
const key = issue.path.map(String).join('.') || '_';
|
|
(fields[key] ??= []).push(issue.message);
|
|
}
|
|
|
|
return fields;
|
|
}
|