From 6b416dce31965b19adbd32e4bdd7f34bb5b6ca88 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Mon, 9 Mar 2026 20:28:04 +0100 Subject: [PATCH] add custom matches --- drizzle/0000_tricky_jane_foster.sql | 237 +++ drizzle/meta/0000_snapshot.json | 1760 +++++++++++++++++ drizzle/meta/_journal.json | 13 + scripts/backfill-personal-team-names.ts | 56 + src/lib/components/Sidebar.svelte | 151 +- src/lib/server/bancho/dm-handler.ts | 74 +- src/lib/server/db/schema.ts | 47 + src/lib/server/match/engine.ts | 4 +- src/lib/server/match/invites.ts | 246 +++ src/lib/server/match/types.ts | 7 + src/lib/server/notifications.ts | 67 + src/routes/(app)/+layout.server.ts | 31 +- src/routes/(app)/matches/+page.server.ts | 88 +- src/routes/(app)/matches/+page.svelte | 283 ++- src/routes/(app)/teams/+page.svelte | 11 +- src/routes/(app)/teams/[id]/+page.server.ts | 56 + src/routes/(app)/teams/[id]/+page.svelte | 94 + src/routes/api/invites/+server.ts | 36 + src/routes/api/invites/[id]/accept/+server.ts | 14 + src/routes/api/invites/[id]/cancel/+server.ts | 14 + .../api/invites/[id]/decline/+server.ts | 14 + src/routes/api/notifications/+server.ts | 13 + src/routes/api/notifications/count/+server.ts | 9 + src/routes/api/notifications/read/+server.ts | 19 + 24 files changed, 3289 insertions(+), 55 deletions(-) create mode 100644 drizzle/0000_tricky_jane_foster.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 scripts/backfill-personal-team-names.ts create mode 100644 src/lib/server/match/invites.ts create mode 100644 src/lib/server/notifications.ts create mode 100644 src/routes/(app)/teams/[id]/+page.server.ts create mode 100644 src/routes/(app)/teams/[id]/+page.svelte create mode 100644 src/routes/api/invites/+server.ts create mode 100644 src/routes/api/invites/[id]/accept/+server.ts create mode 100644 src/routes/api/invites/[id]/cancel/+server.ts create mode 100644 src/routes/api/invites/[id]/decline/+server.ts create mode 100644 src/routes/api/notifications/+server.ts create mode 100644 src/routes/api/notifications/count/+server.ts create mode 100644 src/routes/api/notifications/read/+server.ts diff --git a/drizzle/0000_tricky_jane_foster.sql b/drizzle/0000_tricky_jane_foster.sql new file mode 100644 index 0000000..5d073ce --- /dev/null +++ b/drizzle/0000_tricky_jane_foster.sql @@ -0,0 +1,237 @@ +CREATE TABLE "mappool" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "created_by" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "verified_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "mappool_slot" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "mappool_id" uuid NOT NULL, + "category" text NOT NULL, + "order_in_category" integer NOT NULL, + "beatmap_id" text NOT NULL, + "star_rating" real, + "bpm" real, + "total_length" integer, + "mods" text[] DEFAULT '{}' NOT NULL, + "title" text, + "artist" text, + "version" text, + "cover_url" text, + "list_cover_url" text +); +--> statement-breakpoint +CREATE TABLE "match" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text, + "state" text DEFAULT 'CREATED' NOT NULL, + "config" jsonb NOT NULL, + "mappool_id" uuid, + "osu_lobby_id" integer, + "scheduled_at" timestamp with time zone, + "started_at" timestamp with time zone, + "finished_at" timestamp with time zone, + "winner_id" uuid, + "created_by" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "match_game" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "match_id" uuid NOT NULL, + "game_number" integer NOT NULL, + "mappool_slot_id" uuid NOT NULL, + "picked_by_participant_id" uuid, + "winner_participant_id" uuid, + "state" text DEFAULT 'PENDING' NOT NULL, + "started_at" timestamp with time zone, + "finished_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "match_game_score" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "match_game_id" uuid NOT NULL, + "player_id" uuid NOT NULL, + "score" integer DEFAULT 0 NOT NULL, + "accuracy" real DEFAULT 0 NOT NULL, + "max_combo" integer DEFAULT 0 NOT NULL, + "count_300" integer DEFAULT 0 NOT NULL, + "count_100" integer DEFAULT 0 NOT NULL, + "count_50" integer DEFAULT 0 NOT NULL, + "count_miss" integer DEFAULT 0 NOT NULL, + "mods" text[] DEFAULT '{}' NOT NULL, + "passed" boolean DEFAULT false NOT NULL, + "pp" real +); +--> statement-breakpoint +CREATE TABLE "match_invite" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_by" text NOT NULL, + "creator_team_id" uuid NOT NULL, + "invited_team_id" uuid NOT NULL, + "config" jsonb NOT NULL, + "mappool_id" uuid NOT NULL, + "name" text, + "message" text, + "scheduled_at" timestamp with time zone, + "expires_at" timestamp with time zone NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "match_id" uuid, + "responded_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "match_participant" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "match_id" uuid NOT NULL, + "team_id" uuid NOT NULL, + "slot" integer NOT NULL, + "score" integer DEFAULT 0 NOT NULL, + "roll_value" integer, + "pick_order" integer +); +--> statement-breakpoint +CREATE TABLE "match_participant_player" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "participant_id" uuid NOT NULL, + "user_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "match_queue" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "team_id" uuid NOT NULL, + "elo" integer DEFAULT 1000 NOT NULL, + "joined_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notification" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "type" text NOT NULL, + "title" text NOT NULL, + "message" text, + "reference_id" text, + "read" boolean DEFAULT false NOT NULL, + "actioned_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "player_rating" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "elo" integer DEFAULT 1000 NOT NULL, + "wins" integer DEFAULT 0 NOT NULL, + "losses" integer DEFAULT 0 NOT NULL, + "initial_elo" integer, + "osu_rank_at_seed" integer, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "player_rating_user_id_unique" UNIQUE("user_id") +); +--> statement-breakpoint +CREATE TABLE "team" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "is_personal" boolean DEFAULT false NOT NULL, + "owner_id" text, + "avatar_url" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "team_member" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "team_id" uuid NOT NULL, + "user_id" text NOT NULL, + "role" text DEFAULT 'member', + "joined_at" timestamp with time zone DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "role" text DEFAULT 'player' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "mappool_slot" ADD CONSTRAINT "mappool_slot_mappool_id_mappool_id_fk" FOREIGN KEY ("mappool_id") REFERENCES "public"."mappool"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match" ADD CONSTRAINT "match_mappool_id_mappool_id_fk" FOREIGN KEY ("mappool_id") REFERENCES "public"."mappool"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match" ADD CONSTRAINT "match_winner_id_team_id_fk" FOREIGN KEY ("winner_id") REFERENCES "public"."team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game" ADD CONSTRAINT "match_game_match_id_match_id_fk" FOREIGN KEY ("match_id") REFERENCES "public"."match"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game" ADD CONSTRAINT "match_game_mappool_slot_id_mappool_slot_id_fk" FOREIGN KEY ("mappool_slot_id") REFERENCES "public"."mappool_slot"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game" ADD CONSTRAINT "match_game_picked_by_participant_id_match_participant_id_fk" FOREIGN KEY ("picked_by_participant_id") REFERENCES "public"."match_participant"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game" ADD CONSTRAINT "match_game_winner_participant_id_match_participant_id_fk" FOREIGN KEY ("winner_participant_id") REFERENCES "public"."match_participant"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game_score" ADD CONSTRAINT "match_game_score_match_game_id_match_game_id_fk" FOREIGN KEY ("match_game_id") REFERENCES "public"."match_game"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_game_score" ADD CONSTRAINT "match_game_score_player_id_match_participant_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."match_participant_player"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_invite" ADD CONSTRAINT "match_invite_creator_team_id_team_id_fk" FOREIGN KEY ("creator_team_id") REFERENCES "public"."team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_invite" ADD CONSTRAINT "match_invite_invited_team_id_team_id_fk" FOREIGN KEY ("invited_team_id") REFERENCES "public"."team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_invite" ADD CONSTRAINT "match_invite_mappool_id_mappool_id_fk" FOREIGN KEY ("mappool_id") REFERENCES "public"."mappool"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_invite" ADD CONSTRAINT "match_invite_match_id_match_id_fk" FOREIGN KEY ("match_id") REFERENCES "public"."match"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_participant" ADD CONSTRAINT "match_participant_match_id_match_id_fk" FOREIGN KEY ("match_id") REFERENCES "public"."match"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_participant" ADD CONSTRAINT "match_participant_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_participant_player" ADD CONSTRAINT "match_participant_player_participant_id_match_participant_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."match_participant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "match_queue" ADD CONSTRAINT "match_queue_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "team_member" ADD CONSTRAINT "team_member_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_match_state" ON "match" USING btree ("state");--> statement-breakpoint +CREATE INDEX "idx_match_game_match" ON "match_game" USING btree ("match_id");--> statement-breakpoint +CREATE INDEX "idx_mgs_game" ON "match_game_score" USING btree ("match_game_id");--> statement-breakpoint +CREATE INDEX "idx_invite_invited_team" ON "match_invite" USING btree ("invited_team_id");--> statement-breakpoint +CREATE INDEX "idx_invite_created_by" ON "match_invite" USING btree ("created_by");--> statement-breakpoint +CREATE INDEX "idx_invite_status" ON "match_invite" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_mp_match_team" ON "match_participant" USING btree ("match_id","team_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_mp_match_slot" ON "match_participant" USING btree ("match_id","slot");--> statement-breakpoint +CREATE INDEX "idx_mp_match" ON "match_participant" USING btree ("match_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_mpp_participant_user" ON "match_participant_player" USING btree ("participant_id","user_id");--> statement-breakpoint +CREATE INDEX "idx_mpp_participant" ON "match_participant_player" USING btree ("participant_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_queue_user" ON "match_queue" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_queue_elo" ON "match_queue" USING btree ("elo");--> statement-breakpoint +CREATE INDEX "idx_notification_user" ON "notification" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_notification_user_read" ON "notification" USING btree ("user_id","read");--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..71ac5cf --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1760 @@ +{ + "id": "2a33222f-3ef2-41cd-9454-eacae15729f6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mappool": { + "name": "mappool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mappool_slot": { + "name": "mappool_slot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mappool_id": { + "name": "mappool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_in_category": { + "name": "order_in_category", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "beatmap_id": { + "name": "beatmap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "star_rating": { + "name": "star_rating", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bpm": { + "name": "bpm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_length": { + "name": "total_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mods": { + "name": "mods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "list_cover_url": { + "name": "list_cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mappool_slot_mappool_id_mappool_id_fk": { + "name": "mappool_slot_mappool_id_mappool_id_fk", + "tableFrom": "mappool_slot", + "tableTo": "mappool", + "columnsFrom": [ + "mappool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match": { + "name": "match", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "mappool_id": { + "name": "mappool_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "osu_lobby_id": { + "name": "osu_lobby_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_match_state": { + "name": "idx_match_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_mappool_id_mappool_id_fk": { + "name": "match_mappool_id_mappool_id_fk", + "tableFrom": "match", + "tableTo": "mappool", + "columnsFrom": [ + "mappool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_winner_id_team_id_fk": { + "name": "match_winner_id_team_id_fk", + "tableFrom": "match", + "tableTo": "team", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_game": { + "name": "match_game", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mappool_slot_id": { + "name": "mappool_slot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "picked_by_participant_id": { + "name": "picked_by_participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_participant_id": { + "name": "winner_participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_match_game_match": { + "name": "idx_match_game_match", + "columns": [ + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_game_match_id_match_id_fk": { + "name": "match_game_match_id_match_id_fk", + "tableFrom": "match_game", + "tableTo": "match", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "match_game_mappool_slot_id_mappool_slot_id_fk": { + "name": "match_game_mappool_slot_id_mappool_slot_id_fk", + "tableFrom": "match_game", + "tableTo": "mappool_slot", + "columnsFrom": [ + "mappool_slot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_game_picked_by_participant_id_match_participant_id_fk": { + "name": "match_game_picked_by_participant_id_match_participant_id_fk", + "tableFrom": "match_game", + "tableTo": "match_participant", + "columnsFrom": [ + "picked_by_participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_game_winner_participant_id_match_participant_id_fk": { + "name": "match_game_winner_participant_id_match_participant_id_fk", + "tableFrom": "match_game", + "tableTo": "match_participant", + "columnsFrom": [ + "winner_participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_game_score": { + "name": "match_game_score", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "match_game_id": { + "name": "match_game_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "accuracy": { + "name": "accuracy", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_combo": { + "name": "max_combo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "count_300": { + "name": "count_300", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "count_100": { + "name": "count_100", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "count_50": { + "name": "count_50", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "count_miss": { + "name": "count_miss", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "mods": { + "name": "mods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pp": { + "name": "pp", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_mgs_game": { + "name": "idx_mgs_game", + "columns": [ + { + "expression": "match_game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_game_score_match_game_id_match_game_id_fk": { + "name": "match_game_score_match_game_id_match_game_id_fk", + "tableFrom": "match_game_score", + "tableTo": "match_game", + "columnsFrom": [ + "match_game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "match_game_score_player_id_match_participant_player_id_fk": { + "name": "match_game_score_player_id_match_participant_player_id_fk", + "tableFrom": "match_game_score", + "tableTo": "match_participant_player", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_invite": { + "name": "match_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_team_id": { + "name": "creator_team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_team_id": { + "name": "invited_team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "mappool_id": { + "name": "mappool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invite_invited_team": { + "name": "idx_invite_invited_team", + "columns": [ + { + "expression": "invited_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invite_created_by": { + "name": "idx_invite_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invite_status": { + "name": "idx_invite_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_invite_creator_team_id_team_id_fk": { + "name": "match_invite_creator_team_id_team_id_fk", + "tableFrom": "match_invite", + "tableTo": "team", + "columnsFrom": [ + "creator_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_invite_invited_team_id_team_id_fk": { + "name": "match_invite_invited_team_id_team_id_fk", + "tableFrom": "match_invite", + "tableTo": "team", + "columnsFrom": [ + "invited_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_invite_mappool_id_mappool_id_fk": { + "name": "match_invite_mappool_id_mappool_id_fk", + "tableFrom": "match_invite", + "tableTo": "mappool", + "columnsFrom": [ + "mappool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "match_invite_match_id_match_id_fk": { + "name": "match_invite_match_id_match_id_fk", + "tableFrom": "match_invite", + "tableTo": "match", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_participant": { + "name": "match_participant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slot": { + "name": "slot", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "roll_value": { + "name": "roll_value", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pick_order": { + "name": "pick_order", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_mp_match_team": { + "name": "idx_mp_match_team", + "columns": [ + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_mp_match_slot": { + "name": "idx_mp_match_slot", + "columns": [ + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_mp_match": { + "name": "idx_mp_match", + "columns": [ + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_participant_match_id_match_id_fk": { + "name": "match_participant_match_id_match_id_fk", + "tableFrom": "match_participant", + "tableTo": "match", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "match_participant_team_id_team_id_fk": { + "name": "match_participant_team_id_team_id_fk", + "tableFrom": "match_participant", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_participant_player": { + "name": "match_participant_player", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_mpp_participant_user": { + "name": "idx_mpp_participant_user", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_mpp_participant": { + "name": "idx_mpp_participant", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_participant_player_participant_id_match_participant_id_fk": { + "name": "match_participant_player_participant_id_match_participant_id_fk", + "tableFrom": "match_participant_player", + "tableTo": "match_participant", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_queue": { + "name": "match_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elo": { + "name": "elo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1000 + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_queue_user": { + "name": "idx_queue_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_queue_elo": { + "name": "idx_queue_elo", + "columns": [ + { + "expression": "elo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_queue_team_id_team_id_fk": { + "name": "match_queue_team_id_team_id_fk", + "tableFrom": "match_queue", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "actioned_at": { + "name": "actioned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_notification_user": { + "name": "idx_notification_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_user_read": { + "name": "idx_notification_user_read", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_rating": { + "name": "player_rating", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "elo": { + "name": "elo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1000 + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "initial_elo": { + "name": "initial_elo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "osu_rank_at_seed": { + "name": "osu_rank_at_seed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "player_rating_user_id_unique": { + "name": "player_rating_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_personal": { + "name": "is_personal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'player'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..2e7ae40 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1773082177642, + "tag": "0000_tricky_jane_foster", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/scripts/backfill-personal-team-names.ts b/scripts/backfill-personal-team-names.ts new file mode 100644 index 0000000..bcd6243 --- /dev/null +++ b/scripts/backfill-personal-team-names.ts @@ -0,0 +1,56 @@ +/** + * Backfill personal team names to match the player's current username. + * Removes any " (personal team)" suffix or other naming inconsistencies. + * + * Usage: bun scripts/backfill-personal-team-names.ts + */ + +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { eq, and } from 'drizzle-orm'; +import { pgTable, text, uuid, boolean, timestamp } from 'drizzle-orm/pg-core'; + +const DATABASE_URL = process.env.DATABASE_URL; +if (!DATABASE_URL) throw new Error('DATABASE_URL not set'); + +const client = postgres(DATABASE_URL); +const db = drizzle(client); + +const team = pgTable('team', { + id: uuid('id').primaryKey(), + name: text('name').notNull(), + isPersonal: boolean('is_personal').notNull(), + ownerId: text('owner_id') +}); + +const user = pgTable('user', { + id: text('id').primaryKey(), + name: text('name').notNull() +}); + +async function main() { + const personalTeams = await db.select().from(team).where(eq(team.isPersonal, true)); + console.log(`Found ${personalTeams.length} personal teams`); + + let updated = 0; + for (const t of personalTeams) { + if (!t.ownerId) continue; + + const [owner] = await db.select().from(user).where(eq(user.id, t.ownerId)); + if (!owner) continue; + + if (t.name !== owner.name) { + await db.update(team).set({ name: owner.name }).where(eq(team.id, t.id)); + console.log(` "${t.name}" -> "${owner.name}"`); + updated++; + } + } + + console.log(`Updated ${updated} personal team names`); + process.exit(0); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index eff6861..d81ebfc 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -8,8 +8,12 @@ const user = $derived(page.data?.user); const activeMatch = $derived(page.data?.activeMatch); + const unreadCount = $derived((page.data as any)?.unreadNotificationCount ?? 0); let { open = $bindable(false) } = $props(); + let notifOpen = $state(false); + let notifications = $state([]); + let loadingNotifs = $state(false); function isActive(href: string) { if (href === '/') return page.url.pathname === '/'; @@ -22,7 +26,66 @@ $effect(() => { page.url.pathname; open = false; + notifOpen = false; }); + + async function toggleNotifications() { + notifOpen = !notifOpen; + if (notifOpen) { + await loadNotifications(); + } + } + + async function loadNotifications() { + loadingNotifs = true; + try { + const res = await fetch('/api/notifications'); + if (res.ok) notifications = await res.json(); + } finally { + loadingNotifs = false; + } + } + + async function handleAcceptInvite(referenceId: string) { + const res = await fetch(`/api/invites/${referenceId}/accept`, { method: 'POST' }); + if (res.ok) { + const match = await res.json(); + notifications = notifications.map((n) => + n.referenceId === referenceId ? { ...n, actionedAt: new Date().toISOString(), read: true } : n + ); + window.location.href = `/matches/${match.id}`; + } + } + + async function handleDeclineInvite(referenceId: string) { + const res = await fetch(`/api/invites/${referenceId}/decline`, { method: 'POST' }); + if (res.ok) { + notifications = notifications.map((n) => + n.referenceId === referenceId ? { ...n, actionedAt: new Date().toISOString(), read: true } : n + ); + } + } + + async function markAllRead() { + await fetch('/api/notifications/read', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ all: true }) + }); + notifications = notifications.map((n) => ({ ...n, read: true })); + } + + function timeAgo(date: string | Date) { + const d = new Date(date); + const diff = Date.now() - d.getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; + } {#snippet navIcon(name: string)} @@ -57,15 +120,87 @@ ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 lg:z-40" > - - - Vash Esports - Vash Esports - + +
+ + Vash Esports + Vash Esports + + {#if user} +
+ + + {#if notifOpen} + + + + +
+
+ Notifications + {#if notifications.some((n) => !n.read)} + + {/if} +
+
+ {#if loadingNotifs} +
Loading...
+ {:else if notifications.length === 0} +
No notifications
+ {:else} + {#each notifications as n} +
+
+ {#if !n.read} + + {/if} +
+

{n.title}

+ {#if n.message} +

{n.message}

+ {/if} +

{timeAgo(n.createdAt)}

+ + {#if n.type === 'match_invite' && !n.actionedAt && n.referenceId} +
+ + +
+ {/if} +
+
+
+ {/each} + {/if} +
+
+ {/if} +
+ {/if} +
- {#if activeMatch} + {#if activeMatch && page.url.pathname !== `/matches/${activeMatch.id}`}
- {:else} + {:else if !activeMatch}