From 13e917183f33f3990ced583f3463def45519e406 Mon Sep 17 00:00:00 2001 From: blake Date: Sat, 7 Feb 2026 20:38:56 -0600 Subject: [PATCH] added commentary --- src/db/schema/commentary.js | 44 +++++++++-------- src/index.js | 3 +- src/routes/commentary.route.js | 87 ++++++++++++++++++++++++++++++++++ validation/commentary.js | 17 +++++++ 4 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 src/routes/commentary.route.js create mode 100644 validation/commentary.js diff --git a/src/db/schema/commentary.js b/src/db/schema/commentary.js index 1c84fcb..3b626e1 100644 --- a/src/db/schema/commentary.js +++ b/src/db/schema/commentary.js @@ -1,20 +1,26 @@ +import { + serial, + pgTable, + text, + integer, + jsonb, + timestamp, +} from "drizzle-orm/pg-core"; +import { matches } from "./matches.js"; - -import { serial, pgTable, text, integer, jsonb, timestamp} from "drizzle-orm/pg-core"; -import {matches} from './matches' - - -export const commentary = pgTable('commentary', { - id: serial('id').primaryKey(), - matchId: integer('match_id').notNull().references(()=> matches.id), - minute: integer('minute'), - sequence: integer('sequence'), - period: text("period"), - eventType: text('event_type'), - actor: text('actor'), - team: text('team'), - message: text('message').notNull(), - metadata: jsonb('metadata'), - tags: text('tags').array(), - createdAt: timestamp('created_at').notNull().defaultNow() - }) \ No newline at end of file +export const commentary = pgTable("commentary", { + id: serial("id").primaryKey(), + matchId: integer("match_id") + .notNull() + .references(() => matches.id), + minute: integer("minute"), + sequence: integer("sequence"), + period: text("period"), + eventType: text("event_type"), + actor: text("actor"), + team: text("team"), + message: text("message").notNull(), + metadata: jsonb("metadata"), + tags: text("tags").array(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); diff --git a/src/index.js b/src/index.js index 3304d3e..00ae009 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import http from "http"; import { matchRouter } from "./routes/matches.route.js"; import { attachWebsocketServer } from "./ws/server.js"; import { securityMiddleware } from "./utils/arkjet.js"; +import { comRouter } from "./routes/commentary.route.js"; const PORT = process.env.PORT || 8081; const HOST = process.env.HOST || "0.0.0.0"; @@ -23,9 +24,9 @@ app.get("/", (_, res) => { app.use(securityMiddleware()); app.use("/matches", matchRouter); +app.use("/matches/:id/commentary", comRouter); const { broadcastMatchCreated } = attachWebsocketServer(server); - app.locals.broadcastMatchCreated = broadcastMatchCreated; server.listen(PORT, HOST, () => { diff --git a/src/routes/commentary.route.js b/src/routes/commentary.route.js new file mode 100644 index 0000000..7b1c645 --- /dev/null +++ b/src/routes/commentary.route.js @@ -0,0 +1,87 @@ +import { Router } from "express"; +import { matchIdParamSchema } from "../../validation/matches.js"; +import { + createCommentarySchema, + listCommentaryQuerySchema, +} from "../../validation/commentary.js"; +import { db } from "../db/db.js"; +import { commentary } from "../db/schema/commentary.js"; +import { desc, eq } from "drizzle-orm"; + +export const comRouter = Router({ mergeParams: true }); // this is something we want when we are passing the params from somwhere else in the route. + +const MAX_LIMIT = 100; +comRouter.get("/", async (req, res) => { + const paramsResult = matchIdParamSchema.safeParse(req.params); + + if (!paramsResult.success) { + return res + .status(400) + .json({ error: "Invalid match ID.", details: paramsResult.error.issues }); + } + + const queryResult = listCommentaryQuerySchema.safeParse(req.query); + + if (!queryResult.success) { + return res.status(400).json({ + error: "Invalid query.", + details: queryResult.error.issues, + }); + } + + try { + const { id: matchId } = paramsResult.data; + const { limit = 10 } = queryResult.data; + const safeLimit = Math.min(limit, MAX_LIMIT); + + const results = await db + .select() + .from(commentary) + .where(eq(commentary.matchId, matchId)) + .orderBy(desc(commentary.createdAt)) + .limit(safeLimit); + + res.status(200).json({ data: results }); + } catch (e) { + console.error("Failed to fetch commentary: ", e); + res.status(500).json({ error: "Failed to fetch commentary." }); + } +}); + +comRouter.post("/", async (req, res) => { + const paramsResult = matchIdParamSchema.safeParse(req.params); + + if (!paramsResult.success) { + return res + .status(400) + .json({ error: "Invalid match ID.", details: paramsResult.error.issues }); + } + + const bodyResult = createCommentarySchema.safeParse(req.body); + + if (!bodyResult.success) { + return res.status(400).json({ + error: "Invalid commentary payload.", + details: bodyResult.error.issues, + }); + } + + try { + const { minutes, ...rest } = bodyResult.data; + + const [result] = await db + .insert(commentary) + .values({ + matchId: paramsResult.data.id, + minutes, + ...rest, + }) + .returning(); + + res.status(201).json({ data: result }); + } catch (e) { + console.error("Failed to create commentary:", e); + + res.status(500).json({ error: "Failed to create commentary." }); + } +}); diff --git a/validation/commentary.js b/validation/commentary.js new file mode 100644 index 0000000..2ff4d5e --- /dev/null +++ b/validation/commentary.js @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const listCommentaryQuerySchema = z.object({ + limit: z.coerce.number().int().positive().max(100).optional(), +}); + +export const createCommentarySchema = z.object({ + minutes: z.number().int().nonnegative(), + sequence: z.number().int().optional(), + period: z.string().optional(), + eventType: z.string().optional(), + actor: z.string().optional(), + team: z.string().optional(), + message: z.string().min(1), + metadata: z.record(z.string(), z.any()).optional(), + tags: z.array(z.string()).optional(), +});