feat(server): adopt zod DTOs for the day-note body contracts

This commit is contained in:
jubnl
2026-07-26 17:44:25 +02:00
parent 4e49e9c21e
commit 3762d3306c
6 changed files with 46 additions and 44 deletions
@@ -76,8 +76,6 @@ export const BODY_CONTRACT_ALLOW_LIST: string[] = [
'CollectionsController.reorder',
'DayAssignmentsController.create',
'DayAssignmentsController.reorder',
'DayNotesController.create',
'DayNotesController.update',
'DaysController.create',
'DaysController.reorder',
'DaysController.transport',
+9 -25
View File
@@ -12,33 +12,19 @@ import {
} from '@nestjs/common';
import type { User } from '../../types';
import { DayNotesService } from './day-notes.service';
import { DayNoteCreateDto, DayNoteUpdateDto } from './day-notes.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
type DayNoteBody = { text?: string; time?: string; icon?: string; sort_order?: number };
// Runs BEFORE the trip-access check, so an over-long field 400s first. The `time`
// cap matches the shared dayNote schema (max 250) and the note dialog's counter;
// it was 150 here, which rejected valid 151–250 char notes with a confusing error.
const MAX_LENGTHS: Record<string, number> = { text: 500, time: 250 };
function validateLengths(body: Record<string, unknown>): void {
for (const [field, max] of Object.entries(MAX_LENGTHS)) {
const value = body[field];
if (value && typeof value === 'string' && value.length > max) {
throw new HttpException({ error: `${field} must be ${max} characters or less` }, 400);
}
}
}
/**
* /api/trips/:tripId/days/:dayId/notes — free-text annotations on a day.
*
* Byte-identical to the legacy Express route (server/src/routes/dayNotes.ts):
* the string-length guard runs first (400), then trip access (404), then the
* 'day_edit' permission (403); create 201 / rest 200; the bespoke "Day not
* found" / "Note not found" / "Text required" bodies; WebSocket broadcasts with
* the forwarded X-Socket-Id.
* Bodies validate against the @trek/shared day-note contracts via the global
* ZodValidationPipe (which, like the legacy string-length guard, 400s before
* the trip-access check), then trip access (404), then the 'day_edit'
* permission (403); create 201 / rest 200; the bespoke "Day not found" /
* "Note not found" / "Text required" bodies; WebSocket broadcasts with the
* forwarded X-Socket-Id.
*/
@Controller('api/trips/:tripId/days/:dayId/notes')
@UseGuards(JwtAuthGuard)
@@ -70,10 +56,9 @@ export class DayNotesController {
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Param('dayId') dayId: string,
@Body() body: DayNoteBody,
@Body() body: DayNoteCreateDto,
@Headers('x-socket-id') socketId?: string,
) {
validateLengths(body);
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
if (!this.notes.dayExists(dayId, tripId)) {
@@ -93,10 +78,9 @@ export class DayNotesController {
@Param('tripId') tripId: string,
@Param('dayId') dayId: string,
@Param('id') id: string,
@Body() body: DayNoteBody,
@Body() body: DayNoteUpdateDto,
@Headers('x-socket-id') socketId?: string,
) {
validateLengths(body);
const trip = this.requireTrip(tripId, user);
this.requireEdit(trip, user);
const current = this.notes.getNote(id, dayId, tripId);
+11
View File
@@ -0,0 +1,11 @@
import { createZodDto } from 'nestjs-zod';
import { dayNoteCreateRequestSchema, dayNoteUpdateRequestSchema } from '@trek/shared';
/**
* Server-side createZodDto wrappers over the @trek/shared day-note contracts.
* The global ZodValidationPipe (APP_PIPE in app.module.ts) validates any
* @Body() parameter typed with one of these classes by metatype — the Zod
* schemas in shared/ remain the single source of truth for the wire contract.
*/
export class DayNoteCreateDto extends createZodDto(dayNoteCreateRequestSchema) {}
export class DayNoteUpdateDto extends createZodDto(dayNoteUpdateRequestSchema) {}