This commit is contained in:
2026-02-04 20:29:42 -06:00
parent e37c64fb1d
commit 515ddb24d6
18 changed files with 921 additions and 19 deletions

51
biome.json Normal file
View File

@@ -0,0 +1,51 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"suspicious": {
"noConsole": {
"level":"on",
"options": {
"allow": ["error", "info", "warn"]
}
}
},
"correctness": {
"useJsxKeyInIterable": "error"
}
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"recommended": true,
"organizeImports": "on"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"json": {
"formatter": {
"enabled": false
}
}
}

View File

@@ -1,12 +1,14 @@
import {defineConfig} from 'drizzle-kit'
if(!process.env.DATABASE_URL){
throw new Error('DATABASE_URL is not defined')
if(!process.env.DATABASE_HOST){
throw new Error('Data base info missing ')
}
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
export default defineConfig({
dialect:'postgresql',
schema: "./src/db/schema",
out: './migrations',
dbCredentials: {url: process.env.DATABASE_URL}
dbCredentials: {url: dbURL}
})

View File

@@ -0,0 +1,13 @@
CREATE TYPE "public"."match_status" AS ENUM('scheduled', 'live', 'finished');--> statement-breakpoint
CREATE TABLE "matches" (
"id" serial PRIMARY KEY NOT NULL,
"sport" text NOT NULL,
"home_team" text NOT NULL,
"away_team" text NOT NULL,
"status" "match_status" DEFAULT 'scheduled' NOT NULL,
"start_time" timestamp,
"end_time" timestamp,
"home_score" integer DEFAULT 0 NOT NULL,
"away_score" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);

View File

@@ -0,0 +1,3 @@
CREATE TABLE "commentary" (
"id" serial PRIMARY KEY NOT NULL
);

View File

@@ -0,0 +1,12 @@
ALTER TABLE "commentary" ADD COLUMN "match_id" integer NOT NULL;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "minute" integer;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "sequence" integer;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "period" text;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "event_type" text;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "actor" text;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "team" text;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "message" text NOT NULL;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "metadata" jsonb;--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "tags" text[];--> statement-breakpoint
ALTER TABLE "commentary" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
ALTER TABLE "commentary" ADD CONSTRAINT "commentary_match_id_matches_id_fk" FOREIGN KEY ("match_id") REFERENCES "public"."matches"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1,107 @@
{
"id": "cc319841-30d4-4928-ba5c-fe3352ba1bf8",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.matches": {
"name": "matches",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"sport": {
"name": "sport",
"type": "text",
"primaryKey": false,
"notNull": true
},
"home_team": {
"name": "home_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"away_team": {
"name": "away_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "match_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'scheduled'"
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"end_time": {
"name": "end_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"home_score": {
"name": "home_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"away_score": {
"name": "away_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.match_status": {
"name": "match_status",
"schema": "public",
"values": [
"scheduled",
"live",
"finished"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,126 @@
{
"id": "919e2530-f5a0-4aab-8a61-b4a3bacad982",
"prevId": "cc319841-30d4-4928-ba5c-fe3352ba1bf8",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.commentary": {
"name": "commentary",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.matches": {
"name": "matches",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"sport": {
"name": "sport",
"type": "text",
"primaryKey": false,
"notNull": true
},
"home_team": {
"name": "home_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"away_team": {
"name": "away_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "match_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'scheduled'"
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"end_time": {
"name": "end_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"home_score": {
"name": "home_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"away_score": {
"name": "away_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.match_status": {
"name": "match_status",
"schema": "public",
"values": [
"scheduled",
"live",
"finished"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,207 @@
{
"id": "0e98917e-e04a-4424-aa92-9c66371c9960",
"prevId": "919e2530-f5a0-4aab-8a61-b4a3bacad982",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.commentary": {
"name": "commentary",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"match_id": {
"name": "match_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"minute": {
"name": "minute",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"period": {
"name": "period",
"type": "text",
"primaryKey": false,
"notNull": false
},
"event_type": {
"name": "event_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"actor": {
"name": "actor",
"type": "text",
"primaryKey": false,
"notNull": false
},
"team": {
"name": "team",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"tags": {
"name": "tags",
"type": "text[]",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"commentary_match_id_matches_id_fk": {
"name": "commentary_match_id_matches_id_fk",
"tableFrom": "commentary",
"tableTo": "matches",
"columnsFrom": [
"match_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.matches": {
"name": "matches",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"sport": {
"name": "sport",
"type": "text",
"primaryKey": false,
"notNull": true
},
"home_team": {
"name": "home_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"away_team": {
"name": "away_team",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "match_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'scheduled'"
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"end_time": {
"name": "end_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"home_score": {
"name": "home_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"away_score": {
"name": "away_score",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.match_status": {
"name": "match_status",
"schema": "public",
"values": [
"scheduled",
"live",
"finished"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,27 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1770045471965,
"tag": "0000_slim_shiva",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1770045929269,
"tag": "0001_dark_ben_grimm",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1770060258034,
"tag": "0002_careless_franklin_richards",
"breakpoints": true
}
]
}

190
package-lock.json generated
View File

@@ -13,9 +13,12 @@
"drizzle-orm": "^0.45.1",
"express": "^5.2.1",
"pg": "^8.18.0",
"wscat": "^6.1.0"
"postgres": "^3.4.8",
"wscat": "^6.1.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@types/node": "^25.1.0",
"@types/pg": "^8.16.0",
"@types/ws": "^8.18.1",
@@ -24,6 +27,169 @@
"typescript": "^5.9.3"
}
},
"node_modules/@biomejs/biome": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz",
"integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.3.13",
"@biomejs/cli-darwin-x64": "2.3.13",
"@biomejs/cli-linux-arm64": "2.3.13",
"@biomejs/cli-linux-arm64-musl": "2.3.13",
"@biomejs/cli-linux-x64": "2.3.13",
"@biomejs/cli-linux-x64-musl": "2.3.13",
"@biomejs/cli-win32-arm64": "2.3.13",
"@biomejs/cli-win32-x64": "2.3.13"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz",
"integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz",
"integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz",
"integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz",
"integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz",
"integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz",
"integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz",
"integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz",
"integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@dotenvx/dotenvx": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz",
@@ -2171,6 +2337,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postgres": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz",
"integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==",
"license": "Unlicense",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -3138,6 +3317,15 @@
"engines": {
"node": ">=0.4"
}
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -6,7 +6,10 @@
"type": "module",
"scripts": {
"dev": "dotenvx run -f .env -- node --watch src/index.js",
"start": "dotenvx run -f .env -- node src/index.js"
"start": "dotenvx run -f .env -- node src/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
},
"repository": {
"type": "git",
@@ -20,9 +23,12 @@
"drizzle-orm": "^0.45.1",
"express": "^5.2.1",
"pg": "^8.18.0",
"wscat": "^6.1.0"
"postgres": "^3.4.8",
"wscat": "^6.1.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@types/node": "^25.1.0",
"@types/pg": "^8.16.0",
"@types/ws": "^8.18.1",

View File

@@ -1,12 +1,21 @@
import { drizzle } from "drizzle-orm/node-postgres";
import pg from 'pg'
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
if(!process.env.DATABASE_URL){
throw new Error('DATABASE_URL is not defined')
if(!process.env.DATABASE_HOST){
throw new Error('Data base info missing ')
}
export const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL
})
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
export const db = drizzle(pool)
const queryClient = postgres(dbURL, {
max: 10,
idle_timeout: 60,
connect_timeout: 30,
max_lifetime: 1000 * 6 * 5,
onnotice: (n) => {
console.info("PG notice: ", n.message);
},
});
export const db = drizzle({ client: queryClient })

View File

@@ -0,0 +1,20 @@
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()
})

View File

@@ -1,9 +1,17 @@
import { pgEnum, serial } from "drizzle-orm/pg-core";
import { pgEnum, serial , integer, pgTable, text,timestamp,} from "drizzle-orm/pg-core";
export const matchStatusEnum = pgEnum('match_status',['scheduled','live', 'finished'])
export const matches = pgTable('matches', {
id: serial('id').primaryKey(),
sport: text('sport').noNull(),
homeTeam: text('home_team').notNull()
sport: text('sport').notNull(),
homeTeam: text('home_team').notNull(),
awayTeam: text("away_team").notNull(),
status: matchStatusEnum('status').notNull().default('scheduled'),
startTime: timestamp('start_time'),
endTime: timestamp('end_time'),
homeScore: integer('home_score').notNull().default(0),
awayScore: integer('away_score').notNull().default(0),
createdAt: timestamp('created_at').notNull().defaultNow()
})

View File

@@ -1,17 +1,22 @@
import express from 'express'
// routes
import { matchRouter } from './routes/matches.route.js'
const app = express()
const port = process.env.PORT || 8081
app.use(express.json())
app.get('/',(req,res)=>{
app.get('/',(_,res)=>{
res.send('Hello from express server!')
})
app.use('/matches', matchRouter)
app.listen(port, ()=>{
console.log(`Listening on port ${port}`)
console.info(`Listening on port ${port}`)
})

View File

@@ -0,0 +1,43 @@
import { Router } from "express";
import { createMatchSchema, listMatchesQuerySchema } from "../../validation/matches.js";
import { db } from "../db/db.js";
import { matches } from "../db/schema/matches.js";
import { getMatchStatus } from "../utils/match-status.utlis.js";
export const matchRouter = Router()
matchRouter.get('/', (_,res)=>{
const parsed = listMatchesQuerySchema.safeParse(req.query)
if(!parsed.success){
return res.status(400).json({error: 'Invalid payload', details: JSON.stringify(parsed.error)})
}
return res.status(200).json({message: 'Matches List'})
})
matchRouter.post('/', async (req,res)=>{
const parsed = createMatchSchema.safeParse(req.body)
const {data: {startTime, endTime, homeScore, awayScore}} = parsed
if(!parsed.success){
return res.status(400).json({error: 'Invalid payload', details: JSON.stringify(parsed.error)})
}
try {
const [event] = await db.insert(matches).values({
...parsed.data,
startTime: new Date(startTime),
endTime: new Date(endTime),
homeScore: homeScore ?? 0,
awayScore: awayScore ?? 0,
status: getMatchStatus(startTime, endTime)
}).returning()
res.status(201).json({data: event})
} catch (e) {
return res.status(500).json({error: 'Failed to create match.', details: JSON.stringify(e)})
}
})

View File

@@ -0,0 +1,35 @@
import { MATCH_STATUS } from "../../validation/matches.js"
export const getMatchStatus =(startTime, endTime, now = new Date())=>{
const start = new Date(startTime)
const end = new Date(endTime)
if(Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())){
return null
}
if (now < start){
return MATCH_STATUS.SCHEDULED
}
if (now >= end){
return MATCH_STATUS.FINISHED
}
return MATCH_STATUS.LIVE
}
export const syncMatchStatus = async(match, updateStatus)=>{
const nextStatus = getMatchStatus(match.startTime, match.endTime)
if(!nextStatus){
return match.status
}
if(match.status !== nextStatus){
await updateStatus(nextStatus)
match.status = nextStatus
}
return match.status
}

40
validation/matches.js Normal file
View File

@@ -0,0 +1,40 @@
import { z } from 'zod';
export const MATCH_STATUS = {
SCHEDULED: 'scheduled',
LIVE: 'live',
FINISHED: 'finished',
};
export const listMatchesQuerySchema = z.object({
limit: z.coerce.number().int().positive().max(100).optional(),
});
export const matchIdParamSchema = z.object({
id: z.coerce.number().int().positive(),
});
export const createMatchSchema = z.object({
sport: z.string().min(1),
homeTeam: z.string().min(1),
awayTeam: z.string().min(1),
startTime: z.iso.datetime(),
endTime: z.iso.datetime(),
homeScore: z.coerce.number().int().nonnegative().optional(),
awayScore: z.coerce.number().int().nonnegative().optional(),
}).superRefine((data, ctx) => {
const start = new Date(data.startTime);
const end = new Date(data.endTime);
if (end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "endTime must be chronologically after startTime",
path: ["endTime"],
});
}
});
export const updateScoreSchema = z.object({
homeScore: z.coerce.number().int().nonnegative(),
awayScore: z.coerce.number().int().nonnegative(),
});