feat: refactored /api into its own router with error handler
This commit is contained in:
@@ -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.");
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
|
||||||
|
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
|
||||||
+3
-133
@@ -1,146 +1,16 @@
|
|||||||
import { unlink } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import multer from "multer";
|
|
||||||
import ViteExpress from "vite-express";
|
import ViteExpress from "vite-express";
|
||||||
import z from "zod";
|
import { apiRouter } from "./api.ts";
|
||||||
import { db } from "./db/database.ts";
|
import { baseUploadsFolder } from "./base-uploads-folder.ts";
|
||||||
|
|
||||||
const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
|
|
||||||
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
dest: baseUploadsFolder,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
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) => {
|
app.use((req, _res, next) => {
|
||||||
console.log(`Received request ${req.url}`);
|
console.log(`Received request ${req.url}`);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/announcements", async (_, res) => {
|
app.use("/api", apiRouter);
|
||||||
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("/images", express.static(baseUploadsFolder));
|
app.use("/images", express.static(baseUploadsFolder));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user