From 1a7deed5b01ebbab34fe17f149c6c3810a92bbb0 Mon Sep 17 00:00:00 2001 From: redglow Date: Wed, 24 Jun 2026 10:09:48 +0200 Subject: [PATCH] feat: refactored /api into its own router with error handler --- src/server/api.ts | 142 ++++++++++++++++++++++++++++++ src/server/base-uploads-folder.ts | 2 + src/server/main.ts | 136 +--------------------------- 3 files changed, 147 insertions(+), 133 deletions(-) create mode 100644 src/server/api.ts create mode 100644 src/server/base-uploads-folder.ts diff --git a/src/server/api.ts b/src/server/api.ts new file mode 100644 index 0000000..f04eb2a --- /dev/null +++ b/src/server/api.ts @@ -0,0 +1,142 @@ +import { unlink } from "node:fs/promises"; +import path from "node:path"; +import { NextFunction, Request, Response, Router } from "express"; +import multer from "multer"; +import z from "zod"; +import { baseUploadsFolder } from "./base-uploads-folder.ts"; +import { db } from "./db/database.ts"; + +const PostInput = z.object({ + file: z.object({ filename: z.string() }), + body: z.object({ + publication_date: z.coerce + .date() + .transform((x) => new Date(x).toISOString()), + title: z.string().nonempty(), + text: z.string().nonempty(), + }), +}); + +const PatchInput = z.object({ + file: z.object({ filename: z.string().optional() }).optional(), + body: z.object({ + publication_date: z.coerce + .date() + .optional() + .transform((x) => (x ? new Date(x).toISOString() : x)), + title: z.string().nonempty().optional(), + text: z.string().nonempty().optional(), + }), +}); + +const AnnouncementIdInput = z.object({ + announcementId: z.coerce.number().positive(), +}); + +const GetFromGame = z.object({ + gameName: z.string().nonempty(), +}); + +const upload = multer({ + dest: baseUploadsFolder, +}); + +export const apiRouter = Router(); + +apiRouter.get("/announcements", async (_, res) => { + console.log(`Processing GET /api/announcements`); + const announcements = await db + .selectFrom("announcements") + .select(["id", "title", "text", "publication_date", "image"]) + .orderBy("announcements.publication_date", "desc") + .execute(); + res.json(announcements); +}); + +apiRouter.get("/announcements/from-game", async (req, res) => { + console.log(`Processing GET /api/announcements/from-game`); + const fromGameData = GetFromGame.parse(req.query); + console.log(`Request from game ${fromGameData.gameName}`); + const announcements = await db + .selectFrom("announcements") + .select(["id", "title", "text", "publication_date", "image"]) + .orderBy("announcements.publication_date", "desc") + .where("publication_date", "<=", new Date().toISOString()) + .limit(3) + .execute(); + res.json(announcements); +}); + +apiRouter.get("/announcements/:announcementId", async (req, res) => { + console.log(`Processing GET /api/announcements/:announcementId`); + const { announcementId } = AnnouncementIdInput.parse(req.params); + const announcement = await db + .selectFrom("announcements") + .select(["id", "title", "text", "publication_date", "image"]) + .where("id", "=", announcementId) + .executeTakeFirstOrThrow(); + res.json(announcement); +}); + +apiRouter.post("/announcements", upload.single("image"), async (req, res) => { + console.log(`Processing POST /api/announcements`); + const postInput = PostInput.parse(req); + const announcement = { + title: postInput.body.title, + text: postInput.body.text, + publication_date: postInput.body.publication_date, + image: postInput.file.filename, + }; + await db.insertInto("announcements").values(announcement).execute(); + res.redirect("/"); +}); + +apiRouter.post( + "/announcements/:announcementId", + upload.single("image"), + async (req, res) => { + console.log(`Processing POST /api/announcements/:announcementId`); + const patchInput = PatchInput.parse(req); + const { announcementId } = AnnouncementIdInput.parse(req.params); + await db + .updateTable("announcements") + .set({ + title: patchInput.body.title, + text: patchInput.body.text, + publication_date: patchInput.body.publication_date, + image: patchInput.file?.filename, + }) + .where("id", "=", announcementId) + .execute(); + + res.redirect("/"); + }, +); + +apiRouter.delete("/announcements/:announcementId", async (req, res) => { + console.log(`Processing DELETE /api/announcements`); + + const { announcementId } = AnnouncementIdInput.parse(req.params); + + const { image } = await db + .selectFrom("announcements") + .select("image") + .where("id", "=", announcementId) + .executeTakeFirstOrThrow(); + + await db + .deleteFrom("announcements") + .where("id", "=", announcementId) + .execute(); + + await unlink(path.join(baseUploadsFolder, image)); + + res.status(204).send(); +}); + +apiRouter.use( + (_err: Error, _req: Request, res: Response, _next: NextFunction) => { + console.error(_err); + res.status(500).send("Internal Server Error."); + }, +); diff --git a/src/server/base-uploads-folder.ts b/src/server/base-uploads-folder.ts new file mode 100644 index 0000000..f56bb82 --- /dev/null +++ b/src/server/base-uploads-folder.ts @@ -0,0 +1,2 @@ +export const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/"; +console.log(`Using ${baseUploadsFolder} as base uploads folder`); diff --git a/src/server/main.ts b/src/server/main.ts index d80cc6f..10e7d16 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -1,146 +1,16 @@ -import { unlink } from "node:fs/promises"; -import path from "node:path"; import express from "express"; -import multer from "multer"; import ViteExpress from "vite-express"; -import z from "zod"; -import { db } from "./db/database.ts"; - -const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/"; -console.log(`Using ${baseUploadsFolder} as base uploads folder`); - -const upload = multer({ - dest: baseUploadsFolder, -}); +import { apiRouter } from "./api.ts"; +import { baseUploadsFolder } from "./base-uploads-folder.ts"; const app = express(); -const PostInput = z.object({ - file: z.object({ filename: z.string() }), - body: z.object({ - publication_date: z.coerce - .date() - .transform((x) => new Date(x).toISOString()), - title: z.string().nonempty(), - text: z.string().nonempty(), - }), -}); - -const PatchInput = z.object({ - file: z.object({ filename: z.string().optional() }).optional(), - body: z.object({ - publication_date: z.coerce - .date() - .optional() - .transform((x) => (x ? new Date(x).toISOString() : x)), - title: z.string().nonempty().optional(), - text: z.string().nonempty().optional(), - }), -}); - -const AnnouncementIdInput = z.object({ - announcementId: z.coerce.number().positive(), -}); - -const GetFromGame = z.object({ - gameName: z.string().nonempty(), -}); - app.use((req, _res, next) => { console.log(`Received request ${req.url}`); next(); }); -app.get("/api/announcements", async (_, res) => { - console.log(`Processing GET /api/announcements`); - const announcements = await db - .selectFrom("announcements") - .select(["id", "title", "text", "publication_date", "image"]) - .orderBy("announcements.publication_date", "desc") - .execute(); - res.json(announcements); -}); - -app.get("/api/announcements/from-game", async (req, res) => { - console.log(`Processing GET /api/announcements/from-game`); - const fromGameData = GetFromGame.parse(req.query); - console.log(`Request from game ${fromGameData.gameName}`); - const announcements = await db - .selectFrom("announcements") - .select(["id", "title", "text", "publication_date", "image"]) - .orderBy("announcements.publication_date", "desc") - .where("publication_date", "<=", new Date().toISOString()) - .limit(3) - .execute(); - res.json(announcements); -}); - -app.get("/api/announcements/:announcementId", async (req, res) => { - console.log(`Processing GET /api/announcements/:announcementId`); - const { announcementId } = AnnouncementIdInput.parse(req.params); - const announcement = await db - .selectFrom("announcements") - .select(["id", "title", "text", "publication_date", "image"]) - .where("id", "=", announcementId) - .executeTakeFirstOrThrow(); - res.json(announcement); -}); - -app.post("/api/announcements", upload.single("image"), async (req, res) => { - console.log(`Processing POST /api/announcements`); - const postInput = PostInput.parse(req); - const announcement = { - title: postInput.body.title, - text: postInput.body.text, - publication_date: postInput.body.publication_date, - image: postInput.file.filename, - }; - await db.insertInto("announcements").values(announcement).execute(); - res.redirect("/"); -}); - -app.post( - "/api/announcements/:announcementId", - upload.single("image"), - async (req, res) => { - console.log(`Processing POST /api/announcements/:announcementId`); - const patchInput = PatchInput.parse(req); - const { announcementId } = AnnouncementIdInput.parse(req.params); - await db - .updateTable("announcements") - .set({ - title: patchInput.body.title, - text: patchInput.body.text, - publication_date: patchInput.body.publication_date, - image: patchInput.file?.filename, - }) - .where("id", "=", announcementId) - .execute(); - - res.redirect("/"); - }, -); - -app.delete("/api/announcements/:announcementId", async (req, res) => { - console.log(`Processing DELETE /api/announcements`); - - const { announcementId } = AnnouncementIdInput.parse(req.params); - - const { image } = await db - .selectFrom("announcements") - .select("image") - .where("id", "=", announcementId) - .executeTakeFirstOrThrow(); - - await db - .deleteFrom("announcements") - .where("id", "=", announcementId) - .execute(); - - await unlink(path.join(baseUploadsFolder, image)); - - res.status(204).send(); -}); +app.use("/api", apiRouter); app.use("/images", express.static(baseUploadsFolder));