Files
The-transit-Project/transit_api/repo/lines.mock.js
T
Sven laptop 7d55580d87 Refactor transit_api into a repo/routes module split
Split the 5,441-line index.js into an 86-line bootstrap, thin domain
route modules, shared lib/ helpers, and a repo/ layer that isolates the
DEV_MOCK branches (repo/*.mock.js). Repo methods return {statusCode, body};
routes are one-line wrappers. Pin search_path to "transit.bus" and
document the /bus/ prefix with legacy root aliases (Deprecation header).

Parity verified: 57/57 byte-identical on the stub differential harness
plus 24/24 live read-only responses vs the original against real
PostgreSQL. The only intentional behavior change: mock reorder/delete
bus-line endpoints no longer require a live DB (previously 500 in
DEV_MOCK=1 without PG). DEV_MOCK gate and node --check clean.
2026-09-24 20:18:32 +02:00

308 lines
10 KiB
JavaScript

const { store } = require("../mock/store");
const { reportError } = require("../lib/report-error");
// Bus-line repository methods (DEV_MOCK mode). Each mirrors the original
// handler's `if (DEV_MOCK) { ... }` branch verbatim and returns
// { statusCode, body }. The original reorder/delete handlers acquired a
// db.pool.connect() before the mock branch; the mock implementation below
// intentionally skips that so mock-only deletion/reorder work without a
// database (documented improvement).
async function listLineStops(req) {
try {
const { busLineId } = req.params;
const stops = store.routeStops
.filter((rs) => String(rs.busLineId) === String(busLineId))
.sort((a, b) => a.stopOrder - b.stopOrder)
.map((rs) => ({
id: rs.id,
order: rs.stopOrder,
name: rs.name,
latitude: rs.latitude,
longitude: rs.longitude,
estimatedArrivalMinutes: rs.estimatedArrivalMinutes,
}));
return { statusCode: 200, body: { stops } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function listLines(req) {
try {
return {
statusCode: 200,
body: {
busLines: store.busLines
.slice()
.sort((a, b) => a.lineNumber - b.lineNumber),
},
};
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function createLine(req) {
try {
const {
lineNumber,
startStop,
endStop,
estimatedDurationMinutes,
description,
} = req.body;
if (
lineNumber === undefined ||
!startStop ||
!endStop ||
estimatedDurationMinutes === undefined
) {
return { statusCode: 400, body: { error: "missing_required_fields" } };
}
const busLine = {
id: store.nextBusLineId++,
lineNumber: Number(lineNumber),
startStop,
endStop,
estimatedDurationMinutes: Number(estimatedDurationMinutes),
description: description || "",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
store.busLines.push(busLine);
return { statusCode: 201, body: { busLine } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function updateLine(req) {
try {
const { busLineId } = req.params;
const {
lineNumber,
startStop,
endStop,
estimatedDurationMinutes,
description,
} = req.body;
const existing = store.busLines.find(
(line) => String(line.id) === String(busLineId),
);
if (!existing)
return { statusCode: 404, body: { error: "bus_line_not_found" } };
if (lineNumber !== undefined) existing.lineNumber = Number(lineNumber);
if (startStop !== undefined) existing.startStop = startStop;
if (endStop !== undefined) existing.endStop = endStop;
if (estimatedDurationMinutes !== undefined)
existing.estimatedDurationMinutes = Number(estimatedDurationMinutes);
if (description !== undefined) existing.description = description;
existing.updatedAt = new Date().toISOString();
return { statusCode: 200, body: { busLine: existing } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function listLineStopsDetailed(req) {
try {
const { busLineId } = req.params;
if (!busLineId || !Number.isFinite(Number(busLineId))) {
return { statusCode: 400, body: { error: "invalid_bus_line_id" } };
}
const stops = store.routeStops
.filter((rs) => String(rs.busLineId) === String(busLineId))
.sort((a, b) => a.stopOrder - b.stopOrder)
.map((rs) => ({
id: rs.id,
busLineId: rs.busLineId,
stopId: rs.stopId,
stopOrder: rs.stopOrder,
estimatedArrivalMinutes: rs.estimatedArrivalMinutes,
name: rs.name,
latitude: rs.latitude,
longitude: rs.longitude,
}));
return { statusCode: 200, body: { routeStops: stops } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function addLineStop(req) {
try {
const { busLineId } = req.params;
const { stopId, stopOrder, estimatedArrivalMinutes } = req.body;
if (!stopId || stopOrder === undefined) {
return { statusCode: 400, body: { error: "missing_required_fields" } };
}
const stop = store.stops.find((s) => String(s.id) === String(stopId));
const routeStop = {
id: store.nextRouteStopId++,
busLineId: Number(busLineId),
stopId: Number(stopId),
stopOrder: Number(stopOrder),
estimatedArrivalMinutes: estimatedArrivalMinutes ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
name: stop ? stop.name : `stop-${stopId}`,
latitude: stop ? stop.latitude : 0,
longitude: stop ? stop.longitude : 0,
};
// remove existing with same busLineId & stopOrder
store.routeStops = store.routeStops.filter(
(rs) =>
!(
rs.busLineId === routeStop.busLineId &&
rs.stopOrder === routeStop.stopOrder
),
);
store.routeStops.push(routeStop);
return { statusCode: 201, body: { routeStop } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function reorderLineStops(req) {
try {
const { busLineId } = req.params;
const { routeStopIds } = req.body || {};
if (!Array.isArray(routeStopIds) || routeStopIds.length === 0) {
return { statusCode: 400, body: { error: "routeStopIds array required" } };
}
const normalizedIds = routeStopIds
.map((id) => Number(id))
.filter((id) => Number.isFinite(id));
if (normalizedIds.length !== routeStopIds.length) {
return { statusCode: 400, body: { error: "routeStopIds must contain only numeric ids" } };
}
const busLineStops = store.routeStops
.filter((rs) => String(rs.busLineId) === String(busLineId))
.sort((a, b) => a.stopOrder - b.stopOrder);
const idSet = new Set(busLineStops.map((rs) => Number(rs.id)));
const sameCount = busLineStops.length === normalizedIds.length;
const sameIds = sameCount && normalizedIds.every((id) => idSet.has(id));
if (!sameIds) {
return { statusCode: 400, body: { error: "routeStopIds must match current bus line stops" } };
}
const updated = normalizedIds.map((routeStopId, index) => {
const stop = store.routeStops.find(
(rs) =>
Number(rs.id) === routeStopId &&
String(rs.busLineId) === String(busLineId),
);
if (!stop) return null;
stop.stopOrder = index + 1;
stop.updatedAt = new Date().toISOString();
return stop;
});
if (updated.some((item) => !item)) {
return { statusCode: 400, body: { error: "routeStopIds must match current bus line stops" } };
}
store.routeStops = store.routeStops.map((routeStop) => {
const reordered = updated.find((item) => item.id === routeStop.id);
return reordered || routeStop;
});
return { statusCode: 200, body: { routeStops: updated } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function deleteLine(req) {
try {
const { busLineId } = req.params;
if (!busLineId || !Number.isFinite(Number(busLineId))) {
return { statusCode: 400, body: { error: "invalid_bus_line_id" } };
}
const lineIndex = store.busLines.findIndex(
(bl) => Number(bl.id) === Number(busLineId),
);
if (lineIndex === -1) {
return { statusCode: 404, body: { error: "bus_line_not_found" } };
}
const deletedLine = store.busLines.splice(lineIndex, 1)[0];
// Also delete route_stops for this line
store.routeStops = store.routeStops.filter(
(rs) => String(rs.busLineId) !== String(busLineId),
);
return { statusCode: 200, body: { success: true, busLine: deletedLine } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function deleteLineStop(req) {
try {
const { busLineId, routeStopId } = req.params;
if (!busLineId || !Number.isFinite(Number(busLineId))) {
return { statusCode: 400, body: { error: "invalid_bus_line_id" } };
}
if (!routeStopId || !Number.isFinite(Number(routeStopId))) {
return { statusCode: 400, body: { error: "invalid_route_stop_id" } };
}
const index = store.routeStops.findIndex(
(rs) =>
Number(rs.id) === Number(routeStopId) &&
String(rs.busLineId) === String(busLineId),
);
if (index === -1) {
return { statusCode: 404, body: { error: "route_stop_not_found" } };
}
const deletedRouteStop = store.routeStops.splice(index, 1)[0];
return { statusCode: 200, body: { success: true, routeStop: deletedRouteStop } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
module.exports = {
listLineStops,
listLines,
createLine,
updateLine,
listLineStopsDetailed,
addLineStop,
reorderLineStops,
deleteLine,
deleteLineStop,
};