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,545 @@
|
||||
const { store: _mock } = require("../mock/store");
|
||||
const { toDateKey, expandRecurringScheduleRows, scheduleMatchesFilters, parseScheduleFilters, mapRecurringOccurrenceRows, materializeScheduleOverviewRow } = require("../lib/schedule");
|
||||
const { reportError } = require("../lib/report-error");
|
||||
|
||||
// Schedules repository methods (DEV_MOCK mode). Exact copies of the original
|
||||
// handlers’ mock branches, returning { statusCode, body }. Note: the markStopPassed
|
||||
// mock branch reproduces the original ReferenceError (it reads an undeclared
|
||||
// `delayMinutes`), so it always returns 500 in mock mode, exactly as before.
|
||||
|
||||
async function listSchedules(req) {
|
||||
try {
|
||||
const filters = parseScheduleFilters(req.query, {
|
||||
statuses: ["planned", "active"],
|
||||
});
|
||||
|
||||
const routeStopsByLine = new Map();
|
||||
for (const routeStop of _mock.routeStops || []) {
|
||||
const busLineId = Number(routeStop.busLineId);
|
||||
if (!routeStopsByLine.has(busLineId)) {
|
||||
routeStopsByLine.set(busLineId, []);
|
||||
}
|
||||
routeStopsByLine.get(busLineId).push({
|
||||
stop_id: routeStop.stopId,
|
||||
stop_order: routeStop.stopOrder,
|
||||
});
|
||||
}
|
||||
|
||||
const schedules = expandRecurringScheduleRows(
|
||||
(_mock.schedules || []).map((s) => {
|
||||
const busLine = _mock.busLines.find((bl) => bl.id === s.busLineId);
|
||||
const bus = _mock.fleetBuses.find((b) => b.id === s.busId);
|
||||
return {
|
||||
id: s.id,
|
||||
busLineId: s.busLineId,
|
||||
busId: s.busId,
|
||||
startTime: s.startTime,
|
||||
endTime: s.endTime,
|
||||
weekdays: s.weekdays || [],
|
||||
status: s.status,
|
||||
lineNumber: busLine?.lineNumber || 0,
|
||||
busName: bus?.name || "Unknown Bus",
|
||||
driverId: null,
|
||||
estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60,
|
||||
};
|
||||
}),
|
||||
).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
||||
|
||||
return { statusCode: 200, body: { schedules } };
|
||||
} 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 getSchedulesOverview(req) {
|
||||
try {
|
||||
const filters = parseScheduleFilters(req.query, {
|
||||
statuses: ["planned", "active"],
|
||||
});
|
||||
const overviewDateKey = filters.date || null;
|
||||
|
||||
const routeStopsByLine = new Map();
|
||||
|
||||
for (const routeStop of _mock.routeStops || []) {
|
||||
const busLineId = Number(routeStop.busLineId);
|
||||
if (!routeStopsByLine.has(busLineId)) {
|
||||
routeStopsByLine.set(busLineId, []);
|
||||
}
|
||||
|
||||
routeStopsByLine.get(busLineId).push({
|
||||
bus_line_id: busLineId,
|
||||
stop_id: routeStop.stopId,
|
||||
stop_order: routeStop.stopOrder,
|
||||
estimated_arrival_minutes: routeStop.estimatedArrivalMinutes,
|
||||
stop_name: routeStop.name || "",
|
||||
});
|
||||
}
|
||||
|
||||
const baseRows = (_mock.schedules || []).map((schedule) => {
|
||||
const busLine = _mock.busLines.find(
|
||||
(line) => Number(line.id) === Number(schedule.busLineId),
|
||||
);
|
||||
const bus = _mock.fleetBuses.find(
|
||||
(vehicle) => Number(vehicle.id) === Number(schedule.busId),
|
||||
);
|
||||
const driver = _mock.drivers.find(
|
||||
(person) => Number(person.id) === Number(schedule.driverId),
|
||||
);
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
busLineId: schedule.busLineId,
|
||||
busId: schedule.busId,
|
||||
driverId: schedule.driverId ?? null,
|
||||
driverName: driver ? driver.name : null,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
weekdays: schedule.weekdays || [],
|
||||
status: schedule.status,
|
||||
lineNumber: busLine?.lineNumber || 0,
|
||||
busName: bus?.name || "Unknown Bus",
|
||||
licensePlate: bus?.licensePlate || "",
|
||||
createdAt: schedule.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const schedules = mapRecurringOccurrenceRows(
|
||||
baseRows,
|
||||
overviewDateKey,
|
||||
routeStopsByLine,
|
||||
).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
date: overviewDateKey,
|
||||
summary: {
|
||||
total: schedules.length,
|
||||
recurring: schedules.filter(
|
||||
(schedule) => schedule.scheduleType === "recurring",
|
||||
).length,
|
||||
oneTime: schedules.filter(
|
||||
(schedule) => schedule.scheduleType === "one_time",
|
||||
).length,
|
||||
},
|
||||
schedules,
|
||||
},
|
||||
};
|
||||
} 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 listRecurringSchedules(req) {
|
||||
try {
|
||||
const filters = parseScheduleFilters(req.query);
|
||||
|
||||
const routeStopsByLine = new Map();
|
||||
|
||||
for (const routeStop of _mock.routeStops || []) {
|
||||
const busLineId = Number(routeStop.busLineId);
|
||||
if (!routeStopsByLine.has(busLineId)) {
|
||||
routeStopsByLine.set(busLineId, []);
|
||||
}
|
||||
|
||||
routeStopsByLine.get(busLineId).push({
|
||||
bus_line_id: busLineId,
|
||||
stop_id: routeStop.stopId,
|
||||
stop_order: routeStop.stopOrder,
|
||||
estimated_arrival_minutes: routeStop.estimatedArrivalMinutes,
|
||||
stop_name: routeStop.name || "",
|
||||
});
|
||||
}
|
||||
|
||||
const baseRows = (_mock.schedules || [])
|
||||
.filter(
|
||||
(schedule) =>
|
||||
Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0,
|
||||
)
|
||||
.map((schedule) => {
|
||||
const busLine = _mock.busLines.find(
|
||||
(line) => Number(line.id) === Number(schedule.busLineId),
|
||||
);
|
||||
const bus = _mock.fleetBuses.find(
|
||||
(vehicle) => Number(vehicle.id) === Number(schedule.busId),
|
||||
);
|
||||
const driver = _mock.drivers.find(
|
||||
(person) => Number(person.id) === Number(schedule.driverId),
|
||||
);
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
busLineId: schedule.busLineId,
|
||||
busId: schedule.busId,
|
||||
driverId: schedule.driverId ?? null,
|
||||
driverName: driver ? driver.name : null,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
weekdays: schedule.weekdays || [],
|
||||
lineNumber: busLine?.lineNumber || 0,
|
||||
busName: bus?.name || "Unknown Bus",
|
||||
licensePlate: bus?.licensePlate || "",
|
||||
createdAt: schedule.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const schedules = baseRows
|
||||
.filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine))
|
||||
.map((row) => {
|
||||
const materialized = materializeScheduleOverviewRow(
|
||||
row,
|
||||
null,
|
||||
routeStopsByLine,
|
||||
);
|
||||
return {
|
||||
...materialized,
|
||||
status: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return { statusCode: 200, body: { schedules } };
|
||||
} 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 getScheduleById(req) {
|
||||
try {
|
||||
const { scheduleId } = req.params;
|
||||
|
||||
const schedule = (_mock.schedules || []).find(
|
||||
(s) => Number(s.id) === Number(scheduleId),
|
||||
);
|
||||
if (!schedule) {
|
||||
return { statusCode: 404, body: { error: "schedule_not_found" } };
|
||||
}
|
||||
|
||||
const busLine = _mock.busLines.find((bl) => bl.id === schedule.busLineId);
|
||||
const bus = _mock.fleetBuses.find((b) => b.id === schedule.busId);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
schedule: {
|
||||
id: schedule.id,
|
||||
busLineId: schedule.busLineId,
|
||||
busId: schedule.busId,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
weekdays: schedule.weekdays || [],
|
||||
status: schedule.status,
|
||||
lineNumber: busLine?.lineNumber || 0,
|
||||
busName: bus?.name || "Unknown Bus",
|
||||
},
|
||||
},
|
||||
};
|
||||
} 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 getScheduleStops(req) {
|
||||
try {
|
||||
const { scheduleId } = req.params;
|
||||
|
||||
const schedule = (_mock.schedules || []).find(
|
||||
(s) => Number(s.id) === Number(scheduleId),
|
||||
);
|
||||
if (!schedule) {
|
||||
return { statusCode: 404, body: { error: "schedule_not_found" } };
|
||||
}
|
||||
const routeStops = (_mock.routeStops || [])
|
||||
.filter((rs) => Number(rs.busLineId) === Number(schedule.busLineId))
|
||||
.sort((a, b) => a.stopOrder - b.stopOrder);
|
||||
|
||||
const formattedStops = routeStops.map((rs) => {
|
||||
const stop = (_mock.stops || []).find(
|
||||
(s) => Number(s.id) === Number(rs.stopId),
|
||||
);
|
||||
return {
|
||||
id: rs.id,
|
||||
order: rs.stopOrder,
|
||||
name: stop?.name || rs.name || "Unknown Stop",
|
||||
latitude: stop?.latitude ?? rs.latitude ?? 0,
|
||||
longitude: stop?.longitude ?? rs.longitude ?? 0,
|
||||
estimatedArrivalMinutes: rs.estimatedArrivalMinutes || 0,
|
||||
};
|
||||
});
|
||||
return { statusCode: 200, body: { stops: formattedStops } };
|
||||
} 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 markStopPassed(req) {
|
||||
try {
|
||||
const scheduleId = Number(req.params.scheduleId);
|
||||
const stopOrder = Number(req.params.stopOrder);
|
||||
|
||||
if (
|
||||
!Number.isFinite(scheduleId) ||
|
||||
!Number.isFinite(stopOrder) ||
|
||||
stopOrder < 1
|
||||
) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: "invalid_schedule_or_stop_order" },
|
||||
};
|
||||
}
|
||||
|
||||
const actualPassedAt = req.body?.actualPassedAt
|
||||
? new Date(req.body.actualPassedAt)
|
||||
: new Date();
|
||||
if (Number.isNaN(actualPassedAt.getTime())) {
|
||||
return { statusCode: 400, body: { error: "invalid_actual_passed_at" } };
|
||||
}
|
||||
|
||||
const schedule = (_mock.schedules || []).find(
|
||||
(s) => Number(s.id) === scheduleId,
|
||||
);
|
||||
if (!schedule) {
|
||||
return { statusCode: 404, body: { error: "schedule_not_found" } };
|
||||
}
|
||||
|
||||
const routeStop = (_mock.routeStops || []).find(
|
||||
(rs) =>
|
||||
Number(rs.busLineId) === Number(schedule.busLineId) &&
|
||||
Number(rs.stopOrder) === stopOrder,
|
||||
);
|
||||
|
||||
if (!routeStop) {
|
||||
return { statusCode: 404, body: { error: "stop_not_found_for_schedule" } };
|
||||
}
|
||||
|
||||
const stop = (_mock.stops || []).find(
|
||||
(s) => Number(s.id) === Number(routeStop.stopId),
|
||||
);
|
||||
const occurrenceDateKey =
|
||||
req.query.date || req.body.date || toDateKey(schedule.startTime);
|
||||
const templateStart = new Date(schedule.startTime);
|
||||
const scheduledPassedAt = new Date(`${occurrenceDateKey}T00:00:00`);
|
||||
scheduledPassedAt.setHours(
|
||||
templateStart.getHours(),
|
||||
templateStart.getMinutes(),
|
||||
templateStart.getSeconds(),
|
||||
templateStart.getMilliseconds(),
|
||||
);
|
||||
scheduledPassedAt.setMinutes(
|
||||
scheduledPassedAt.getMinutes() +
|
||||
(Number(routeStop.estimatedArrivalMinutes) || 0),
|
||||
);
|
||||
|
||||
const existingIndex = (_mock.scheduleStopProgress || []).findIndex(
|
||||
(p) =>
|
||||
Number(p.scheduleId) === scheduleId &&
|
||||
Number(p.stopOrder) === stopOrder,
|
||||
);
|
||||
|
||||
const progressRow = {
|
||||
id:
|
||||
existingIndex >= 0
|
||||
? _mock.scheduleStopProgress[existingIndex].id
|
||||
: _mock.nextScheduleStopProgressId++,
|
||||
scheduleId,
|
||||
busLineId: Number(schedule.busLineId),
|
||||
stopId: Number(routeStop.stopId),
|
||||
stopOrder,
|
||||
stopName: stop?.name || routeStop.name || "Unknown Stop",
|
||||
estimatedArrivalMinutes:
|
||||
Number(routeStop.estimatedArrivalMinutes) || 0,
|
||||
scheduledPassedAt: scheduledPassedAt.toISOString(),
|
||||
scheduledPassedDate: occurrenceDateKey,
|
||||
actualPassedAt: actualPassedAt.toISOString(),
|
||||
delayMinutes,
|
||||
markedByUserId: Number(req.user?.userId) || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
_mock.scheduleStopProgress[existingIndex] = progressRow;
|
||||
} else {
|
||||
_mock.scheduleStopProgress.push(progressRow);
|
||||
}
|
||||
|
||||
const totalStops = (_mock.routeStops || []).filter(
|
||||
(rs) => Number(rs.busLineId) === Number(schedule.busLineId),
|
||||
).length;
|
||||
const passedCount = (_mock.scheduleStopProgress || []).filter(
|
||||
(p) =>
|
||||
Number(p.scheduleId) === scheduleId &&
|
||||
p.scheduledPassedDate === occurrenceDateKey,
|
||||
).length;
|
||||
|
||||
if (passedCount >= totalStops) {
|
||||
schedule.status = "completed";
|
||||
schedule.endTime = actualPassedAt.toISOString();
|
||||
} else if (schedule.status === "planned") {
|
||||
schedule.status = "in_progress";
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
scheduleId,
|
||||
stopOrder,
|
||||
status: schedule.status,
|
||||
delayMinutes,
|
||||
passedStops: passedCount,
|
||||
totalStops,
|
||||
occurrenceDate: occurrenceDateKey,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"POST /driver/schedules/:scheduleId/stops/:stopOrder/passed error:",
|
||||
err,
|
||||
);
|
||||
reportError({ partofservice: "POST /driver/schedules/:scheduleId/stops/:stopOrder/passed", error: err.stack || err.message });
|
||||
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
|
||||
}
|
||||
}
|
||||
|
||||
async function getPublicScheduleProgress(req) {
|
||||
try {
|
||||
const scheduleId = Number(req.params.scheduleId);
|
||||
if (!Number.isFinite(scheduleId)) {
|
||||
return { statusCode: 400, body: { error: "invalid_schedule_id" } };
|
||||
}
|
||||
|
||||
const schedule = (_mock.schedules || []).find(
|
||||
(s) => Number(s.id) === scheduleId,
|
||||
);
|
||||
if (!schedule) {
|
||||
return { statusCode: 404, body: { error: "schedule_not_found" } };
|
||||
}
|
||||
|
||||
const busLine = (_mock.busLines || []).find(
|
||||
(bl) => Number(bl.id) === Number(schedule.busLineId),
|
||||
);
|
||||
const routeStops = (_mock.routeStops || [])
|
||||
.filter((rs) => Number(rs.busLineId) === Number(schedule.busLineId))
|
||||
.sort((a, b) => Number(a.stopOrder) - Number(b.stopOrder));
|
||||
|
||||
const mockProgressDate =
|
||||
req.query.date || req.query.occurrenceDate || null;
|
||||
let progressEvents = (_mock.scheduleStopProgress || []).filter(
|
||||
(p) => Number(p.scheduleId) === scheduleId,
|
||||
);
|
||||
if (mockProgressDate) {
|
||||
progressEvents = progressEvents.filter(
|
||||
(p) => p.scheduledPassedDate === mockProgressDate,
|
||||
);
|
||||
}
|
||||
progressEvents = progressEvents.sort(
|
||||
(a, b) => Number(a.stopOrder) - Number(b.stopOrder),
|
||||
);
|
||||
|
||||
const progressByOrder = new Map(
|
||||
progressEvents.map((p) => [Number(p.stopOrder), p]),
|
||||
);
|
||||
const latestProgress =
|
||||
progressEvents.length > 0
|
||||
? progressEvents[progressEvents.length - 1]
|
||||
: null;
|
||||
const currentDelayMinutes = latestProgress
|
||||
? Number(latestProgress.delayMinutes) || 0
|
||||
: 0;
|
||||
|
||||
const occurrenceDateKey =
|
||||
req.query.date ||
|
||||
req.query.occurrenceDate ||
|
||||
toDateKey(schedule.startTime);
|
||||
const stops = routeStops.map((rs) => {
|
||||
const stop = (_mock.stops || []).find(
|
||||
(s) => Number(s.id) === Number(rs.stopId),
|
||||
);
|
||||
const templateStart = new Date(schedule.startTime);
|
||||
const scheduledTime = new Date(`${occurrenceDateKey}T00:00:00`);
|
||||
scheduledTime.setHours(
|
||||
templateStart.getHours(),
|
||||
templateStart.getMinutes(),
|
||||
templateStart.getSeconds(),
|
||||
templateStart.getMilliseconds(),
|
||||
);
|
||||
scheduledTime.setMinutes(
|
||||
scheduledTime.getMinutes() +
|
||||
(Number(rs.estimatedArrivalMinutes) || 0),
|
||||
);
|
||||
const progress = progressByOrder.get(Number(rs.stopOrder));
|
||||
|
||||
return {
|
||||
stopId: Number(rs.stopId),
|
||||
stopOrder: Number(rs.stopOrder),
|
||||
stopName: stop?.name || rs.name || "Unknown Stop",
|
||||
estimatedArrivalMinutes: Number(rs.estimatedArrivalMinutes) || 0,
|
||||
scheduledPassedAt: scheduledTime.toISOString(),
|
||||
actualPassedAt: progress?.actualPassedAt || null,
|
||||
delayMinutes: progress ? Number(progress.delayMinutes) || 0 : null,
|
||||
status: progress ? "passed" : "pending",
|
||||
};
|
||||
});
|
||||
|
||||
const passedStops = stops.filter((s) => s.status === "passed");
|
||||
const nextStop = stops.find((s) => s.status === "pending") || null;
|
||||
const driverState =
|
||||
passedStops.length === 0
|
||||
? "not_started"
|
||||
: passedStops.length >= stops.length
|
||||
? "completed"
|
||||
: "en_route";
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
schedule: {
|
||||
id: scheduleId,
|
||||
lineNumber: Number(busLine?.lineNumber || 0),
|
||||
startStop: busLine?.startStop || null,
|
||||
endStop: busLine?.endStop || null,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
status: schedule.status,
|
||||
},
|
||||
driverState,
|
||||
currentDelayMinutes,
|
||||
passedStopCount: passedStops.length,
|
||||
totalStopCount: stops.length,
|
||||
nextStop: nextStop
|
||||
? {
|
||||
...nextStop,
|
||||
predictedPassedAt: new Date(
|
||||
new Date(nextStop.scheduledPassedAt).getTime() +
|
||||
currentDelayMinutes * 60000,
|
||||
).toISOString(),
|
||||
}
|
||||
: null,
|
||||
stops,
|
||||
updatedAt: latestProgress?.actualPassedAt || null,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("GET /public/schedules/:scheduleId/progress error:", err);
|
||||
reportError({ partofservice: "GET /public/schedules/:scheduleId/progress", error: err.stack || err.message });
|
||||
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listSchedules,
|
||||
getSchedulesOverview,
|
||||
listRecurringSchedules,
|
||||
getScheduleById,
|
||||
getScheduleStops,
|
||||
markStopPassed,
|
||||
getPublicScheduleProgress,
|
||||
};
|
||||
Reference in New Issue
Block a user