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.
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
const db = require("../db");
|
||||
const { reportError } = require("../lib/report-error");
|
||||
|
||||
// Bus-line repository methods (PostgreSQL mode).
|
||||
// The client-based methods keep their original `db.pool.connect()` call before
|
||||
// the try block, preserving the exact original control flow (including the
|
||||
// connection/rollback/release dance) for byte-identical behavior.
|
||||
|
||||
async function listLineStops(req) {
|
||||
try {
|
||||
const { busLineId } = req.params;
|
||||
|
||||
const result = await db.query(
|
||||
`
|
||||
SELECT
|
||||
rs.id,
|
||||
rs.stop_order,
|
||||
s.name as stop_name,
|
||||
s.latitude,
|
||||
s.longitude,
|
||||
rs.estimated_arrival_minutes
|
||||
FROM route_stops rs
|
||||
JOIN stops s ON s.id = rs.stop_id
|
||||
WHERE rs.bus_line_id = $1
|
||||
ORDER BY rs.stop_order ASC
|
||||
`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
const stops = result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
order: row.stop_order,
|
||||
name: row.stop_name,
|
||||
latitude: parseFloat(row.latitude),
|
||||
longitude: parseFloat(row.longitude),
|
||||
estimatedArrivalMinutes: row.estimated_arrival_minutes,
|
||||
}));
|
||||
|
||||
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 {
|
||||
const result = await db.query(`
|
||||
SELECT id, line_number, start_stop, end_stop, estimated_duration_minutes, description
|
||||
FROM bus_lines
|
||||
ORDER BY line_number ASC, id ASC
|
||||
`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
busLines: result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
lineNumber: row.line_number,
|
||||
startStop: row.start_stop,
|
||||
endStop: row.end_stop,
|
||||
estimatedDurationMinutes: row.estimated_duration_minutes,
|
||||
description: row.description,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} 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 result = await db.query(
|
||||
`
|
||||
WITH next_id AS (
|
||||
SELECT COALESCE(MAX(id), 0) + 1 AS id FROM bus_lines
|
||||
)
|
||||
INSERT INTO bus_lines (id, line_number, start_stop, end_stop, estimated_duration_minutes, description)
|
||||
SELECT next_id.id, $1, $2, $3, $4, $5
|
||||
FROM next_id
|
||||
RETURNING id, line_number, start_stop, end_stop, estimated_duration_minutes, description
|
||||
`,
|
||||
[
|
||||
lineNumber,
|
||||
startStop,
|
||||
endStop,
|
||||
estimatedDurationMinutes,
|
||||
description || null,
|
||||
],
|
||||
);
|
||||
|
||||
const busLine = result.rows[0];
|
||||
return {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
busLine: {
|
||||
id: busLine.id,
|
||||
lineNumber: busLine.line_number,
|
||||
startStop: busLine.start_stop,
|
||||
endStop: busLine.end_stop,
|
||||
estimatedDurationMinutes: busLine.estimated_duration_minutes,
|
||||
description: busLine.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
} 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 result = await db.query(
|
||||
`
|
||||
UPDATE bus_lines
|
||||
SET
|
||||
line_number = COALESCE($1, line_number),
|
||||
start_stop = COALESCE($2, start_stop),
|
||||
end_stop = COALESCE($3, end_stop),
|
||||
estimated_duration_minutes = COALESCE($4, estimated_duration_minutes),
|
||||
description = COALESCE($5, description)
|
||||
WHERE id = $6
|
||||
RETURNING id, line_number, start_stop, end_stop, estimated_duration_minutes, description
|
||||
`,
|
||||
[
|
||||
lineNumber ?? null,
|
||||
startStop ?? null,
|
||||
endStop ?? null,
|
||||
estimatedDurationMinutes ?? null,
|
||||
description ?? null,
|
||||
busLineId,
|
||||
],
|
||||
);
|
||||
|
||||
if (!result.rows[0])
|
||||
return { statusCode: 404, body: { error: "bus_line_not_found" } };
|
||||
|
||||
const busLine = result.rows[0];
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
busLine: {
|
||||
id: busLine.id,
|
||||
lineNumber: busLine.line_number,
|
||||
startStop: busLine.start_stop,
|
||||
endStop: busLine.end_stop,
|
||||
estimatedDurationMinutes: busLine.estimated_duration_minutes,
|
||||
description: busLine.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
} 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 result = await db.query(
|
||||
`
|
||||
SELECT
|
||||
rs.id,
|
||||
rs.bus_line_id,
|
||||
rs.stop_id,
|
||||
rs.stop_order,
|
||||
rs.estimated_arrival_minutes,
|
||||
s.name as stop_name,
|
||||
s.latitude,
|
||||
s.longitude
|
||||
FROM route_stops rs
|
||||
JOIN stops s ON s.id = rs.stop_id
|
||||
WHERE rs.bus_line_id = $1
|
||||
ORDER BY rs.stop_order ASC
|
||||
`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
routeStops: result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
busLineId: row.bus_line_id,
|
||||
stopId: row.stop_id,
|
||||
stopOrder: row.stop_order,
|
||||
estimatedArrivalMinutes: row.estimated_arrival_minutes,
|
||||
name: row.stop_name,
|
||||
latitude: parseFloat(row.latitude),
|
||||
longitude: parseFloat(row.longitude),
|
||||
})),
|
||||
},
|
||||
};
|
||||
} 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 result = await db.query(
|
||||
`
|
||||
WITH next_id AS (
|
||||
SELECT COALESCE(MAX(id), 0) + 1 AS id FROM route_stops
|
||||
)
|
||||
INSERT INTO route_stops (id, bus_line_id, stop_id, stop_order, estimated_arrival_minutes)
|
||||
SELECT next_id.id, $1, $2, $3, $4
|
||||
FROM next_id
|
||||
ON CONFLICT (bus_line_id, stop_order) DO UPDATE SET
|
||||
stop_id = EXCLUDED.stop_id,
|
||||
estimated_arrival_minutes = EXCLUDED.estimated_arrival_minutes,
|
||||
updated_at = NOW()
|
||||
RETURNING id, bus_line_id, stop_id, stop_order, estimated_arrival_minutes, created_at, updated_at
|
||||
`,
|
||||
[busLineId, stopId, stopOrder, estimatedArrivalMinutes ?? null],
|
||||
);
|
||||
|
||||
const link = result.rows[0];
|
||||
return {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
routeStop: {
|
||||
id: link.id,
|
||||
busLineId: link.bus_line_id,
|
||||
stopId: link.stop_id,
|
||||
stopOrder: link.stop_order,
|
||||
estimatedArrivalMinutes: link.estimated_arrival_minutes,
|
||||
createdAt: link.created_at,
|
||||
updatedAt: link.updated_at,
|
||||
},
|
||||
},
|
||||
};
|
||||
} 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) {
|
||||
const client = await db.pool.connect();
|
||||
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 current = await client.query(
|
||||
`SELECT id FROM route_stops WHERE bus_line_id = $1 ORDER BY stop_order ASC`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
const currentIds = current.rows.map((row) => Number(row.id));
|
||||
const sameCount = currentIds.length === normalizedIds.length;
|
||||
const sameIds =
|
||||
sameCount && normalizedIds.every((id) => currentIds.includes(id));
|
||||
|
||||
if (!sameIds) {
|
||||
return { statusCode: 400, body: { error: "routeStopIds must match current bus line stops" } };
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (let index = 0; index < normalizedIds.length; index += 1) {
|
||||
await client.query(
|
||||
`UPDATE route_stops
|
||||
SET stop_order = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND bus_line_id = $3`,
|
||||
[-(index + 1), normalizedIds[index], busLineId],
|
||||
);
|
||||
}
|
||||
|
||||
for (let index = 0; index < normalizedIds.length; index += 1) {
|
||||
await client.query(
|
||||
`UPDATE route_stops
|
||||
SET stop_order = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND bus_line_id = $3`,
|
||||
[index + 1, normalizedIds[index], busLineId],
|
||||
);
|
||||
}
|
||||
|
||||
const result = await client.query(
|
||||
`SELECT
|
||||
rs.id,
|
||||
rs.stop_order,
|
||||
s.name as stop_name,
|
||||
s.latitude,
|
||||
s.longitude,
|
||||
rs.estimated_arrival_minutes
|
||||
FROM route_stops rs
|
||||
JOIN stops s ON s.id = rs.stop_id
|
||||
WHERE rs.bus_line_id = $1
|
||||
ORDER BY rs.stop_order ASC`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
routeStops: result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
order: row.stop_order,
|
||||
name: row.stop_name,
|
||||
latitude: parseFloat(row.latitude),
|
||||
longitude: parseFloat(row.longitude),
|
||||
estimatedArrivalMinutes: row.estimated_arrival_minutes,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK").catch(() => {});
|
||||
console.error(err);
|
||||
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
|
||||
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLine(req) {
|
||||
const client = await db.pool.connect();
|
||||
try {
|
||||
const { busLineId } = req.params;
|
||||
|
||||
if (!busLineId || !Number.isFinite(Number(busLineId))) {
|
||||
return { statusCode: 400, body: { error: "invalid_bus_line_id" } };
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Delete associated route_stops first
|
||||
await client.query(
|
||||
`DELETE FROM route_stops WHERE bus_line_id = $1`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
// Delete the bus line
|
||||
const result = await client.query(
|
||||
`DELETE FROM bus_lines WHERE id = $1 RETURNING id, line_number`,
|
||||
[busLineId],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return { statusCode: 404, body: { error: "bus_line_not_found" } };
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { statusCode: 200, body: { success: true, busLine: result.rows[0] } };
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK").catch(() => {});
|
||||
console.error(err);
|
||||
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
|
||||
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLineStop(req) {
|
||||
const client = await db.pool.connect();
|
||||
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" } };
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Find and delete the route_stop
|
||||
const result = await client.query(
|
||||
`DELETE FROM route_stops
|
||||
WHERE id = $1 AND bus_line_id = $2
|
||||
RETURNING id, stop_order`,
|
||||
[routeStopId, busLineId],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return { statusCode: 404, body: { error: "route_stop_not_found" } };
|
||||
}
|
||||
|
||||
const deletedOrder = result.rows[0].stop_order;
|
||||
|
||||
// Re-order remaining stops
|
||||
await client.query(
|
||||
`UPDATE route_stops
|
||||
SET stop_order = stop_order - 1, updated_at = NOW()
|
||||
WHERE bus_line_id = $1 AND stop_order > $2`,
|
||||
[busLineId, deletedOrder],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { statusCode: 200, body: { success: true, routeStop: result.rows[0] } };
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK").catch(() => {});
|
||||
console.error(err);
|
||||
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
|
||||
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listLineStops,
|
||||
listLines,
|
||||
createLine,
|
||||
updateLine,
|
||||
listLineStopsDetailed,
|
||||
addLineStop,
|
||||
reorderLineStops,
|
||||
deleteLine,
|
||||
deleteLineStop,
|
||||
};
|
||||
Reference in New Issue
Block a user