From 515ddb24d6b907803de2edf8a5de1b4226813a61 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 4 Feb 2026 20:29:42 -0600 Subject: [PATCH] sync --- biome.json | 51 +++++ drizzle.config.js | 8 +- migrations/0000_slim_shiva.sql | 13 ++ migrations/0001_dark_ben_grimm.sql | 3 + .../0002_careless_franklin_richards.sql | 12 + migrations/meta/0000_snapshot.json | 107 +++++++++ migrations/meta/0001_snapshot.json | 126 +++++++++++ migrations/meta/0002_snapshot.json | 207 ++++++++++++++++++ migrations/meta/_journal.json | 27 +++ package-lock.json | 190 +++++++++++++++- package.json | 10 +- src/db/db.js | 25 ++- src/db/schema/commentary.js | 20 ++ src/db/schema/matches.js | 14 +- src/index.js | 9 +- src/routes/matches.route.js | 43 ++++ src/utils/match-status.utlis.js | 35 +++ validation/matches.js | 40 ++++ 18 files changed, 921 insertions(+), 19 deletions(-) create mode 100644 biome.json create mode 100644 migrations/0000_slim_shiva.sql create mode 100644 migrations/0001_dark_ben_grimm.sql create mode 100644 migrations/0002_careless_franklin_richards.sql create mode 100644 migrations/meta/0000_snapshot.json create mode 100644 migrations/meta/0001_snapshot.json create mode 100644 migrations/meta/0002_snapshot.json create mode 100644 migrations/meta/_journal.json create mode 100644 src/db/schema/commentary.js create mode 100644 src/routes/matches.route.js create mode 100644 src/utils/match-status.utlis.js create mode 100644 validation/matches.js diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..9e4dcdf --- /dev/null +++ b/biome.json @@ -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 + } + } +} diff --git a/drizzle.config.js b/drizzle.config.js index 94cf8ca..8ec3bf5 100644 --- a/drizzle.config.js +++ b/drizzle.config.js @@ -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} }) diff --git a/migrations/0000_slim_shiva.sql b/migrations/0000_slim_shiva.sql new file mode 100644 index 0000000..6e5e701 --- /dev/null +++ b/migrations/0000_slim_shiva.sql @@ -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 +); diff --git a/migrations/0001_dark_ben_grimm.sql b/migrations/0001_dark_ben_grimm.sql new file mode 100644 index 0000000..725b5ca --- /dev/null +++ b/migrations/0001_dark_ben_grimm.sql @@ -0,0 +1,3 @@ +CREATE TABLE "commentary" ( + "id" serial PRIMARY KEY NOT NULL +); diff --git a/migrations/0002_careless_franklin_richards.sql b/migrations/0002_careless_franklin_richards.sql new file mode 100644 index 0000000..f0fa304 --- /dev/null +++ b/migrations/0002_careless_franklin_richards.sql @@ -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; \ No newline at end of file diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..e8bdfdc --- /dev/null +++ b/migrations/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..a8de147 --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..f545183 --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json new file mode 100644 index 0000000..0d1926e --- /dev/null +++ b/migrations/meta/_journal.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5d6fe26..7c4dbfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 565d0e0..500a66e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/db/db.js b/src/db/db.js index eef1ba1..c2d1f27 100644 --- a/src/db/db.js +++ b/src/db/db.js @@ -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) \ No newline at end of file +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 }) \ No newline at end of file diff --git a/src/db/schema/commentary.js b/src/db/schema/commentary.js new file mode 100644 index 0000000..1c84fcb --- /dev/null +++ b/src/db/schema/commentary.js @@ -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() + }) \ No newline at end of file diff --git a/src/db/schema/matches.js b/src/db/schema/matches.js index 3542ae1..822fc4e 100644 --- a/src/db/schema/matches.js +++ b/src/db/schema/matches.js @@ -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() }) \ No newline at end of file diff --git a/src/index.js b/src/index.js index c902929..54a7a90 100644 --- a/src/index.js +++ b/src/index.js @@ -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}`) }) diff --git a/src/routes/matches.route.js b/src/routes/matches.route.js new file mode 100644 index 0000000..59c10ba --- /dev/null +++ b/src/routes/matches.route.js @@ -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)}) + } +}) \ No newline at end of file diff --git a/src/utils/match-status.utlis.js b/src/utils/match-status.utlis.js new file mode 100644 index 0000000..8760066 --- /dev/null +++ b/src/utils/match-status.utlis.js @@ -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 +} \ No newline at end of file diff --git a/validation/matches.js b/validation/matches.js new file mode 100644 index 0000000..a057f9e --- /dev/null +++ b/validation/matches.js @@ -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(), +}); \ No newline at end of file