Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -5,6 +5,8 @@ import multer from "multer";
|
||||
import { parseWord, PARSER_VERSION } from "./parser.js";
|
||||
import { scheduleFor } from "./schedules.js";
|
||||
import { rateLimit, semaphore } from "./limits.js";
|
||||
import { apiError, describe } from "./errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { timetable } from "./timetable.js";
|
||||
import {
|
||||
getIndex,
|
||||
@@ -39,8 +41,9 @@ app.use("/api", (_req, res, next) => {
|
||||
});
|
||||
// Raised whenever an endpoint is added or changed, so a deployment is easy to verify:
|
||||
// 2 — /api/timetable for the Android client; 3 — its range=semester;
|
||||
// 4 — compression, ETag revalidation, throttled refresh and import limits.
|
||||
const API_VERSION = 4;
|
||||
// 4 — compression, ETag revalidation, throttled refresh and import limits;
|
||||
// 5 — error codes (`code` beside `error`, see server/errors.js), timetable 304 before any work.
|
||||
const API_VERSION = 5;
|
||||
|
||||
// Liveness plus what the server currently reads from the university, without network calls.
|
||||
app.get("/api/health", async (_req, res) => {
|
||||
@@ -54,7 +57,7 @@ app.get("/api/health", async (_req, res) => {
|
||||
});
|
||||
});
|
||||
const isDate = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s || "");
|
||||
const badRequest = (res, error) => res.status(400).json({ error });
|
||||
const badRequest = (res, error) => res.status(400).json({ error, code: "bad_request" });
|
||||
// University-wide lookups over every catalog document.
|
||||
app.get("/api/directory/search", async (req, res, next) => {
|
||||
try {
|
||||
@@ -112,16 +115,28 @@ app.get("/api/timetable", async (req, res, next) => {
|
||||
if (!doc || !group) return badRequest(res, "Укажите документ (doc) и группу (group).");
|
||||
if (!isDate(from)) return badRequest(res, "Дата должна быть в формате ГГГГ-ММ-ДД.");
|
||||
const file = await documentFor(doc);
|
||||
const calendar = await directoryCalendar();
|
||||
const range = req.query.range === "semester" ? "semester" : undefined;
|
||||
const today = todayIso();
|
||||
// The answer follows only from these: the document's version, the parser and the server
|
||||
// that read it, the week table, the request and the date. An unchanged one comes back
|
||||
// as an empty 304 before the document is opened or a single day is computed.
|
||||
const etag = 'W/"tt-' + createHash("sha1")
|
||||
.update([API_VERSION, PARSER_VERSION, file.id, file.modified, calendar?.updatedAt, group, from, req.query.days, range, today].join("|"))
|
||||
.digest("base64url")
|
||||
.slice(0, 22) + '"';
|
||||
res.set("ETag", etag);
|
||||
if (req.fresh) return res.status(304).end();
|
||||
res.json(
|
||||
timetable({
|
||||
file,
|
||||
schedule: await scheduleFor(file),
|
||||
calendar: await directoryCalendar(),
|
||||
calendar,
|
||||
group,
|
||||
from,
|
||||
days: req.query.days,
|
||||
range: req.query.range === "semester" ? "semester" : undefined,
|
||||
today: todayIso(),
|
||||
range,
|
||||
today,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -194,39 +209,30 @@ const upload = multer({
|
||||
app.post("/api/import", importLimit, upload.single("file"), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file)
|
||||
return res.status(400).json({ error: "Выберите файл Word." });
|
||||
return badRequest(res, "Выберите файл Word.");
|
||||
// Multer decodes multipart names as latin1 by default.
|
||||
const name = Buffer.from(req.file.originalname, "latin1").toString("utf8");
|
||||
if (!/\.docx?$/i.test(name))
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Поддерживаются файлы .docx и .doc." });
|
||||
return badRequest(res, "Поддерживаются файлы .docx и .doc.");
|
||||
res.json(await parsing(() => parseWord(req.file.buffer, { name, uploaded: true })));
|
||||
} catch (e) {
|
||||
// A file that cannot be read is the client's problem (422); a busy server keeps its 429.
|
||||
e.status ??= 422;
|
||||
next(e);
|
||||
// A file that cannot be read is the client's problem; a busy server keeps its 429.
|
||||
next(e.status || e.code === "LIMIT_FILE_SIZE" ? e : apiError(422, "unreadable_file", e.message));
|
||||
}
|
||||
});
|
||||
const dist = fileURLToPath(new URL("../dist/", import.meta.url));
|
||||
app.use(express.static(dist, { maxAge: 0 }));
|
||||
app.get("/{*path}", (req, res, next) =>
|
||||
req.path.startsWith("/api/")
|
||||
? res.status(404).json({ error: "Адрес не найден." })
|
||||
? res.status(404).json({ error: "Адрес не найден.", code: "unknown_endpoint" })
|
||||
: res.sendFile(dist + "index.html", (err) => err && next(err)),
|
||||
);
|
||||
app.use((error, _req, res, _next) => {
|
||||
console.error(error.message);
|
||||
res
|
||||
.status(error.code === "LIMIT_FILE_SIZE" ? 413 : error.status || 503)
|
||||
.json({
|
||||
error:
|
||||
error.code === "LIMIT_FILE_SIZE"
|
||||
? "Файл больше 10 МБ. Выберите документ меньшего размера."
|
||||
: [404, 422, 429].includes(error.status)
|
||||
? error.message
|
||||
: "Не удалось загрузить расписание. Проверьте подключение и попробуйте ещё раз.",
|
||||
});
|
||||
app.use((error, req, res, _next) => {
|
||||
const { status, code, message } = describe(error);
|
||||
// Faults of the server keep their stack; expected errors need a line.
|
||||
if (status >= 500 && code === "internal") console.error(req.method, req.originalUrl, error);
|
||||
else console.error(`${req.method} ${req.originalUrl} → ${status} ${code}: ${error.message}`);
|
||||
res.status(status).json({ error: message, code });
|
||||
});
|
||||
const port = Number(process.env.PORT || 3001);
|
||||
app.listen(port, process.env.HOST || "127.0.0.1", () => {
|
||||
|
||||
Reference in New Issue
Block a user