feat(api): accept several yards on each end of a route filter

Bookings, contracts and train schedules all validated originYardId /
destinationYardId (originStationId / destinationStationId) as a single
@IsUUID and matched with `=`, so a list could be narrowed to exactly one
lane. The filter bar can now ask for several stations per end, and each
end independently, which needs the same on the server.

@IdListParam() is the shared transform: one id, a comma-separated list,
or a repeated query param, always landing as a string[]. It yields
undefined rather than [] when nothing usable is left — a repository that
branches on `?.length` can then never hand TypeORM an empty array, which
compiles to the syntax error IN (). It stays backwards compatible with
the single-value form, so existing deep links and saved views are
unaffected.

Matching moves to IN (:...ids) — for contracts inside the two existing
EXISTS subqueries, which keeps meaning "has a route from one of these
origins" AND "has a route to one of these destinations", not necessarily
the same route. All three statements were EXPLAIN-validated against
edr_dev.
This commit is contained in:
Nathnael
2026-08-20 11:24:46 +00:00
parent 684c3173f4
commit 7446adaa88
8 changed files with 165 additions and 40 deletions

View File

@@ -9,6 +9,7 @@ import {
TRADE_DIRECTIONS,
} from './create-booking.dto';
import { PAYMENT_STATUSES } from '../entities/booking.entity';
import { IdListParam } from '../../../common/dto/id-list.transform';
export class FilterBookingDto {
@ApiPropertyOptional({ enum: BOOKING_STATUSES })
@@ -96,15 +97,23 @@ export class FilterBookingDto {
@IsDateString()
scheduledTo?: string;
@ApiPropertyOptional({ format: 'uuid', description: 'Filter by origin yard' })
@ApiPropertyOptional({
description:
'Filter by origin yard — one id or a comma-separated list; a booking matches if it leaves ANY of them.',
})
@IsOptional()
@IsUUID()
originYardId?: string;
@IdListParam()
@IsUUID(undefined, { each: true })
originYardId?: string[];
@ApiPropertyOptional({ format: 'uuid', description: 'Filter by destination yard' })
@ApiPropertyOptional({
description:
'Filter by destination yard — one id or a comma-separated list; a booking matches if it arrives at ANY of them. Combined with originYardId by AND.',
})
@IsOptional()
@IsUUID()
destinationYardId?: string;
@IdListParam()
@IsUUID(undefined, { each: true })
destinationYardId?: string[];
@ApiPropertyOptional({ enum: ['true', 'false'], description: 'Filter government vs private bookings' })
@IsOptional()