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

@@ -4570,8 +4570,15 @@ export class TrainSchedulingService {
const base: FindOptionsWhere<TrainSchedule> = {};
if (allowedDirections) base.direction = In(allowedDirections) as never;
if (query.status) base.status = query.status;
if (query.originStationId) base.originStationId = query.originStationId;
if (query.destinationStationId) base.destinationStationId = query.destinationStationId;
// Each end is an OR-list, the two ends AND together (origin-only and
// destination-only are both valid queries). `?.length` guards the empty
// array — `In([])` compiles to `IN ()`, a syntax error.
if (query.originStationId?.length) {
base.originStationId = In(query.originStationId) as never;
}
if (query.destinationStationId?.length) {
base.destinationStationId = In(query.destinationStationId) as never;
}
if (query.freightType) base.id = this.scheduleFreightTypeFilter(query.freightType) as never;
// Search fans out across every human-recognizable label; each OR variant