mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
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:
@@ -48,8 +48,10 @@ export interface ContractListFilterOptions {
|
||||
hasClearanceDocuments?: boolean;
|
||||
createdFrom?: string;
|
||||
createdTo?: string;
|
||||
originYardId?: string;
|
||||
destinationYardId?: string;
|
||||
/** Any of these origin yards (OR). ANDed with `destinationYardId`. */
|
||||
originYardId?: string[];
|
||||
/** Any of these destination yards (OR). ANDed with `originYardId`. */
|
||||
destinationYardId?: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -494,20 +496,25 @@ export class ContractsRepository extends BaseRepository<Contract> {
|
||||
// Routes are one-to-many (a contract can list several lanes), so origin
|
||||
// and destination each need their own EXISTS — a plain join would
|
||||
// duplicate the contract row per matching route.
|
||||
if (omit !== 'originYardId' && options.originYardId) {
|
||||
// Each end is an OR-list, the two ends AND together. Note this still means
|
||||
// "has a route from one of these origins" AND "has a route to one of these
|
||||
// destinations" — not necessarily the SAME route, which is what the two
|
||||
// separate EXISTS have always meant and what the filter bar's two
|
||||
// independent pickers describe.
|
||||
if (omit !== 'originYardId' && options.originYardId?.length) {
|
||||
qb.andWhere(
|
||||
'EXISTS (SELECT 1 FROM freight.contract_routes cr_o ' +
|
||||
'WHERE cr_o.contract_id = contract.id AND cr_o.deleted_at IS NULL ' +
|
||||
'AND cr_o.origin_yard_id = :originYardId)',
|
||||
{ originYardId: options.originYardId },
|
||||
'AND cr_o.origin_yard_id IN (:...originYardIds))',
|
||||
{ originYardIds: options.originYardId },
|
||||
);
|
||||
}
|
||||
if (omit !== 'destinationYardId' && options.destinationYardId) {
|
||||
if (omit !== 'destinationYardId' && options.destinationYardId?.length) {
|
||||
qb.andWhere(
|
||||
'EXISTS (SELECT 1 FROM freight.contract_routes cr_d ' +
|
||||
'WHERE cr_d.contract_id = contract.id AND cr_d.deleted_at IS NULL ' +
|
||||
'AND cr_d.destination_yard_id = :destinationYardId)',
|
||||
{ destinationYardId: options.destinationYardId },
|
||||
'AND cr_d.destination_yard_id IN (:...destinationYardIds))',
|
||||
{ destinationYardIds: options.destinationYardId },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user