diff --git a/packages/core/migrations/0000_chunky_pet_avengers.sql b/packages/core/migrations/0000_chunky_pet_avengers.sql new file mode 100644 index 00000000..b346c8b6 --- /dev/null +++ b/packages/core/migrations/0000_chunky_pet_avengers.sql @@ -0,0 +1,488 @@ +CREATE TYPE "public"."status" AS ENUM('active', 'completed', 'upcoming');--> 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 (3), + "refresh_token_expires_at" timestamp (3), + "scope" text, + "password" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp (3) NOT NULL, + "token" text NOT NULL, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3) NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + "active_organization_id" text, + "active_team_id" text, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp (3) NOT NULL, + "created_at" timestamp (3), + "updated_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "feedback" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "topic" text NOT NULL, + "rating" text NOT NULL, + "feedback" text NOT NULL, + "user_id" text, + "user_email" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "feedback_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "game" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "organization_id" text NOT NULL, + "team_id" text NOT NULL, + "seasonId" integer NOT NULL, + "opponent_name" text NOT NULL, + "opponent_team_id" text, + "game_date" timestamp (3) NOT NULL, + "venue" text NOT NULL, + "is_home_game" boolean NOT NULL, + "game_type" text DEFAULT 'regular' NOT NULL, + "status" text DEFAULT 'scheduled' NOT NULL, + "home_score" integer DEFAULT 0, + "away_score" integer DEFAULT 0, + "notes" text, + "location" text, + "uniform_color" text, + "arrival_time" timestamp (3), + "opponent_logo_url" text, + "external_game_id" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "game_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "email" text NOT NULL, + "role" text, + "team_id" text, + "status" text DEFAULT 'pending' NOT NULL, + "expires_at" timestamp (3) NOT NULL, + "inviter_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text DEFAULT 'member' NOT NULL, + "created_at" timestamp (3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "logo" text, + "created_at" timestamp (3) NOT NULL, + "metadata" text, + CONSTRAINT "organization_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "player_contact_info" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "player_id" integer NOT NULL, + "email" text, + "phone" text, + "facebook" text, + "instagram" text, + "whatsapp" text, + "linkedin" text, + "groupme" text, + "emergency_contact_name" text, + "emergency_contact_phone" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "player_contact_info_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "player_contact_info_player_id_unique" UNIQUE("player_id") +); +--> statement-breakpoint +CREATE TABLE "player" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "organization_id" text NOT NULL, + "user_id" text, + "name" text, + "email" text, + "phone" text, + "date_of_birth" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "player_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "team_player" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "team_id" text NOT NULL, + "player_id" integer NOT NULL, + "jersey_number" integer, + "position" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "team_player_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "pro_data_ingestion" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "league_id" integer NOT NULL, + "season_id" integer, + "entity_type" varchar(50) NOT NULL, + "source_url" text, + "source_type" varchar(20), + "status" varchar(20) DEFAULT 'pending' NOT NULL, + "started_at" timestamp (3), + "completed_at" timestamp (3), + "records_processed" integer DEFAULT 0, + "records_created" integer DEFAULT 0, + "records_updated" integer DEFAULT 0, + "records_skipped" integer DEFAULT 0, + "duration_ms" integer, + "error_message" text, + "error_stack" text, + "raw_data_url" text, + "manifest_version" integer, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_data_ingestion_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "pro_game" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "season_id" integer NOT NULL, + "external_id" text, + "home_team_id" integer NOT NULL, + "away_team_id" integer NOT NULL, + "game_date" timestamp (3) NOT NULL, + "week" varchar(20), + "game_number" integer, + "venue" text, + "venue_city" text, + "status" varchar(20) DEFAULT 'scheduled' NOT NULL, + "home_score" integer, + "away_score" integer, + "is_overtime" boolean DEFAULT false, + "overtime_periods" integer DEFAULT 0, + "play_by_play_url" text, + "home_team_stats" jsonb, + "away_team_stats" jsonb, + "broadcaster" text, + "stream_url" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_game_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_game_season_external" UNIQUE("season_id","external_id") +); +--> statement-breakpoint +CREATE TABLE "pro_league" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "code" varchar(10) NOT NULL, + "name" text NOT NULL, + "short_name" text, + "country" varchar(2), + "is_active" boolean DEFAULT true NOT NULL, + "founded_year" integer, + "defunct_year" integer, + "website_url" text, + "logo_url" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_league_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_league_code_unique" UNIQUE("code") +); +--> statement-breakpoint +CREATE TABLE "pro_player_season" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "player_id" integer NOT NULL, + "season_id" integer NOT NULL, + "team_id" integer, + "jersey_number" integer, + "position" varchar(20), + "is_captain" boolean DEFAULT false, + "stats" jsonb, + "post_season_stats" jsonb, + "goalie_stats" jsonb, + "post_season_goalie_stats" jsonb, + "games_played" integer DEFAULT 0, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_player_season_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_player_season_unique" UNIQUE("player_id","season_id") +); +--> statement-breakpoint +CREATE TABLE "pro_player" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "league_id" integer NOT NULL, + "external_id" text NOT NULL, + "canonical_player_id" integer, + "first_name" text, + "last_name" text NOT NULL, + "full_name" text, + "position" varchar(20), + "date_of_birth" date, + "birthplace" text, + "country" varchar(2), + "height" varchar(10), + "weight" integer, + "handedness" varchar(5), + "college" text, + "high_school" text, + "profile_url" text, + "photo_url" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_player_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_player_league_external" UNIQUE("league_id","external_id") +); +--> statement-breakpoint +CREATE TABLE "pro_season" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "league_id" integer NOT NULL, + "external_id" text NOT NULL, + "year" integer NOT NULL, + "display_name" text NOT NULL, + "start_date" date, + "end_date" date, + "is_current" boolean DEFAULT false NOT NULL, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_season_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_season_league_external" UNIQUE("league_id","external_id") +); +--> statement-breakpoint +CREATE TABLE "pro_standings" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "season_id" integer NOT NULL, + "team_id" integer NOT NULL, + "snapshot_date" date NOT NULL, + "position" integer NOT NULL, + "wins" integer DEFAULT 0 NOT NULL, + "losses" integer DEFAULT 0 NOT NULL, + "ties" integer DEFAULT 0, + "overtime_losses" integer DEFAULT 0, + "points" integer, + "win_percentage" integer, + "goals_for" integer DEFAULT 0, + "goals_against" integer DEFAULT 0, + "goal_differential" integer DEFAULT 0, + "conference" varchar(50), + "division" varchar(50), + "clinch_status" varchar(10), + "seed" integer, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_standings_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_standings_season_team_date" UNIQUE("season_id","team_id","snapshot_date") +); +--> statement-breakpoint +CREATE TABLE "pro_team" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "league_id" integer NOT NULL, + "external_id" text NOT NULL, + "code" varchar(10), + "name" text NOT NULL, + "short_name" text, + "city" text, + "logo_url" text, + "primary_color" varchar(7), + "secondary_color" varchar(7), + "website_url" text, + "is_active" boolean DEFAULT true NOT NULL, + "first_season_year" integer, + "last_season_year" integer, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "pro_team_public_id_unique" UNIQUE("public_id"), + CONSTRAINT "pro_team_league_external" UNIQUE("league_id","external_id") +); +--> statement-breakpoint +CREATE TABLE "season" ( + "id" serial PRIMARY KEY NOT NULL, + "public_id" varchar(12) NOT NULL, + "organization_id" text NOT NULL, + "team_id" text NOT NULL, + "name" text NOT NULL, + "start_date" timestamp (3) NOT NULL, + "end_date" timestamp (3), + "status" "status" DEFAULT 'active' NOT NULL, + "division" text, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "season_public_id_unique" UNIQUE("public_id") +); +--> statement-breakpoint +CREATE TABLE "team_member" ( + "id" text PRIMARY KEY NOT NULL, + "team_id" text NOT NULL, + "user_id" text NOT NULL, + "created_at" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "team" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "organization_id" text NOT NULL, + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + CONSTRAINT "team_name_unique" UNIQUE("name") +); +--> 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, + "banned" boolean DEFAULT false, + "ban_reason" text, + "ban_expires" timestamp (3), + "created_at" timestamp (3) NOT NULL, + "updated_at" timestamp (3), + "deleted_at" timestamp (3), + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> 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 +ALTER TABLE "game" ADD CONSTRAINT "game_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "game" ADD CONSTRAINT "game_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "game" ADD CONSTRAINT "game_seasonId_season_id_fk" FOREIGN KEY ("seasonId") REFERENCES "public"."season"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "player_contact_info" ADD CONSTRAINT "player_contact_info_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."player"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "player" ADD CONSTRAINT "player_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "player" ADD CONSTRAINT "player_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "team_player" ADD CONSTRAINT "team_player_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "team_player" ADD CONSTRAINT "team_player_player_id_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."player"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_data_ingestion" ADD CONSTRAINT "pro_data_ingestion_league_id_pro_league_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."pro_league"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_data_ingestion" ADD CONSTRAINT "pro_data_ingestion_season_id_pro_season_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."pro_season"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_game" ADD CONSTRAINT "pro_game_season_id_pro_season_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."pro_season"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_game" ADD CONSTRAINT "pro_game_home_team_id_pro_team_id_fk" FOREIGN KEY ("home_team_id") REFERENCES "public"."pro_team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_game" ADD CONSTRAINT "pro_game_away_team_id_pro_team_id_fk" FOREIGN KEY ("away_team_id") REFERENCES "public"."pro_team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_player_season" ADD CONSTRAINT "pro_player_season_player_id_pro_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."pro_player"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_player_season" ADD CONSTRAINT "pro_player_season_season_id_pro_season_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."pro_season"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_player_season" ADD CONSTRAINT "pro_player_season_team_id_pro_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."pro_team"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_player" ADD CONSTRAINT "pro_player_league_id_pro_league_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."pro_league"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_player" ADD CONSTRAINT "pro_player_canonical_player_id_pro_player_id_fk" FOREIGN KEY ("canonical_player_id") REFERENCES "public"."pro_player"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_season" ADD CONSTRAINT "pro_season_league_id_pro_league_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."pro_league"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_standings" ADD CONSTRAINT "pro_standings_season_id_pro_season_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."pro_season"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_standings" ADD CONSTRAINT "pro_standings_team_id_pro_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."pro_team"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pro_team" ADD CONSTRAINT "pro_team_league_id_pro_league_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."pro_league"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "season" ADD CONSTRAINT "season_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "season" ADD CONSTRAINT "season_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade 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 "team_member" ADD CONSTRAINT "team_member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "team" ADD CONSTRAINT "team_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_user_id_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_user_id_idx" ON "session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_token_idx" ON "session" USING btree ("token");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");--> statement-breakpoint +CREATE INDEX "idx_game_organization" ON "game" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "idx_game_team" ON "game" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "idx_game_date" ON "game" USING btree ("game_date");--> statement-breakpoint +CREATE INDEX "idx_game_status" ON "game" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_game_team_date" ON "game" USING btree ("team_id","game_date");--> statement-breakpoint +CREATE INDEX "invitation_organization_id_idx" ON "invitation" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "invitation_email_idx" ON "invitation" USING btree ("email");--> statement-breakpoint +CREATE INDEX "member_organization_id_idx" ON "member" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "member_user_id_idx" ON "member" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "organization_slug_idx" ON "organization" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "idx_player_contact_info_player" ON "player_contact_info" USING btree ("player_id");--> statement-breakpoint +CREATE INDEX "idx_player_organization" ON "player" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "idx_player_name" ON "player" USING btree ("name");--> statement-breakpoint +CREATE INDEX "idx_player_email" ON "player" USING btree ("email");--> statement-breakpoint +CREATE INDEX "idx_team_player_team" ON "team_player" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "idx_team_player_player" ON "team_player" USING btree ("player_id");--> statement-breakpoint +CREATE INDEX "idx_team_player_unique" ON "team_player" USING btree ("team_id","player_id");--> statement-breakpoint +CREATE INDEX "idx_pro_ingestion_league" ON "pro_data_ingestion" USING btree ("league_id");--> statement-breakpoint +CREATE INDEX "idx_pro_ingestion_season" ON "pro_data_ingestion" USING btree ("season_id");--> statement-breakpoint +CREATE INDEX "idx_pro_ingestion_entity" ON "pro_data_ingestion" USING btree ("entity_type");--> statement-breakpoint +CREATE INDEX "idx_pro_ingestion_status" ON "pro_data_ingestion" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_pro_ingestion_created" ON "pro_data_ingestion" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idx_pro_game_season" ON "pro_game" USING btree ("season_id");--> statement-breakpoint +CREATE INDEX "idx_pro_game_home_team" ON "pro_game" USING btree ("home_team_id");--> statement-breakpoint +CREATE INDEX "idx_pro_game_away_team" ON "pro_game" USING btree ("away_team_id");--> statement-breakpoint +CREATE INDEX "idx_pro_game_date" ON "pro_game" USING btree ("game_date");--> statement-breakpoint +CREATE INDEX "idx_pro_game_status" ON "pro_game" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_pro_league_code" ON "pro_league" USING btree ("code");--> statement-breakpoint +CREATE INDEX "idx_pro_league_active" ON "pro_league" USING btree ("is_active");--> statement-breakpoint +CREATE INDEX "idx_pro_player_season_player" ON "pro_player_season" USING btree ("player_id");--> statement-breakpoint +CREATE INDEX "idx_pro_player_season_season" ON "pro_player_season" USING btree ("season_id");--> statement-breakpoint +CREATE INDEX "idx_pro_player_season_team" ON "pro_player_season" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "idx_pro_player_league" ON "pro_player" USING btree ("league_id");--> statement-breakpoint +CREATE INDEX "idx_pro_player_name" ON "pro_player" USING btree ("last_name","first_name");--> statement-breakpoint +CREATE INDEX "idx_pro_player_canonical" ON "pro_player" USING btree ("canonical_player_id");--> statement-breakpoint +CREATE INDEX "idx_pro_player_position" ON "pro_player" USING btree ("position");--> statement-breakpoint +CREATE INDEX "idx_pro_season_league" ON "pro_season" USING btree ("league_id");--> statement-breakpoint +CREATE INDEX "idx_pro_season_year" ON "pro_season" USING btree ("year");--> statement-breakpoint +CREATE INDEX "idx_pro_season_current" ON "pro_season" USING btree ("is_current");--> statement-breakpoint +CREATE INDEX "idx_pro_standings_season" ON "pro_standings" USING btree ("season_id");--> statement-breakpoint +CREATE INDEX "idx_pro_standings_team" ON "pro_standings" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "idx_pro_standings_date" ON "pro_standings" USING btree ("snapshot_date");--> statement-breakpoint +CREATE INDEX "idx_pro_standings_position" ON "pro_standings" USING btree ("position");--> statement-breakpoint +CREATE INDEX "idx_pro_team_league" ON "pro_team" USING btree ("league_id");--> statement-breakpoint +CREATE INDEX "idx_pro_team_code" ON "pro_team" USING btree ("code");--> statement-breakpoint +CREATE INDEX "idx_pro_team_active" ON "pro_team" USING btree ("is_active");--> statement-breakpoint +CREATE INDEX "idx_season_organization" ON "season" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "idx_season_team" ON "season" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "team_member_team_id_idx" ON "team_member" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX "team_member_user_id_idx" ON "team_member" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "team_member_team_user_idx" ON "team_member" USING btree ("team_id","user_id");--> statement-breakpoint +CREATE INDEX "team_organization_id_idx" ON "team" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "team_name_idx" ON "team" USING btree ("name");--> statement-breakpoint +CREATE INDEX "team_created_at_idx" ON "team" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "user_email_idx" ON "user" USING btree ("email");--> statement-breakpoint +CREATE INDEX "user_name_idx" ON "user" USING btree ("name"); \ No newline at end of file diff --git a/packages/core/migrations/meta/0000_snapshot.json b/packages/core/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..584fa507 --- /dev/null +++ b/packages/core/migrations/meta/0000_snapshot.json @@ -0,0 +1,3752 @@ +{ + "id": "175ff229-f29b-40da-b565-fa61de8e3a5d", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "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 (3)", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp (3)", + "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 (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_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 (3)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "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 + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "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.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 (3)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "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 + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feedback_public_id_unique": { + "name": "feedback_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game": { + "name": "game", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seasonId": { + "name": "seasonId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "opponent_name": { + "name": "opponent_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opponent_team_id": { + "name": "opponent_team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "game_date": { + "name": "game_date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "venue": { + "name": "venue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_home_game": { + "name": "is_home_game", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "game_type": { + "name": "game_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'regular'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "home_score": { + "name": "home_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "away_score": { + "name": "away_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uniform_color": { + "name": "uniform_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arrival_time": { + "name": "arrival_time", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "opponent_logo_url": { + "name": "opponent_logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_game_id": { + "name": "external_game_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_game_organization": { + "name": "idx_game_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_game_team": { + "name": "idx_game_team", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_game_date": { + "name": "idx_game_date", + "columns": [ + { + "expression": "game_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_game_status": { + "name": "idx_game_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_game_team_date": { + "name": "idx_game_team_date", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "game_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_organization_id_organization_id_fk": { + "name": "game_organization_id_organization_id_fk", + "tableFrom": "game", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_team_id_team_id_fk": { + "name": "game_team_id_team_id_fk", + "tableFrom": "game", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_seasonId_season_id_fk": { + "name": "game_seasonId_season_id_fk", + "tableFrom": "game", + "tableTo": "season", + "columnsFrom": [ + "seasonId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "game_public_id_unique": { + "name": "game_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_idx": { + "name": "organization_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_contact_info": { + "name": "player_contact_info", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "facebook": { + "name": "facebook", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instagram": { + "name": "instagram", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "whatsapp": { + "name": "whatsapp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkedin": { + "name": "linkedin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "groupme": { + "name": "groupme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_name": { + "name": "emergency_contact_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_phone": { + "name": "emergency_contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_player_contact_info_player": { + "name": "idx_player_contact_info_player", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "player_contact_info_player_id_player_id_fk": { + "name": "player_contact_info_player_id_player_id_fk", + "tableFrom": "player_contact_info", + "tableTo": "player", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "player_contact_info_public_id_unique": { + "name": "player_contact_info_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "player_contact_info_player_id_unique": { + "name": "player_contact_info_player_id_unique", + "nullsNotDistinct": false, + "columns": [ + "player_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player": { + "name": "player", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_player_organization": { + "name": "idx_player_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_player_name": { + "name": "idx_player_name", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_player_email": { + "name": "idx_player_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "player_organization_id_organization_id_fk": { + "name": "player_organization_id_organization_id_fk", + "tableFrom": "player", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "player_user_id_user_id_fk": { + "name": "player_user_id_user_id_fk", + "tableFrom": "player", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "player_public_id_unique": { + "name": "player_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_player": { + "name": "team_player", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "jersey_number": { + "name": "jersey_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_team_player_team": { + "name": "idx_team_player_team", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_team_player_player": { + "name": "idx_team_player_player", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_team_player_unique": { + "name": "idx_team_player_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_player_team_id_team_id_fk": { + "name": "team_player_team_id_team_id_fk", + "tableFrom": "team_player", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_player_player_id_player_id_fk": { + "name": "team_player_player_id_player_id_fk", + "tableFrom": "team_player", + "tableTo": "player", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_player_public_id_unique": { + "name": "team_player_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_data_ingestion": { + "name": "pro_data_ingestion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "records_processed": { + "name": "records_processed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "records_created": { + "name": "records_created", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "records_updated": { + "name": "records_updated", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "records_skipped": { + "name": "records_skipped", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_data_url": { + "name": "raw_data_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_version": { + "name": "manifest_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_ingestion_league": { + "name": "idx_pro_ingestion_league", + "columns": [ + { + "expression": "league_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_ingestion_season": { + "name": "idx_pro_ingestion_season", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_ingestion_entity": { + "name": "idx_pro_ingestion_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_ingestion_status": { + "name": "idx_pro_ingestion_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_ingestion_created": { + "name": "idx_pro_ingestion_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_data_ingestion_league_id_pro_league_id_fk": { + "name": "pro_data_ingestion_league_id_pro_league_id_fk", + "tableFrom": "pro_data_ingestion", + "tableTo": "pro_league", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_data_ingestion_season_id_pro_season_id_fk": { + "name": "pro_data_ingestion_season_id_pro_season_id_fk", + "tableFrom": "pro_data_ingestion", + "tableTo": "pro_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_data_ingestion_public_id_unique": { + "name": "pro_data_ingestion_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_game": { + "name": "pro_game", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "home_team_id": { + "name": "home_team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "away_team_id": { + "name": "away_team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "game_date": { + "name": "game_date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "week": { + "name": "week", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "venue": { + "name": "venue", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "venue_city": { + "name": "venue_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "home_score": { + "name": "home_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "away_score": { + "name": "away_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_overtime": { + "name": "is_overtime", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "overtime_periods": { + "name": "overtime_periods", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "play_by_play_url": { + "name": "play_by_play_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "home_team_stats": { + "name": "home_team_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "away_team_stats": { + "name": "away_team_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "broadcaster": { + "name": "broadcaster", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_url": { + "name": "stream_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_game_season": { + "name": "idx_pro_game_season", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_game_home_team": { + "name": "idx_pro_game_home_team", + "columns": [ + { + "expression": "home_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_game_away_team": { + "name": "idx_pro_game_away_team", + "columns": [ + { + "expression": "away_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_game_date": { + "name": "idx_pro_game_date", + "columns": [ + { + "expression": "game_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_game_status": { + "name": "idx_pro_game_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_game_season_id_pro_season_id_fk": { + "name": "pro_game_season_id_pro_season_id_fk", + "tableFrom": "pro_game", + "tableTo": "pro_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_game_home_team_id_pro_team_id_fk": { + "name": "pro_game_home_team_id_pro_team_id_fk", + "tableFrom": "pro_game", + "tableTo": "pro_team", + "columnsFrom": [ + "home_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_game_away_team_id_pro_team_id_fk": { + "name": "pro_game_away_team_id_pro_team_id_fk", + "tableFrom": "pro_game", + "tableTo": "pro_team", + "columnsFrom": [ + "away_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_game_public_id_unique": { + "name": "pro_game_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_game_season_external": { + "name": "pro_game_season_external", + "nullsNotDistinct": false, + "columns": [ + "season_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_league": { + "name": "pro_league", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "founded_year": { + "name": "founded_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "defunct_year": { + "name": "defunct_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_league_code": { + "name": "idx_pro_league_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_league_active": { + "name": "idx_pro_league_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_league_public_id_unique": { + "name": "pro_league_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_league_code_unique": { + "name": "pro_league_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_player_season": { + "name": "pro_player_season", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "jersey_number": { + "name": "jersey_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "is_captain": { + "name": "is_captain", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "stats": { + "name": "stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "post_season_stats": { + "name": "post_season_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "goalie_stats": { + "name": "goalie_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "post_season_goalie_stats": { + "name": "post_season_goalie_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_player_season_player": { + "name": "idx_pro_player_season_player", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_player_season_season": { + "name": "idx_pro_player_season_season", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_player_season_team": { + "name": "idx_pro_player_season_team", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_player_season_player_id_pro_player_id_fk": { + "name": "pro_player_season_player_id_pro_player_id_fk", + "tableFrom": "pro_player_season", + "tableTo": "pro_player", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_player_season_season_id_pro_season_id_fk": { + "name": "pro_player_season_season_id_pro_season_id_fk", + "tableFrom": "pro_player_season", + "tableTo": "pro_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_player_season_team_id_pro_team_id_fk": { + "name": "pro_player_season_team_id_pro_team_id_fk", + "tableFrom": "pro_player_season", + "tableTo": "pro_team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_player_season_public_id_unique": { + "name": "pro_player_season_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_player_season_unique": { + "name": "pro_player_season_unique", + "nullsNotDistinct": false, + "columns": [ + "player_id", + "season_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_player": { + "name": "pro_player", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_player_id": { + "name": "canonical_player_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "birthplace": { + "name": "birthplace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(2)", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "handedness": { + "name": "handedness", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "college": { + "name": "college", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "high_school": { + "name": "high_school", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_url": { + "name": "profile_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_player_league": { + "name": "idx_pro_player_league", + "columns": [ + { + "expression": "league_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_player_name": { + "name": "idx_pro_player_name", + "columns": [ + { + "expression": "last_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "first_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_player_canonical": { + "name": "idx_pro_player_canonical", + "columns": [ + { + "expression": "canonical_player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_player_position": { + "name": "idx_pro_player_position", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_player_league_id_pro_league_id_fk": { + "name": "pro_player_league_id_pro_league_id_fk", + "tableFrom": "pro_player", + "tableTo": "pro_league", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_player_canonical_player_id_pro_player_id_fk": { + "name": "pro_player_canonical_player_id_pro_player_id_fk", + "tableFrom": "pro_player", + "tableTo": "pro_player", + "columnsFrom": [ + "canonical_player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_player_public_id_unique": { + "name": "pro_player_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_player_league_external": { + "name": "pro_player_league_external", + "nullsNotDistinct": false, + "columns": [ + "league_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_season": { + "name": "pro_season", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_season_league": { + "name": "idx_pro_season_league", + "columns": [ + { + "expression": "league_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_season_year": { + "name": "idx_pro_season_year", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_season_current": { + "name": "idx_pro_season_current", + "columns": [ + { + "expression": "is_current", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_season_league_id_pro_league_id_fk": { + "name": "pro_season_league_id_pro_league_id_fk", + "tableFrom": "pro_season", + "tableTo": "pro_league", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_season_public_id_unique": { + "name": "pro_season_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_season_league_external": { + "name": "pro_season_league_external", + "nullsNotDistinct": false, + "columns": [ + "league_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_standings": { + "name": "pro_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ties": { + "name": "ties", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "overtime_losses": { + "name": "overtime_losses", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "win_percentage": { + "name": "win_percentage", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goals_for": { + "name": "goals_for", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "goals_against": { + "name": "goals_against", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "goal_differential": { + "name": "goal_differential", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "conference": { + "name": "conference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "division": { + "name": "division", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "clinch_status": { + "name": "clinch_status", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "seed": { + "name": "seed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_standings_season": { + "name": "idx_pro_standings_season", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_standings_team": { + "name": "idx_pro_standings_team", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_standings_date": { + "name": "idx_pro_standings_date", + "columns": [ + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_standings_position": { + "name": "idx_pro_standings_position", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_standings_season_id_pro_season_id_fk": { + "name": "pro_standings_season_id_pro_season_id_fk", + "tableFrom": "pro_standings", + "tableTo": "pro_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pro_standings_team_id_pro_team_id_fk": { + "name": "pro_standings_team_id_pro_team_id_fk", + "tableFrom": "pro_standings", + "tableTo": "pro_team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_standings_public_id_unique": { + "name": "pro_standings_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_standings_season_team_date": { + "name": "pro_standings_season_team_date", + "nullsNotDistinct": false, + "columns": [ + "season_id", + "team_id", + "snapshot_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pro_team": { + "name": "pro_team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_color": { + "name": "primary_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "secondary_color": { + "name": "secondary_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "first_season_year": { + "name": "first_season_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_season_year": { + "name": "last_season_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pro_team_league": { + "name": "idx_pro_team_league", + "columns": [ + { + "expression": "league_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_team_code": { + "name": "idx_pro_team_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pro_team_active": { + "name": "idx_pro_team_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pro_team_league_id_pro_league_id_fk": { + "name": "pro_team_league_id_pro_league_id_fk", + "tableFrom": "pro_team", + "tableTo": "pro_league", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pro_team_public_id_unique": { + "name": "pro_team_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + }, + "pro_team_league_external": { + "name": "pro_team_league_external", + "nullsNotDistinct": false, + "columns": [ + "league_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season": { + "name": "season", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "division": { + "name": "division", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_season_organization": { + "name": "idx_season_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_season_team": { + "name": "idx_season_team", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_organization_id_organization_id_fk": { + "name": "season_organization_id_organization_id_fk", + "tableFrom": "season", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_team_id_team_id_fk": { + "name": "season_team_id_team_id_fk", + "tableFrom": "season", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "season_public_id_unique": { + "name": "season_public_id_unique", + "nullsNotDistinct": false, + "columns": [ + "public_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_member_team_id_idx": { + "name": "team_member_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_member_user_id_idx": { + "name": "team_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_member_team_user_idx": { + "name": "team_member_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "team_organization_id_idx": { + "name": "team_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_name_idx": { + "name": "team_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_created_at_idx": { + "name": "team_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_organization_id_organization_id_fk": { + "name": "team_organization_id_organization_id_fk", + "tableFrom": "team", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_name_unique": { + "name": "team_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "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": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_idx": { + "name": "user_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_name_idx": { + "name": "user_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "completed", + "upcoming" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/migrations/meta/_journal.json b/packages/core/migrations/meta/_journal.json new file mode 100644 index 00000000..68d9f78d --- /dev/null +++ b/packages/core/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1769156754401, + "tag": "0000_chunky_pet_avengers", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/core/src/pro-league/index.ts b/packages/core/src/pro-league/index.ts new file mode 100644 index 00000000..4b9a6d27 --- /dev/null +++ b/packages/core/src/pro-league/index.ts @@ -0,0 +1,84 @@ +// Pro League Domain - Professional lacrosse league data (PLL, NLL, MLL, MSL, WLA) + +// SQL Tables +export { + proLeagueTable, + proSeasonTable, + proTeamTable, + proPlayerTable, + proPlayerSeasonTable, + proGameTable, + proStandingsTable, + proDataIngestionTable, + type ProLeague, + type ProLeagueInsert, + type ProSeason, + type ProSeasonInsert, + type ProTeam, + type ProTeamInsert, + type ProPlayer, + type ProPlayerInsert, + type ProPlayerSeason, + type ProPlayerSeasonInsert, + type ProGame, + type ProGameInsert, + type ProStandings, + type ProStandingsInsert, + type ProDataIngestion, + type ProDataIngestionInsert, +} from "./pro-league.sql"; + +// JSONB Types +export type { + PlayerStats, + GoalieStats, + TeamStats, + PlayByPlayAction, + GamePlayByPlay, +} from "./pro-league.types"; + +export { isPLLStats, isNLLStats, hasGoalieStats } from "./pro-league.types"; + +// Effect Schemas +export { + LeagueCode, + GameStatus, + IngestionStatus, + EntityType, + SourceType, + CreateLeagueInput, + LeagueOutput, + UpsertSeasonInput, + SeasonOutput, + UpsertTeamInput, + TeamOutput, + UpsertPlayerInput, + PlayerOutput, + UpsertPlayerSeasonInput, + UpsertGameInput, + GameOutput, + UpsertStandingsInput, + CreateIngestionInput, + UpdateIngestionInput, + GetByLeagueInput, + GetBySeasonInput, + GetByLeagueAndYearInput, + GetStandingsInput, +} from "./pro-league.schema"; + +// Errors +export { + ProLeagueNotFoundError, + ProSeasonNotFoundError, + ProTeamNotFoundError, + ProPlayerNotFoundError, + ProGameNotFoundError, + ProIngestionError, + ProUpsertError, +} from "./pro-league.error"; + +// Repository +export { ProLeagueRepo } from "./pro-league.repo"; + +// Service +export { ProLeagueService } from "./pro-league.service"; diff --git a/packages/core/src/pro-league/pro-league.error.ts b/packages/core/src/pro-league/pro-league.error.ts new file mode 100644 index 00000000..7a891393 --- /dev/null +++ b/packages/core/src/pro-league/pro-league.error.ts @@ -0,0 +1,71 @@ +import { Schema } from "effect"; + +// ============================================================================= +// PRO LEAGUE DOMAIN ERRORS +// ============================================================================= + +export class ProLeagueNotFoundError extends Schema.TaggedError()( + "ProLeagueNotFoundError", + { + message: Schema.String, + code: Schema.optional(Schema.String), + id: Schema.optional(Schema.Number), + }, +) {} + +export class ProSeasonNotFoundError extends Schema.TaggedError()( + "ProSeasonNotFoundError", + { + message: Schema.String, + leagueId: Schema.optional(Schema.Number), + externalId: Schema.optional(Schema.String), + year: Schema.optional(Schema.Number), + }, +) {} + +export class ProTeamNotFoundError extends Schema.TaggedError()( + "ProTeamNotFoundError", + { + message: Schema.String, + leagueId: Schema.optional(Schema.Number), + externalId: Schema.optional(Schema.String), + }, +) {} + +export class ProPlayerNotFoundError extends Schema.TaggedError()( + "ProPlayerNotFoundError", + { + message: Schema.String, + leagueId: Schema.optional(Schema.Number), + externalId: Schema.optional(Schema.String), + }, +) {} + +export class ProGameNotFoundError extends Schema.TaggedError()( + "ProGameNotFoundError", + { + message: Schema.String, + seasonId: Schema.optional(Schema.Number), + externalId: Schema.optional(Schema.String), + }, +) {} + +export class ProIngestionError extends Schema.TaggedError()( + "ProIngestionError", + { + message: Schema.String, + leagueCode: Schema.optional(Schema.String), + entityType: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Unknown), + }, +) {} + +export class ProUpsertError extends Schema.TaggedError()( + "ProUpsertError", + { + message: Schema.String, + entity: Schema.String, + externalId: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Unknown), + }, +) {} diff --git a/packages/core/src/pro-league/pro-league.repo.ts b/packages/core/src/pro-league/pro-league.repo.ts new file mode 100644 index 00000000..9aa01c2b --- /dev/null +++ b/packages/core/src/pro-league/pro-league.repo.ts @@ -0,0 +1,823 @@ +import { PgDrizzle } from "@effect/sql-drizzle/Pg"; +import { and, eq, getTableColumns, isNull } from "drizzle-orm"; +import { Array as Arr, Effect } from "effect"; + +import { DatabaseLive } from "../drizzle/drizzle.service"; + +import { ProUpsertError } from "./pro-league.error"; +import type { + CreateIngestionInput, + CreateLeagueInput, + LeagueCode, + UpsertGameInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertSeasonInput, + UpsertStandingsInput, + UpsertTeamInput, + UpdateIngestionInput, +} from "./pro-league.schema"; +import { + proDataIngestionTable, + proGameTable, + proLeagueTable, + proPlayerSeasonTable, + proPlayerTable, + proSeasonTable, + proStandingsTable, + proTeamTable, + type ProGame, + type ProLeague, + type ProPlayer, + type ProPlayerSeason, + type ProSeason, + type ProStandings, + type ProTeam, +} from "./pro-league.sql"; +import type { GoalieStats, PlayerStats, TeamStats } from "./pro-league.types"; + +export class ProLeagueRepo extends Effect.Service()( + "ProLeagueRepo", + { + effect: Effect.gen(function* () { + const db = yield* PgDrizzle; + + // Column helpers (exclude internal id for selects) + const { id: _leagueId, ...leagueCols } = getTableColumns(proLeagueTable); + const { id: _seasonId, ...seasonCols } = getTableColumns(proSeasonTable); + + return { + // ===================================================================== + // LEAGUE OPERATIONS + // ===================================================================== + + getLeagueByCode: (code: LeagueCode) => + db + .select() + .from(proLeagueTable) + .where( + and( + eq(proLeagueTable.code, code), + isNull(proLeagueTable.deletedAt), + ), + ) + .pipe(Effect.flatMap(Arr.head)), + + createLeague: (input: CreateLeagueInput) => + db + .insert(proLeagueTable) + .values({ + code: input.code, + name: input.name, + shortName: input.shortName ?? null, + country: input.country ?? null, + isActive: input.isActive ?? true, + foundedYear: input.foundedYear ?? null, + defunctYear: input.defunctYear ?? null, + websiteUrl: input.websiteUrl ?? null, + logoUrl: input.logoUrl ?? null, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + upsertLeague: (input: CreateLeagueInput) => + db + .insert(proLeagueTable) + .values({ + code: input.code, + name: input.name, + shortName: input.shortName ?? null, + country: input.country ?? null, + isActive: input.isActive ?? true, + foundedYear: input.foundedYear ?? null, + defunctYear: input.defunctYear ?? null, + websiteUrl: input.websiteUrl ?? null, + logoUrl: input.logoUrl ?? null, + }) + .onConflictDoUpdate({ + target: proLeagueTable.code, + set: { + name: input.name, + shortName: input.shortName ?? null, + isActive: input.isActive ?? true, + websiteUrl: input.websiteUrl ?? null, + logoUrl: input.logoUrl ?? null, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + listLeagues: () => + db + .select(leagueCols) + .from(proLeagueTable) + .where(isNull(proLeagueTable.deletedAt)) + .pipe(Effect.map((rows) => rows as Omit[])), + + // ===================================================================== + // SEASON OPERATIONS + // ===================================================================== + + getSeasonByExternalId: (leagueId: number, externalId: string) => + db + .select() + .from(proSeasonTable) + .where( + and( + eq(proSeasonTable.leagueId, leagueId), + eq(proSeasonTable.externalId, externalId), + isNull(proSeasonTable.deletedAt), + ), + ) + .pipe(Effect.flatMap(Arr.head)), + + getSeasonByYear: (leagueId: number, year: number) => + db + .select() + .from(proSeasonTable) + .where( + and( + eq(proSeasonTable.leagueId, leagueId), + eq(proSeasonTable.year, year), + isNull(proSeasonTable.deletedAt), + ), + ) + .pipe(Effect.flatMap(Arr.head)), + + upsertSeason: (input: UpsertSeasonInput) => + db + .insert(proSeasonTable) + .values({ + leagueId: input.leagueId, + externalId: input.externalId, + year: input.year, + displayName: input.displayName, + startDate: input.startDate, + endDate: input.endDate, + isCurrent: input.isCurrent, + }) + .onConflictDoUpdate({ + target: [proSeasonTable.leagueId, proSeasonTable.externalId], + set: { + year: input.year, + displayName: input.displayName, + startDate: input.startDate, + endDate: input.endDate, + isCurrent: input.isCurrent, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + listSeasons: (leagueId: number) => + db + .select(seasonCols) + .from(proSeasonTable) + .where( + and( + eq(proSeasonTable.leagueId, leagueId), + isNull(proSeasonTable.deletedAt), + ), + ) + .pipe(Effect.map((rows) => rows as Omit[])), + + // ===================================================================== + // TEAM OPERATIONS + // ===================================================================== + + getTeamByExternalId: (leagueId: number, externalId: string) => + db + .select() + .from(proTeamTable) + .where( + and( + eq(proTeamTable.leagueId, leagueId), + eq(proTeamTable.externalId, externalId), + isNull(proTeamTable.deletedAt), + ), + ) + .pipe(Effect.flatMap(Arr.head)), + + upsertTeam: (input: UpsertTeamInput) => + db + .insert(proTeamTable) + .values({ + leagueId: input.leagueId, + externalId: input.externalId, + code: input.code, + name: input.name, + shortName: input.shortName, + city: input.city, + logoUrl: input.logoUrl, + primaryColor: input.primaryColor, + secondaryColor: input.secondaryColor, + websiteUrl: input.websiteUrl, + isActive: input.isActive, + firstSeasonYear: input.firstSeasonYear, + lastSeasonYear: input.lastSeasonYear, + }) + .onConflictDoUpdate({ + target: [proTeamTable.leagueId, proTeamTable.externalId], + set: { + code: input.code, + name: input.name, + shortName: input.shortName, + city: input.city, + logoUrl: input.logoUrl, + primaryColor: input.primaryColor, + secondaryColor: input.secondaryColor, + websiteUrl: input.websiteUrl, + isActive: input.isActive, + firstSeasonYear: input.firstSeasonYear, + lastSeasonYear: input.lastSeasonYear, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + bulkUpsertTeams: (inputs: UpsertTeamInput[]) => + Effect.gen(function* () { + if (inputs.length === 0) return []; + + const results: ProTeam[] = []; + for (const input of inputs) { + const result = yield* db + .insert(proTeamTable) + .values({ + leagueId: input.leagueId, + externalId: input.externalId, + code: input.code, + name: input.name, + shortName: input.shortName, + city: input.city, + logoUrl: input.logoUrl, + primaryColor: input.primaryColor, + secondaryColor: input.secondaryColor, + websiteUrl: input.websiteUrl, + isActive: input.isActive, + firstSeasonYear: input.firstSeasonYear, + lastSeasonYear: input.lastSeasonYear, + }) + .onConflictDoUpdate({ + target: [proTeamTable.leagueId, proTeamTable.externalId], + set: { + code: input.code, + name: input.name, + shortName: input.shortName, + city: input.city, + logoUrl: input.logoUrl, + isActive: input.isActive, + }, + }) + .returning() + .pipe( + Effect.flatMap(Arr.head), + Effect.mapError( + (e) => + new ProUpsertError({ + message: "Failed to upsert team", + entity: "team", + externalId: input.externalId, + cause: e, + }), + ), + ); + results.push(result); + } + return results; + }), + + listTeams: (leagueId: number) => + db + .select() + .from(proTeamTable) + .where( + and( + eq(proTeamTable.leagueId, leagueId), + isNull(proTeamTable.deletedAt), + ), + ), + + // ===================================================================== + // PLAYER OPERATIONS + // ===================================================================== + + getPlayerByExternalId: (leagueId: number, externalId: string) => + db + .select() + .from(proPlayerTable) + .where( + and( + eq(proPlayerTable.leagueId, leagueId), + eq(proPlayerTable.externalId, externalId), + isNull(proPlayerTable.deletedAt), + ), + ) + .pipe(Effect.flatMap(Arr.head)), + + upsertPlayer: (input: UpsertPlayerInput) => + db + .insert(proPlayerTable) + .values({ + leagueId: input.leagueId, + externalId: input.externalId, + firstName: input.firstName, + lastName: input.lastName, + fullName: input.fullName, + position: input.position, + dateOfBirth: input.dateOfBirth, + birthplace: input.birthplace, + country: input.country, + height: input.height, + weight: input.weight, + handedness: input.handedness, + college: input.college, + highSchool: input.highSchool, + profileUrl: input.profileUrl, + photoUrl: input.photoUrl, + }) + .onConflictDoUpdate({ + target: [proPlayerTable.leagueId, proPlayerTable.externalId], + set: { + firstName: input.firstName, + lastName: input.lastName, + fullName: input.fullName, + position: input.position, + dateOfBirth: input.dateOfBirth, + birthplace: input.birthplace, + country: input.country, + height: input.height, + weight: input.weight, + handedness: input.handedness, + college: input.college, + highSchool: input.highSchool, + profileUrl: input.profileUrl, + photoUrl: input.photoUrl, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + bulkUpsertPlayers: (inputs: UpsertPlayerInput[]) => + Effect.gen(function* () { + if (inputs.length === 0) return []; + + const results: ProPlayer[] = []; + for (const input of inputs) { + const result = yield* db + .insert(proPlayerTable) + .values({ + leagueId: input.leagueId, + externalId: input.externalId, + firstName: input.firstName, + lastName: input.lastName, + fullName: input.fullName, + position: input.position, + dateOfBirth: input.dateOfBirth, + birthplace: input.birthplace, + country: input.country, + height: input.height, + weight: input.weight, + handedness: input.handedness, + college: input.college, + highSchool: input.highSchool, + profileUrl: input.profileUrl, + photoUrl: input.photoUrl, + }) + .onConflictDoUpdate({ + target: [proPlayerTable.leagueId, proPlayerTable.externalId], + set: { + firstName: input.firstName, + lastName: input.lastName, + fullName: input.fullName, + position: input.position, + height: input.height, + weight: input.weight, + }, + }) + .returning() + .pipe( + Effect.flatMap(Arr.head), + Effect.mapError( + (e) => + new ProUpsertError({ + message: "Failed to upsert player", + entity: "player", + externalId: input.externalId, + cause: e, + }), + ), + ); + results.push(result); + } + return results; + }), + + listPlayers: (leagueId: number) => + db + .select() + .from(proPlayerTable) + .where( + and( + eq(proPlayerTable.leagueId, leagueId), + isNull(proPlayerTable.deletedAt), + ), + ), + + // ===================================================================== + // PLAYER SEASON OPERATIONS + // ===================================================================== + + upsertPlayerSeason: (input: UpsertPlayerSeasonInput) => + db + .insert(proPlayerSeasonTable) + .values({ + playerId: input.playerId, + seasonId: input.seasonId, + teamId: input.teamId, + jerseyNumber: input.jerseyNumber, + position: input.position, + isCaptain: input.isCaptain ?? false, + stats: input.stats as PlayerStats | null, + postSeasonStats: input.postSeasonStats as PlayerStats | null, + goalieStats: input.goalieStats as GoalieStats | null, + postSeasonGoalieStats: + input.postSeasonGoalieStats as GoalieStats | null, + gamesPlayed: input.gamesPlayed ?? 0, + }) + .onConflictDoUpdate({ + target: [ + proPlayerSeasonTable.playerId, + proPlayerSeasonTable.seasonId, + ], + set: { + teamId: input.teamId, + jerseyNumber: input.jerseyNumber, + position: input.position, + isCaptain: input.isCaptain ?? false, + stats: input.stats as PlayerStats | null, + postSeasonStats: input.postSeasonStats as PlayerStats | null, + goalieStats: input.goalieStats as GoalieStats | null, + postSeasonGoalieStats: + input.postSeasonGoalieStats as GoalieStats | null, + gamesPlayed: input.gamesPlayed ?? 0, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + bulkUpsertPlayerSeasons: (inputs: UpsertPlayerSeasonInput[]) => + Effect.gen(function* () { + if (inputs.length === 0) return []; + + const results: ProPlayerSeason[] = []; + for (const input of inputs) { + const result = yield* db + .insert(proPlayerSeasonTable) + .values({ + playerId: input.playerId, + seasonId: input.seasonId, + teamId: input.teamId, + jerseyNumber: input.jerseyNumber, + position: input.position, + isCaptain: input.isCaptain ?? false, + stats: input.stats as PlayerStats | null, + postSeasonStats: input.postSeasonStats as PlayerStats | null, + goalieStats: input.goalieStats as GoalieStats | null, + postSeasonGoalieStats: + input.postSeasonGoalieStats as GoalieStats | null, + gamesPlayed: input.gamesPlayed ?? 0, + }) + .onConflictDoUpdate({ + target: [ + proPlayerSeasonTable.playerId, + proPlayerSeasonTable.seasonId, + ], + set: { + teamId: input.teamId, + jerseyNumber: input.jerseyNumber, + position: input.position, + stats: input.stats as PlayerStats | null, + postSeasonStats: + input.postSeasonStats as PlayerStats | null, + goalieStats: input.goalieStats as GoalieStats | null, + postSeasonGoalieStats: + input.postSeasonGoalieStats as GoalieStats | null, + gamesPlayed: input.gamesPlayed ?? 0, + }, + }) + .returning() + .pipe( + Effect.flatMap(Arr.head), + Effect.mapError( + (e) => + new ProUpsertError({ + message: "Failed to upsert player season", + entity: "playerSeason", + cause: e, + }), + ), + ); + results.push(result); + } + return results; + }), + + getPlayerSeasons: (seasonId: number) => + db + .select() + .from(proPlayerSeasonTable) + .where(eq(proPlayerSeasonTable.seasonId, seasonId)), + + // ===================================================================== + // GAME OPERATIONS + // ===================================================================== + + upsertGame: (input: UpsertGameInput) => + db + .insert(proGameTable) + .values({ + seasonId: input.seasonId, + externalId: input.externalId, + homeTeamId: input.homeTeamId, + awayTeamId: input.awayTeamId, + gameDate: input.gameDate, + week: input.week, + gameNumber: input.gameNumber, + venue: input.venue, + venueCity: input.venueCity, + status: input.status, + homeScore: input.homeScore, + awayScore: input.awayScore, + isOvertime: input.isOvertime ?? false, + overtimePeriods: input.overtimePeriods ?? 0, + playByPlayUrl: input.playByPlayUrl, + homeTeamStats: input.homeTeamStats as TeamStats | null, + awayTeamStats: input.awayTeamStats as TeamStats | null, + broadcaster: input.broadcaster, + streamUrl: input.streamUrl, + }) + .onConflictDoUpdate({ + target: [proGameTable.seasonId, proGameTable.externalId], + set: { + homeTeamId: input.homeTeamId, + awayTeamId: input.awayTeamId, + gameDate: input.gameDate, + week: input.week, + venue: input.venue, + status: input.status, + homeScore: input.homeScore, + awayScore: input.awayScore, + isOvertime: input.isOvertime ?? false, + overtimePeriods: input.overtimePeriods ?? 0, + playByPlayUrl: input.playByPlayUrl, + homeTeamStats: input.homeTeamStats as TeamStats | null, + awayTeamStats: input.awayTeamStats as TeamStats | null, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + bulkUpsertGames: (inputs: UpsertGameInput[]) => + Effect.gen(function* () { + if (inputs.length === 0) return []; + + const results: ProGame[] = []; + for (const input of inputs) { + const result = yield* db + .insert(proGameTable) + .values({ + seasonId: input.seasonId, + externalId: input.externalId, + homeTeamId: input.homeTeamId, + awayTeamId: input.awayTeamId, + gameDate: input.gameDate, + week: input.week, + gameNumber: input.gameNumber, + venue: input.venue, + venueCity: input.venueCity, + status: input.status, + homeScore: input.homeScore, + awayScore: input.awayScore, + isOvertime: input.isOvertime ?? false, + overtimePeriods: input.overtimePeriods ?? 0, + playByPlayUrl: input.playByPlayUrl, + homeTeamStats: input.homeTeamStats as TeamStats | null, + awayTeamStats: input.awayTeamStats as TeamStats | null, + broadcaster: input.broadcaster, + streamUrl: input.streamUrl, + }) + .onConflictDoUpdate({ + target: [proGameTable.seasonId, proGameTable.externalId], + set: { + status: input.status, + homeScore: input.homeScore, + awayScore: input.awayScore, + }, + }) + .returning() + .pipe( + Effect.flatMap(Arr.head), + Effect.mapError( + (e) => + new ProUpsertError({ + message: "Failed to upsert game", + entity: "game", + externalId: input.externalId ?? undefined, + cause: e, + }), + ), + ); + results.push(result); + } + return results; + }), + + listGames: (seasonId: number) => + db + .select() + .from(proGameTable) + .where(eq(proGameTable.seasonId, seasonId)), + + // ===================================================================== + // STANDINGS OPERATIONS + // ===================================================================== + + upsertStandings: (input: UpsertStandingsInput) => + db + .insert(proStandingsTable) + .values({ + seasonId: input.seasonId, + teamId: input.teamId, + snapshotDate: input.snapshotDate, + position: input.position, + wins: input.wins, + losses: input.losses, + ties: input.ties ?? 0, + overtimeLosses: input.overtimeLosses ?? 0, + points: input.points, + winPercentage: input.winPercentage, + goalsFor: input.goalsFor ?? 0, + goalsAgainst: input.goalsAgainst ?? 0, + goalDifferential: input.goalDifferential ?? 0, + conference: input.conference, + division: input.division, + clinchStatus: input.clinchStatus, + seed: input.seed, + }) + .onConflictDoUpdate({ + target: [ + proStandingsTable.seasonId, + proStandingsTable.teamId, + proStandingsTable.snapshotDate, + ], + set: { + position: input.position, + wins: input.wins, + losses: input.losses, + ties: input.ties ?? 0, + overtimeLosses: input.overtimeLosses ?? 0, + points: input.points, + winPercentage: input.winPercentage, + goalsFor: input.goalsFor ?? 0, + goalsAgainst: input.goalsAgainst ?? 0, + goalDifferential: input.goalDifferential ?? 0, + clinchStatus: input.clinchStatus, + seed: input.seed, + }, + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + bulkUpsertStandings: (inputs: UpsertStandingsInput[]) => + Effect.gen(function* () { + if (inputs.length === 0) return []; + + const results: ProStandings[] = []; + for (const input of inputs) { + const result = yield* db + .insert(proStandingsTable) + .values({ + seasonId: input.seasonId, + teamId: input.teamId, + snapshotDate: input.snapshotDate, + position: input.position, + wins: input.wins, + losses: input.losses, + ties: input.ties ?? 0, + overtimeLosses: input.overtimeLosses ?? 0, + points: input.points, + winPercentage: input.winPercentage, + goalsFor: input.goalsFor ?? 0, + goalsAgainst: input.goalsAgainst ?? 0, + goalDifferential: input.goalDifferential ?? 0, + conference: input.conference, + division: input.division, + clinchStatus: input.clinchStatus, + seed: input.seed, + }) + .onConflictDoUpdate({ + target: [ + proStandingsTable.seasonId, + proStandingsTable.teamId, + proStandingsTable.snapshotDate, + ], + set: { + position: input.position, + wins: input.wins, + losses: input.losses, + goalsFor: input.goalsFor ?? 0, + goalsAgainst: input.goalsAgainst ?? 0, + goalDifferential: input.goalDifferential ?? 0, + }, + }) + .returning() + .pipe( + Effect.flatMap(Arr.head), + Effect.mapError( + (e) => + new ProUpsertError({ + message: "Failed to upsert standings", + entity: "standings", + cause: e, + }), + ), + ); + results.push(result); + } + return results; + }), + + getStandings: (seasonId: number, snapshotDate?: Date) => + Effect.gen(function* () { + // If no snapshot date, get the latest + if (!snapshotDate) { + return yield* db + .select() + .from(proStandingsTable) + .where(eq(proStandingsTable.seasonId, seasonId)) + .orderBy(proStandingsTable.snapshotDate); + } + return yield* db + .select() + .from(proStandingsTable) + .where( + and( + eq(proStandingsTable.seasonId, seasonId), + eq(proStandingsTable.snapshotDate, snapshotDate), + ), + ) + .orderBy(proStandingsTable.position); + }), + + // ===================================================================== + // INGESTION OPERATIONS + // ===================================================================== + + createIngestion: (input: CreateIngestionInput) => + db + .insert(proDataIngestionTable) + .values({ + leagueId: input.leagueId, + seasonId: input.seasonId, + entityType: input.entityType, + sourceUrl: input.sourceUrl, + sourceType: input.sourceType, + status: "pending", + }) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + updateIngestion: (input: UpdateIngestionInput) => + db + .update(proDataIngestionTable) + .set({ + status: input.status, + startedAt: input.startedAt, + completedAt: input.completedAt, + recordsProcessed: input.recordsProcessed, + recordsCreated: input.recordsCreated, + recordsUpdated: input.recordsUpdated, + recordsSkipped: input.recordsSkipped, + durationMs: input.durationMs, + errorMessage: input.errorMessage, + errorStack: input.errorStack, + rawDataUrl: input.rawDataUrl, + manifestVersion: input.manifestVersion, + }) + .where(eq(proDataIngestionTable.id, input.id)) + .returning() + .pipe(Effect.flatMap(Arr.head)), + + getRecentIngestions: (leagueId: number, limit = 10) => + db + .select() + .from(proDataIngestionTable) + .where(eq(proDataIngestionTable.leagueId, leagueId)) + .orderBy(proDataIngestionTable.createdAt) + .limit(limit), + } as const; + }), + dependencies: [DatabaseLive], + }, +) {} diff --git a/packages/core/src/pro-league/pro-league.schema.ts b/packages/core/src/pro-league/pro-league.schema.ts new file mode 100644 index 00000000..1a3a4f57 --- /dev/null +++ b/packages/core/src/pro-league/pro-league.schema.ts @@ -0,0 +1,363 @@ +import { Schema } from "effect"; + +import { NanoidSchema, SerialSchema } from "../schema"; + +// ============================================================================= +// LEAGUE CODE +// ============================================================================= + +export const LeagueCode = Schema.Literal("pll", "nll", "mll", "msl", "wla"); +export type LeagueCode = typeof LeagueCode.Type; + +// ============================================================================= +// COMMON SCHEMAS +// ============================================================================= + +export const ProLeagueIdSchema = { + leagueId: SerialSchema, +}; + +export const ProSeasonIdSchema = { + seasonId: SerialSchema, +}; + +export const ProTeamIdSchema = { + teamId: SerialSchema, +}; + +export const ProPlayerIdSchema = { + playerId: SerialSchema, +}; + +export const ExternalIdSchema = { + externalId: Schema.String.pipe( + Schema.minLength(1, { message: () => "External ID is required" }), + ), +}; + +// ============================================================================= +// LEAGUE SCHEMAS +// ============================================================================= + +export class CreateLeagueInput extends Schema.Class( + "CreateLeagueInput", +)({ + code: LeagueCode, + name: Schema.String, + shortName: Schema.optional(Schema.String), + country: Schema.optional(Schema.String), + isActive: Schema.optionalWith(Schema.Boolean, { default: () => true }), + foundedYear: Schema.optional(Schema.Number), + defunctYear: Schema.optional(Schema.Number), + websiteUrl: Schema.optional(Schema.String), + logoUrl: Schema.optional(Schema.String), +}) {} + +export class LeagueOutput extends Schema.Class("LeagueOutput")({ + id: SerialSchema, + publicId: NanoidSchema, + code: LeagueCode, + name: Schema.String, + shortName: Schema.NullOr(Schema.String), + country: Schema.NullOr(Schema.String), + isActive: Schema.Boolean, + foundedYear: Schema.NullOr(Schema.Number), + defunctYear: Schema.NullOr(Schema.Number), + websiteUrl: Schema.NullOr(Schema.String), + logoUrl: Schema.NullOr(Schema.String), +}) {} + +// ============================================================================= +// SEASON SCHEMAS +// ============================================================================= + +export class CreateSeasonInput extends Schema.Class( + "CreateSeasonInput", +)({ + leagueId: SerialSchema, + externalId: Schema.String, + year: Schema.Number.pipe(Schema.int(), Schema.between(1900, 2100)), + displayName: Schema.String, + startDate: Schema.optional(Schema.DateFromSelf), + endDate: Schema.optional(Schema.DateFromSelf), + isCurrent: Schema.optionalWith(Schema.Boolean, { default: () => false }), +}) {} + +export class UpsertSeasonInput extends Schema.Class( + "UpsertSeasonInput", +)({ + leagueId: SerialSchema, + externalId: Schema.String, + year: Schema.Number, + displayName: Schema.String, + startDate: Schema.NullOr(Schema.DateFromSelf), + endDate: Schema.NullOr(Schema.DateFromSelf), + isCurrent: Schema.Boolean, +}) {} + +export class SeasonOutput extends Schema.Class("SeasonOutput")({ + id: SerialSchema, + publicId: NanoidSchema, + leagueId: SerialSchema, + externalId: Schema.String, + year: Schema.Number, + displayName: Schema.String, + startDate: Schema.NullOr(Schema.DateFromSelf), + endDate: Schema.NullOr(Schema.DateFromSelf), + isCurrent: Schema.Boolean, +}) {} + +// ============================================================================= +// TEAM SCHEMAS +// ============================================================================= + +export class UpsertTeamInput extends Schema.Class( + "UpsertTeamInput", +)({ + leagueId: SerialSchema, + externalId: Schema.String, + code: Schema.NullOr(Schema.String), + name: Schema.String, + shortName: Schema.NullOr(Schema.String), + city: Schema.NullOr(Schema.String), + logoUrl: Schema.NullOr(Schema.String), + primaryColor: Schema.NullOr(Schema.String), + secondaryColor: Schema.NullOr(Schema.String), + websiteUrl: Schema.NullOr(Schema.String), + isActive: Schema.Boolean, + firstSeasonYear: Schema.NullOr(Schema.Number), + lastSeasonYear: Schema.NullOr(Schema.Number), +}) {} + +export class TeamOutput extends Schema.Class("TeamOutput")({ + id: SerialSchema, + publicId: NanoidSchema, + leagueId: SerialSchema, + externalId: Schema.String, + code: Schema.NullOr(Schema.String), + name: Schema.String, + shortName: Schema.NullOr(Schema.String), + city: Schema.NullOr(Schema.String), + logoUrl: Schema.NullOr(Schema.String), + isActive: Schema.Boolean, +}) {} + +// ============================================================================= +// PLAYER SCHEMAS +// ============================================================================= + +export class UpsertPlayerInput extends Schema.Class( + "UpsertPlayerInput", +)({ + leagueId: SerialSchema, + externalId: Schema.String, + firstName: Schema.NullOr(Schema.String), + lastName: Schema.String, + fullName: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.String), + dateOfBirth: Schema.NullOr(Schema.DateFromSelf), + birthplace: Schema.NullOr(Schema.String), + country: Schema.NullOr(Schema.String), + height: Schema.NullOr(Schema.String), + weight: Schema.NullOr(Schema.Number), + handedness: Schema.NullOr(Schema.String), + college: Schema.NullOr(Schema.String), + highSchool: Schema.NullOr(Schema.String), + profileUrl: Schema.NullOr(Schema.String), + photoUrl: Schema.NullOr(Schema.String), +}) {} + +export class PlayerOutput extends Schema.Class("PlayerOutput")({ + id: SerialSchema, + publicId: NanoidSchema, + leagueId: SerialSchema, + externalId: Schema.String, + firstName: Schema.NullOr(Schema.String), + lastName: Schema.String, + fullName: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.String), +}) {} + +// ============================================================================= +// PLAYER SEASON SCHEMAS +// ============================================================================= + +// Player season uses Record for stats JSONB (flexible structure) +const StatsRecord = Schema.NullOr( + Schema.Record({ key: Schema.String, value: Schema.Unknown }), +); + +export class UpsertPlayerSeasonInput extends Schema.Class( + "UpsertPlayerSeasonInput", +)({ + playerId: SerialSchema, + seasonId: SerialSchema, + teamId: Schema.NullOr(SerialSchema), + jerseyNumber: Schema.NullOr(Schema.Number), + position: Schema.NullOr(Schema.String), + isCaptain: Schema.optionalWith(Schema.Boolean, { default: () => false }), + stats: StatsRecord, + postSeasonStats: StatsRecord, + goalieStats: StatsRecord, + postSeasonGoalieStats: StatsRecord, + gamesPlayed: Schema.optionalWith(Schema.Number, { default: () => 0 }), +}) {} + +// ============================================================================= +// GAME SCHEMAS +// ============================================================================= + +export const GameStatus = Schema.Literal( + "scheduled", + "in_progress", + "final", + "postponed", + "cancelled", +); +export type GameStatus = typeof GameStatus.Type; + +export class UpsertGameInput extends Schema.Class( + "UpsertGameInput", +)({ + seasonId: SerialSchema, + externalId: Schema.NullOr(Schema.String), + homeTeamId: SerialSchema, + awayTeamId: SerialSchema, + gameDate: Schema.DateFromSelf, + week: Schema.NullOr(Schema.String), + gameNumber: Schema.NullOr(Schema.Number), + venue: Schema.NullOr(Schema.String), + venueCity: Schema.NullOr(Schema.String), + status: GameStatus, + homeScore: Schema.NullOr(Schema.Number), + awayScore: Schema.NullOr(Schema.Number), + isOvertime: Schema.optionalWith(Schema.Boolean, { default: () => false }), + overtimePeriods: Schema.optionalWith(Schema.Number, { default: () => 0 }), + playByPlayUrl: Schema.NullOr(Schema.String), + homeTeamStats: StatsRecord, + awayTeamStats: StatsRecord, + broadcaster: Schema.NullOr(Schema.String), + streamUrl: Schema.NullOr(Schema.String), +}) {} + +export class GameOutput extends Schema.Class("GameOutput")({ + id: SerialSchema, + publicId: NanoidSchema, + seasonId: SerialSchema, + externalId: Schema.NullOr(Schema.String), + homeTeamId: SerialSchema, + awayTeamId: SerialSchema, + gameDate: Schema.DateFromSelf, + status: GameStatus, + homeScore: Schema.NullOr(Schema.Number), + awayScore: Schema.NullOr(Schema.Number), +}) {} + +// ============================================================================= +// STANDINGS SCHEMAS +// ============================================================================= + +export class UpsertStandingsInput extends Schema.Class( + "UpsertStandingsInput", +)({ + seasonId: SerialSchema, + teamId: SerialSchema, + snapshotDate: Schema.DateFromSelf, + position: Schema.Number, + wins: Schema.Number, + losses: Schema.Number, + ties: Schema.optionalWith(Schema.Number, { default: () => 0 }), + overtimeLosses: Schema.optionalWith(Schema.Number, { default: () => 0 }), + points: Schema.NullOr(Schema.Number), + winPercentage: Schema.NullOr(Schema.Number), + goalsFor: Schema.optionalWith(Schema.Number, { default: () => 0 }), + goalsAgainst: Schema.optionalWith(Schema.Number, { default: () => 0 }), + goalDifferential: Schema.optionalWith(Schema.Number, { default: () => 0 }), + conference: Schema.NullOr(Schema.String), + division: Schema.NullOr(Schema.String), + clinchStatus: Schema.NullOr(Schema.String), + seed: Schema.NullOr(Schema.Number), +}) {} + +// ============================================================================= +// INGESTION SCHEMAS +// ============================================================================= + +export const IngestionStatus = Schema.Literal( + "pending", + "running", + "completed", + "failed", +); +export type IngestionStatus = typeof IngestionStatus.Type; + +export const EntityType = Schema.Literal( + "teams", + "players", + "games", + "standings", + "play_by_play", + "full_season", +); +export type EntityType = typeof EntityType.Type; + +export const SourceType = Schema.Literal("api", "scrape", "wayback"); +export type SourceType = typeof SourceType.Type; + +export class CreateIngestionInput extends Schema.Class( + "CreateIngestionInput", +)({ + leagueId: SerialSchema, + seasonId: Schema.NullOr(SerialSchema), + entityType: EntityType, + sourceUrl: Schema.NullOr(Schema.String), + sourceType: Schema.NullOr(SourceType), +}) {} + +export class UpdateIngestionInput extends Schema.Class( + "UpdateIngestionInput", +)({ + id: SerialSchema, + status: IngestionStatus, + startedAt: Schema.NullOr(Schema.DateFromSelf), + completedAt: Schema.NullOr(Schema.DateFromSelf), + recordsProcessed: Schema.optional(Schema.Number), + recordsCreated: Schema.optional(Schema.Number), + recordsUpdated: Schema.optional(Schema.Number), + recordsSkipped: Schema.optional(Schema.Number), + durationMs: Schema.NullOr(Schema.Number), + errorMessage: Schema.NullOr(Schema.String), + errorStack: Schema.NullOr(Schema.String), + rawDataUrl: Schema.NullOr(Schema.String), + manifestVersion: Schema.NullOr(Schema.Number), +}) {} + +// ============================================================================= +// QUERY SCHEMAS +// ============================================================================= + +export class GetByLeagueInput extends Schema.Class( + "GetByLeagueInput", +)({ + leagueCode: LeagueCode, +}) {} + +export class GetBySeasonInput extends Schema.Class( + "GetBySeasonInput", +)({ + seasonId: SerialSchema, +}) {} + +export class GetByLeagueAndYearInput extends Schema.Class( + "GetByLeagueAndYearInput", +)({ + leagueCode: LeagueCode, + year: Schema.Number, +}) {} + +export class GetStandingsInput extends Schema.Class( + "GetStandingsInput", +)({ + seasonId: SerialSchema, + snapshotDate: Schema.optional(Schema.DateFromSelf), +}) {} diff --git a/packages/core/src/pro-league/pro-league.service.ts b/packages/core/src/pro-league/pro-league.service.ts new file mode 100644 index 00000000..f9ef2be3 --- /dev/null +++ b/packages/core/src/pro-league/pro-league.service.ts @@ -0,0 +1,438 @@ +import { Effect, Schema } from "effect"; + +import { parsePostgresError } from "../util"; + +import { + ProLeagueNotFoundError, + ProSeasonNotFoundError, +} from "./pro-league.error"; +import { ProLeagueRepo } from "./pro-league.repo"; +import type { + CreateLeagueInput, + LeagueCode, + UpsertGameInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertSeasonInput, + UpsertStandingsInput, + UpsertTeamInput, +} from "./pro-league.schema"; +import { + GetByLeagueAndYearInput, + GetByLeagueInput, + GetBySeasonInput, + GetStandingsInput, +} from "./pro-league.schema"; + +export class ProLeagueService extends Effect.Service()( + "ProLeagueService", + { + effect: Effect.gen(function* () { + const repo = yield* ProLeagueRepo; + + return { + // ===================================================================== + // LEAGUE OPERATIONS + // ===================================================================== + + /** + * Get league by code (pll, nll, mll, msl, wla) + */ + getLeagueByCode: (code: LeagueCode) => + repo.getLeagueByCode(code).pipe( + Effect.catchTag("NoSuchElementException", () => + Effect.fail( + new ProLeagueNotFoundError({ + message: `League not found: ${code}`, + code, + }), + ), + ), + Effect.tapError((e) => Effect.logError("Failed to get league", e)), + ), + + /** + * Create or update a league + */ + upsertLeague: (input: CreateLeagueInput) => + repo.upsertLeague(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tap((league) => + Effect.log(`Upserted league: ${league.code}`), + ), + Effect.tapError((e) => + Effect.logError("Failed to upsert league", e), + ), + ), + + /** + * List all active leagues + */ + listLeagues: () => + repo.listLeagues().pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to list leagues", e), + ), + ), + + // ===================================================================== + // SEASON OPERATIONS + // ===================================================================== + + /** + * Get season by league code and year + */ + getSeasonByLeagueAndYear: (input: GetByLeagueAndYearInput) => + Effect.gen(function* () { + const decoded = yield* Schema.decode(GetByLeagueAndYearInput)( + input, + ); + const league = yield* repo.getLeagueByCode(decoded.leagueCode).pipe( + Effect.catchTag("NoSuchElementException", () => + Effect.fail( + new ProLeagueNotFoundError({ + message: `League not found: ${decoded.leagueCode}`, + code: decoded.leagueCode, + }), + ), + ), + ); + return yield* repo.getSeasonByYear(league.id, decoded.year).pipe( + Effect.catchTag("NoSuchElementException", () => + Effect.fail( + new ProSeasonNotFoundError({ + message: `Season not found: ${decoded.leagueCode} ${decoded.year}`, + leagueId: league.id, + year: decoded.year, + }), + ), + ), + ); + }).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => Effect.logError("Failed to get season", e)), + ), + + /** + * Upsert a season + */ + upsertSeason: (input: UpsertSeasonInput) => + repo.upsertSeason(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tap((season) => + Effect.log(`Upserted season: ${season.displayName}`), + ), + Effect.tapError((e) => + Effect.logError("Failed to upsert season", e), + ), + ), + + /** + * List seasons for a league + */ + listSeasons: (input: GetByLeagueInput) => + Effect.gen(function* () { + const decoded = yield* Schema.decode(GetByLeagueInput)(input); + const league = yield* repo.getLeagueByCode(decoded.leagueCode).pipe( + Effect.catchTag("NoSuchElementException", () => + Effect.fail( + new ProLeagueNotFoundError({ + message: `League not found: ${decoded.leagueCode}`, + code: decoded.leagueCode, + }), + ), + ), + ); + return yield* repo.listSeasons(league.id); + }).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to list seasons", e), + ), + ), + + // ===================================================================== + // TEAM OPERATIONS + // ===================================================================== + + /** + * Upsert a team + */ + upsertTeam: (input: UpsertTeamInput) => + repo.upsertTeam(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tap((team) => Effect.log(`Upserted team: ${team.name}`)), + Effect.tapError((e) => Effect.logError("Failed to upsert team", e)), + ), + + /** + * Bulk upsert teams + */ + bulkUpsertTeams: (inputs: UpsertTeamInput[]) => + repo.bulkUpsertTeams(inputs).pipe( + Effect.tap((teams) => Effect.log(`Upserted ${teams.length} teams`)), + Effect.tapError((e) => + Effect.logError("Failed to bulk upsert teams", e), + ), + ), + + /** + * List teams for a league + */ + listTeams: (leagueId: number) => + repo.listTeams(leagueId).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => Effect.logError("Failed to list teams", e)), + ), + + /** + * Get team by external ID + */ + getTeamByExternalId: (leagueId: number, externalId: string) => + repo.getTeamByExternalId(leagueId, externalId), + + // ===================================================================== + // PLAYER OPERATIONS + // ===================================================================== + + /** + * Upsert a player + */ + upsertPlayer: (input: UpsertPlayerInput) => + repo.upsertPlayer(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tap((player) => + Effect.log(`Upserted player: ${player.lastName}`), + ), + Effect.tapError((e) => + Effect.logError("Failed to upsert player", e), + ), + ), + + /** + * Bulk upsert players + */ + bulkUpsertPlayers: (inputs: UpsertPlayerInput[]) => + repo.bulkUpsertPlayers(inputs).pipe( + Effect.tap((players) => + Effect.log(`Upserted ${players.length} players`), + ), + Effect.tapError((e) => + Effect.logError("Failed to bulk upsert players", e), + ), + ), + + /** + * List players for a league + */ + listPlayers: (leagueId: number) => + repo.listPlayers(leagueId).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to list players", e), + ), + ), + + /** + * Get player by external ID + */ + getPlayerByExternalId: (leagueId: number, externalId: string) => + repo.getPlayerByExternalId(leagueId, externalId), + + // ===================================================================== + // PLAYER SEASON OPERATIONS + // ===================================================================== + + /** + * Upsert player season stats + */ + upsertPlayerSeason: (input: UpsertPlayerSeasonInput) => + repo.upsertPlayerSeason(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to upsert player season", e), + ), + ), + + /** + * Bulk upsert player season stats + */ + bulkUpsertPlayerSeasons: (inputs: UpsertPlayerSeasonInput[]) => + repo.bulkUpsertPlayerSeasons(inputs).pipe( + Effect.tap((seasons) => + Effect.log(`Upserted ${seasons.length} player seasons`), + ), + Effect.tapError((e) => + Effect.logError("Failed to bulk upsert player seasons", e), + ), + ), + + /** + * Get all player seasons for a season + */ + getPlayerSeasons: (input: GetBySeasonInput) => + Effect.gen(function* () { + const decoded = yield* Schema.decode(GetBySeasonInput)(input); + return yield* repo.getPlayerSeasons(decoded.seasonId); + }).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to get player seasons", e), + ), + ), + + // ===================================================================== + // GAME OPERATIONS + // ===================================================================== + + /** + * Upsert a game + */ + upsertGame: (input: UpsertGameInput) => + repo.upsertGame(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tap((game) => Effect.log(`Upserted game: ${game.publicId}`)), + Effect.tapError((e) => Effect.logError("Failed to upsert game", e)), + ), + + /** + * Bulk upsert games + */ + bulkUpsertGames: (inputs: UpsertGameInput[]) => + repo.bulkUpsertGames(inputs).pipe( + Effect.tap((games) => Effect.log(`Upserted ${games.length} games`)), + Effect.tapError((e) => + Effect.logError("Failed to bulk upsert games", e), + ), + ), + + /** + * List games for a season + */ + listGames: (input: GetBySeasonInput) => + Effect.gen(function* () { + const decoded = yield* Schema.decode(GetBySeasonInput)(input); + return yield* repo.listGames(decoded.seasonId); + }).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => Effect.logError("Failed to list games", e)), + ), + + // ===================================================================== + // STANDINGS OPERATIONS + // ===================================================================== + + /** + * Upsert standings + */ + upsertStandings: (input: UpsertStandingsInput) => + repo.upsertStandings(input).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to upsert standings", e), + ), + ), + + /** + * Bulk upsert standings + */ + bulkUpsertStandings: (inputs: UpsertStandingsInput[]) => + repo.bulkUpsertStandings(inputs).pipe( + Effect.tap((standings) => + Effect.log(`Upserted ${standings.length} standings entries`), + ), + Effect.tapError((e) => + Effect.logError("Failed to bulk upsert standings", e), + ), + ), + + /** + * Get standings for a season (optionally for a specific snapshot date) + */ + getStandings: (input: GetStandingsInput) => + Effect.gen(function* () { + const decoded = yield* Schema.decode(GetStandingsInput)(input); + return yield* repo.getStandings( + decoded.seasonId, + decoded.snapshotDate, + ); + }).pipe( + Effect.catchTag("SqlError", (e) => + Effect.fail(parsePostgresError(e)), + ), + Effect.tapError((e) => + Effect.logError("Failed to get standings", e), + ), + ), + + // ===================================================================== + // INGESTION OPERATIONS + // ===================================================================== + + /** + * Create a new ingestion record + */ + createIngestion: repo.createIngestion, + + /** + * Update an ingestion record + */ + updateIngestion: repo.updateIngestion, + + /** + * Get recent ingestion records for a league + */ + getRecentIngestions: repo.getRecentIngestions, + + // ===================================================================== + // DIRECT REPO ACCESS (for pipeline use) + // ===================================================================== + + /** + * Get league by code (returns Option-like behavior) + */ + getLeagueByCodeOptional: repo.getLeagueByCode, + + /** + * Get season by external ID + */ + getSeasonByExternalId: repo.getSeasonByExternalId, + + /** + * Get season by year + */ + getSeasonByYear: repo.getSeasonByYear, + } as const; + }), + dependencies: [ProLeagueRepo.Default], + }, +) {} diff --git a/packages/core/src/pro-league/pro-league.sql.ts b/packages/core/src/pro-league/pro-league.sql.ts new file mode 100644 index 00000000..2c121c93 --- /dev/null +++ b/packages/core/src/pro-league/pro-league.sql.ts @@ -0,0 +1,366 @@ +import { + type AnyPgColumn, + boolean, + date, + index, + integer, + jsonb, + pgTable, + text, + unique, + varchar, +} from "drizzle-orm/pg-core"; + +import { ids, timestamp, timestamps } from "../drizzle/drizzle.type"; + +import type { GoalieStats, PlayerStats, TeamStats } from "./pro-league.types"; + +// ============================================================================= +// PRO LEAGUE - Reference table for professional lacrosse leagues +// ============================================================================= + +export const proLeagueTable = pgTable( + "pro_league", + { + ...ids, + code: varchar("code", { length: 10 }).notNull().unique(), // pll, nll, mll, msl, wla + name: text("name").notNull(), // "Premier Lacrosse League" + shortName: text("short_name"), // "PLL" + country: varchar("country", { length: 2 }), // "US", "CA" + isActive: boolean("is_active").notNull().default(true), + foundedYear: integer("founded_year"), + defunctYear: integer("defunct_year"), // For MLL (2020) + websiteUrl: text("website_url"), + logoUrl: text("logo_url"), + ...timestamps, + }, + (table) => [ + index("idx_pro_league_code").on(table.code), + index("idx_pro_league_active").on(table.isActive), + ], +); + +export type ProLeague = typeof proLeagueTable.$inferSelect; +export type ProLeagueInsert = typeof proLeagueTable.$inferInsert; + +// ============================================================================= +// PRO SEASON - Season per league with external season identifiers +// ============================================================================= + +export const proSeasonTable = pgTable( + "pro_season", + { + ...ids, + leagueId: integer("league_id") + .notNull() + .references(() => proLeagueTable.id, { onDelete: "cascade" }), + externalId: text("external_id").notNull(), // League-specific season ID (e.g., "225" for NLL) + year: integer("year").notNull(), // Primary year (e.g., 2024 for 2024-25) + displayName: text("display_name").notNull(), // "2024-25" or "2024" + startDate: date("start_date", { mode: "date" }), + endDate: date("end_date", { mode: "date" }), + isCurrent: boolean("is_current").notNull().default(false), + ...timestamps, + }, + (table) => [ + unique("pro_season_league_external").on(table.leagueId, table.externalId), + index("idx_pro_season_league").on(table.leagueId), + index("idx_pro_season_year").on(table.year), + index("idx_pro_season_current").on(table.isCurrent), + ], +); + +export type ProSeason = typeof proSeasonTable.$inferSelect; +export type ProSeasonInsert = typeof proSeasonTable.$inferInsert; + +// ============================================================================= +// PRO TEAM - Teams with external IDs, logos, colors +// ============================================================================= + +export const proTeamTable = pgTable( + "pro_team", + { + ...ids, + leagueId: integer("league_id") + .notNull() + .references(() => proLeagueTable.id, { onDelete: "cascade" }), + externalId: text("external_id").notNull(), // League-specific team ID + code: varchar("code", { length: 10 }), // Team code/abbreviation (e.g., "ATL", "TOR") + name: text("name").notNull(), // Full name (e.g., "Atlas Lacrosse Club") + shortName: text("short_name"), // Short name (e.g., "Atlas") + city: text("city"), // Home city + logoUrl: text("logo_url"), + primaryColor: varchar("primary_color", { length: 7 }), // Hex color (#RRGGBB) + secondaryColor: varchar("secondary_color", { length: 7 }), + websiteUrl: text("website_url"), + isActive: boolean("is_active").notNull().default(true), + // Tracking when team was active + firstSeasonYear: integer("first_season_year"), + lastSeasonYear: integer("last_season_year"), // null if still active + ...timestamps, + }, + (table) => [ + unique("pro_team_league_external").on(table.leagueId, table.externalId), + index("idx_pro_team_league").on(table.leagueId), + index("idx_pro_team_code").on(table.code), + index("idx_pro_team_active").on(table.isActive), + ], +); + +export type ProTeam = typeof proTeamTable.$inferSelect; +export type ProTeamInsert = typeof proTeamTable.$inferInsert; + +// ============================================================================= +// PRO PLAYER - Players with external IDs, demographics +// Cross-league matching via canonical_player_id self-reference +// ============================================================================= + +export const proPlayerTable = pgTable( + "pro_player", + { + ...ids, + leagueId: integer("league_id") + .notNull() + .references(() => proLeagueTable.id, { onDelete: "cascade" }), + externalId: text("external_id").notNull(), // League-specific player ID + // Cross-league matching: points to "master" record across leagues + canonicalPlayerId: integer("canonical_player_id").references( + (): AnyPgColumn => proPlayerTable.id, + { onDelete: "set null" }, + ), + firstName: text("first_name"), + lastName: text("last_name").notNull(), + fullName: text("full_name"), // Computed or source-provided + position: varchar("position", { length: 20 }), // A, M, D, G, SSDM, LSM, FO + dateOfBirth: date("date_of_birth", { mode: "date" }), + birthplace: text("birthplace"), // City, State/Province + country: varchar("country", { length: 2 }), + height: varchar("height", { length: 10 }), // "6'2" or "188cm" + weight: integer("weight"), // in lbs + handedness: varchar("handedness", { length: 5 }), // "L", "R" + college: text("college"), + highSchool: text("high_school"), + profileUrl: text("profile_url"), + photoUrl: text("photo_url"), + ...timestamps, + }, + (table) => [ + unique("pro_player_league_external").on(table.leagueId, table.externalId), + index("idx_pro_player_league").on(table.leagueId), + index("idx_pro_player_name").on(table.lastName, table.firstName), + index("idx_pro_player_canonical").on(table.canonicalPlayerId), + index("idx_pro_player_position").on(table.position), + ], +); + +export type ProPlayer = typeof proPlayerTable.$inferSelect; +export type ProPlayerInsert = typeof proPlayerTable.$inferInsert; + +// ============================================================================= +// PRO PLAYER SEASON - Junction table: roster + stats per season +// JSONB for league-varying stats structures +// ============================================================================= + +export const proPlayerSeasonTable = pgTable( + "pro_player_season", + { + ...ids, + playerId: integer("player_id") + .notNull() + .references(() => proPlayerTable.id, { onDelete: "cascade" }), + seasonId: integer("season_id") + .notNull() + .references(() => proSeasonTable.id, { onDelete: "cascade" }), + teamId: integer("team_id").references(() => proTeamTable.id, { + onDelete: "set null", + }), + // Roster info + jerseyNumber: integer("jersey_number"), + position: varchar("position", { length: 20 }), // May differ from player.position + isCaptain: boolean("is_captain").default(false), + // Stats - JSONB for flexibility across leagues + stats: jsonb("stats").$type(), + postSeasonStats: jsonb("post_season_stats").$type(), + // For goalies - separate stats structure + goalieStats: jsonb("goalie_stats").$type(), + postSeasonGoalieStats: jsonb( + "post_season_goalie_stats", + ).$type(), + // Metadata + gamesPlayed: integer("games_played").default(0), + ...timestamps, + }, + (table) => [ + unique("pro_player_season_unique").on(table.playerId, table.seasonId), + index("idx_pro_player_season_player").on(table.playerId), + index("idx_pro_player_season_season").on(table.seasonId), + index("idx_pro_player_season_team").on(table.teamId), + ], +); + +export type ProPlayerSeason = typeof proPlayerSeasonTable.$inferSelect; +export type ProPlayerSeasonInsert = typeof proPlayerSeasonTable.$inferInsert; + +// ============================================================================= +// PRO GAME - Games with scores, venue, status +// ============================================================================= + +export const proGameTable = pgTable( + "pro_game", + { + ...ids, + seasonId: integer("season_id") + .notNull() + .references(() => proSeasonTable.id, { onDelete: "cascade" }), + externalId: text("external_id"), // League-specific game ID + homeTeamId: integer("home_team_id") + .notNull() + .references(() => proTeamTable.id, { onDelete: "cascade" }), + awayTeamId: integer("away_team_id") + .notNull() + .references(() => proTeamTable.id, { onDelete: "cascade" }), + // Scheduling + gameDate: timestamp("game_date").notNull(), + week: varchar("week", { length: 20 }), // "Week 1", "Championship Series" + gameNumber: integer("game_number"), // Within season + // Venue + venue: text("venue"), + venueCity: text("venue_city"), + // Status + status: varchar("status", { length: 20 }).notNull().default("scheduled"), + // 'scheduled', 'in_progress', 'final', 'postponed', 'cancelled' + // Scores + homeScore: integer("home_score"), + awayScore: integer("away_score"), + // Overtime/shootout tracking + isOvertime: boolean("is_overtime").default(false), + overtimePeriods: integer("overtime_periods").default(0), + // Play-by-play stored in R2 + playByPlayUrl: text("play_by_play_url"), // R2 URL reference + // Team stats for this game (JSONB) + homeTeamStats: jsonb("home_team_stats").$type(), + awayTeamStats: jsonb("away_team_stats").$type(), + // Broadcast info + broadcaster: text("broadcaster"), + streamUrl: text("stream_url"), + ...timestamps, + }, + (table) => [ + unique("pro_game_season_external").on(table.seasonId, table.externalId), + index("idx_pro_game_season").on(table.seasonId), + index("idx_pro_game_home_team").on(table.homeTeamId), + index("idx_pro_game_away_team").on(table.awayTeamId), + index("idx_pro_game_date").on(table.gameDate), + index("idx_pro_game_status").on(table.status), + ], +); + +export type ProGame = typeof proGameTable.$inferSelect; +export type ProGameInsert = typeof proGameTable.$inferInsert; + +// ============================================================================= +// PRO STANDINGS - Team standings per season (snapshot-based for time-series) +// ============================================================================= + +export const proStandingsTable = pgTable( + "pro_standings", + { + ...ids, + seasonId: integer("season_id") + .notNull() + .references(() => proSeasonTable.id, { onDelete: "cascade" }), + teamId: integer("team_id") + .notNull() + .references(() => proTeamTable.id, { onDelete: "cascade" }), + // Snapshot date for time-series analysis + snapshotDate: date("snapshot_date", { mode: "date" }).notNull(), + // Core standings + position: integer("position").notNull(), // Rank in standings + wins: integer("wins").notNull().default(0), + losses: integer("losses").notNull().default(0), + ties: integer("ties").default(0), + overtimeLosses: integer("overtime_losses").default(0), + // Points (some leagues use points system) + points: integer("points"), + winPercentage: integer("win_percentage"), // Stored as integer (e.g., 750 = .750) + // Goals + goalsFor: integer("goals_for").default(0), + goalsAgainst: integer("goals_against").default(0), + goalDifferential: integer("goal_differential").default(0), + // Conference/Division (if applicable) + conference: varchar("conference", { length: 50 }), + division: varchar("division", { length: 50 }), + // Playoff qualification + clinchStatus: varchar("clinch_status", { length: 10 }), // 'x', 'y', 'z', 'e' + seed: integer("seed"), + ...timestamps, + }, + (table) => [ + // Unique per team per season per date + unique("pro_standings_season_team_date").on( + table.seasonId, + table.teamId, + table.snapshotDate, + ), + index("idx_pro_standings_season").on(table.seasonId), + index("idx_pro_standings_team").on(table.teamId), + index("idx_pro_standings_date").on(table.snapshotDate), + index("idx_pro_standings_position").on(table.position), + ], +); + +export type ProStandings = typeof proStandingsTable.$inferSelect; +export type ProStandingsInsert = typeof proStandingsTable.$inferInsert; + +// ============================================================================= +// PRO DATA INGESTION - Extraction provenance tracking +// ============================================================================= + +export const proDataIngestionTable = pgTable( + "pro_data_ingestion", + { + ...ids, + leagueId: integer("league_id") + .notNull() + .references(() => proLeagueTable.id, { onDelete: "cascade" }), + seasonId: integer("season_id").references(() => proSeasonTable.id, { + onDelete: "set null", + }), + // What was extracted + entityType: varchar("entity_type", { length: 50 }).notNull(), + // 'teams', 'players', 'games', 'standings', 'play_by_play', 'full_season' + // Source info + sourceUrl: text("source_url"), + sourceType: varchar("source_type", { length: 20 }), // 'api', 'scrape', 'wayback' + // Status + status: varchar("status", { length: 20 }).notNull().default("pending"), + // 'pending', 'running', 'completed', 'failed' + startedAt: timestamp("started_at"), + completedAt: timestamp("completed_at"), + // Metrics + recordsProcessed: integer("records_processed").default(0), + recordsCreated: integer("records_created").default(0), + recordsUpdated: integer("records_updated").default(0), + recordsSkipped: integer("records_skipped").default(0), + // Duration in milliseconds + durationMs: integer("duration_ms"), + // Error tracking + errorMessage: text("error_message"), + errorStack: text("error_stack"), + // Raw extraction output location in R2 + rawDataUrl: text("raw_data_url"), + // Manifest version for incremental extractions + manifestVersion: integer("manifest_version"), + ...timestamps, + }, + (table) => [ + index("idx_pro_ingestion_league").on(table.leagueId), + index("idx_pro_ingestion_season").on(table.seasonId), + index("idx_pro_ingestion_entity").on(table.entityType), + index("idx_pro_ingestion_status").on(table.status), + index("idx_pro_ingestion_created").on(table.createdAt), + ], +); + +export type ProDataIngestion = typeof proDataIngestionTable.$inferSelect; +export type ProDataIngestionInsert = typeof proDataIngestionTable.$inferInsert; diff --git a/packages/core/src/pro-league/pro-league.types.ts b/packages/core/src/pro-league/pro-league.types.ts new file mode 100644 index 00000000..b89a74da --- /dev/null +++ b/packages/core/src/pro-league/pro-league.types.ts @@ -0,0 +1,316 @@ +/** + * JSONB type definitions for pro-league stats + * + * These types are used with Drizzle's $type() to provide type safety + * for JSONB columns. The stats structures vary by league, so we use a + * flexible structure with optional fields. + */ + +// ============================================================================= +// PLAYER STATS (Field players - A, M, D, SSDM, LSM, FO) +// ============================================================================= + +/** + * Field player stats - covers offense, defense, and specialist positions + * All fields optional to handle league variations + */ +export interface PlayerStats { + // Games + gamesPlayed?: number; + gamesStarted?: number; + + // Offense + goals?: number; + onePointGoals?: number; + twoPointGoals?: number; // PLL-specific + assists?: number; + points?: number; + scoringPoints?: number; + + // Shooting + shots?: number; + shotsOnGoal?: number; + shotPct?: number; + shotsOnGoalPct?: number; + twoPointShots?: number; + twoPointShotPct?: number; + twoPointShotsOnGoal?: number; + twoPointShotsOnGoalPct?: number; + + // Possession + groundBalls?: number; + looseBalls?: number; + turnovers?: number; + causedTurnovers?: number; + + // Faceoffs (FO specialists) + faceoffsWon?: number; + faceoffsLost?: number; + faceoffs?: number; + faceoffPct?: number; + foRecord?: string; // "150-75" format + + // Defense + blockedShots?: number; + + // Penalties + penaltyMinutes?: number; + numPenalties?: number; + pim?: number; + pimValue?: number; + + // Power play / Short-handed + powerPlayGoals?: number; + powerPlayAssists?: number; + powerPlayShots?: number; + shortHandedGoals?: number; + shortHandedShots?: number; + + // Plus/Minus + plusMinus?: number; + + // Time on field (seconds) + tof?: number; + timeOnField?: number; + + // Advanced (PLL-specific) + touches?: number; + totalPasses?: number; + unassistedGoals?: number; + assistedGoals?: number; + passRate?: number; + shotRate?: number; + goalRate?: number; + assistRate?: number; + turnoverRate?: number; + + // Shot breakdown (PLL advanced) + lhShots?: number; // Left-hand shots + lhGoals?: number; + lhShotPct?: number; + rhShots?: number; // Right-hand shots + rhGoals?: number; + rhShotPct?: number; + + // Goal types (PLL advanced) + settledGoals?: number; + fastbreakGoals?: number; + substitutionGoals?: number; + doorstepGoals?: number; + + // Assist types (PLL advanced) + assistOpportunities?: number; + settledAssists?: number; + fastbreakAssists?: number; + dodgeAssists?: number; + pnrAssists?: number; // Pick and roll + + // Injury (if tracked) + injuryStatus?: string; + injuryDescription?: string; +} + +// ============================================================================= +// GOALIE STATS +// ============================================================================= + +/** + * Goalie-specific statistics + * All fields optional to handle league variations + */ +export interface GoalieStats { + // Games + gamesPlayed?: number; + gamesStarted?: number; + minutes?: number; + + // Record + wins?: number; + losses?: number; + ties?: number; + + // Saves + saves?: number; + savePct?: number; + + // Goals against + goalsAgainst?: number; + goalsAgainstAvg?: number; // GAA + twoPointGoalsAgainst?: number; + twoPtGaa?: number; + scoresAgainst?: number; // Total points allowed (PLL) + saa?: number; // Scores against average + + // Power play / Short-handed + powerPlayGoalsAgainst?: number; + shortHandedGoalsAgainst?: number; + powerPlayShotsAgainst?: number; + shortHandedShotsAgainst?: number; + + // Clearing + clears?: number; + clearAttempts?: number; + clearPct?: number; + + // Advanced + shotsFaced?: number; + highSaves?: number; + lowSaves?: number; + stickSaves?: number; + + // Time + timeOnField?: number; +} + +// ============================================================================= +// TEAM STATS +// ============================================================================= + +/** + * Team-level statistics (typically for a game or season aggregate) + * All fields optional to handle league variations + */ +export interface TeamStats { + // Games + gamesPlayed?: number; + + // Scoring + scores?: number; // Total points (PLL: 1pt + 2pt) + scoresAgainst?: number; + goals?: number; + goalsAgainst?: number; + onePointGoals?: number; + twoPointGoals?: number; + twoPointGoalsAgainst?: number; + assists?: number; + + // Shooting + shots?: number; + shotsOnGoal?: number; + shotPct?: number; + shotsOnGoalPct?: number; + twoPointShots?: number; + twoPointShotPct?: number; + twoPointShotsOnGoal?: number; + twoPointShotsOnGoalPct?: number; + + // Possession + groundBalls?: number; + turnovers?: number; + causedTurnovers?: number; + + // Faceoffs + faceoffsWon?: number; + faceoffsLost?: number; + faceoffs?: number; + faceoffPct?: number; + + // Goaltending + saves?: number; + savePct?: number; + saa?: number; + + // Penalties + numPenalties?: number; + pim?: number; + penaltyMinutes?: number; + offsides?: number; + shotClockExpirations?: number; + + // Special teams + powerPlayGoals?: number; + powerPlayShots?: number; + powerPlayPct?: number; + powerPlayGoalsAgainst?: number; + powerPlayShotsAgainst?: number; + powerPlayGoalsAgainstPct?: number; + shortHandedGoals?: number; + shortHandedShots?: number; + shortHandedPct?: number; + shortHandedShotsAgainst?: number; + shortHandedGoalsAgainst?: number; + shortHandedGoalsAgainstPct?: number; + manDownPct?: number; + timesManUp?: number; + timesShortHanded?: number; + + // Clearing/Rides + clears?: number; + clearAttempts?: number; + clearPct?: number; + rides?: number; + rideAttempts?: number; + ridesPct?: number; + + // Per game averages + scoresPG?: number; + shotsPG?: number; + + // Advanced + touches?: number; + totalPasses?: number; +} + +// ============================================================================= +// PLAY-BY-PLAY TYPES (for R2 storage) +// ============================================================================= + +/** + * Single play/action in a game + */ +export interface PlayByPlayAction { + id: number | string; + period: number; + minutes: number; + seconds: number; + teamId?: string; + playerId?: string; + playerName?: string; + actionType: string; // 'goal', 'save', 'faceoff', 'penalty', etc. + description?: string; + // Additional context + assistPlayerId?: string; + assistPlayerName?: string; + isGoal?: boolean; + isTwoPoint?: boolean; + isPowerPlay?: boolean; + isShortHanded?: boolean; +} + +/** + * Full play-by-play for a game (stored in R2) + */ +export interface GamePlayByPlay { + gameId: string; + seasonId: string; + leagueCode: string; + homeTeamId: string; + awayTeamId: string; + actions: PlayByPlayAction[]; + extractedAt: string; // ISO timestamp + source: string; +} + +// ============================================================================= +// LEAGUE-SPECIFIC TYPE GUARDS +// ============================================================================= + +/** + * Check if stats have PLL-specific fields + */ +export const isPLLStats = (stats: PlayerStats): boolean => + stats.twoPointGoals !== undefined || stats.touches !== undefined; + +/** + * Check if stats have NLL-specific fields + */ +export const isNLLStats = (stats: PlayerStats): boolean => + stats.looseBalls !== undefined && stats.twoPointGoals === undefined; + +/** + * Check if goalie stats are present + */ +export const hasGoalieStats = ( + stats: GoalieStats | null | undefined, +): stats is GoalieStats => + stats !== null && stats !== undefined && stats.saves !== undefined; diff --git a/packages/core/src/runtime.server.ts b/packages/core/src/runtime.server.ts index a6d08f4a..675bd211 100644 --- a/packages/core/src/runtime.server.ts +++ b/packages/core/src/runtime.server.ts @@ -6,6 +6,7 @@ import { GameService } from "./game/game.service"; import { OrganizationService } from "./organization/organization.service"; import { PlayerContactInfoService } from "./player/contact-info/contact-info.service"; import { PlayerService } from "./player/player.service"; +import { ProLeagueService } from "./pro-league/pro-league.service"; import { SeasonService } from "./season/season.service"; import { TeamService } from "./team/team.service"; @@ -18,6 +19,7 @@ const MainLayer = Layer.mergeAll( PlayerService.Default, PlayerContactInfoService.Default, FeedbackService.Default, + ProLeagueService.Default, ); export const RuntimeServer = ManagedRuntime.make(MainLayer); diff --git a/packages/pipeline/src/load/index.ts b/packages/pipeline/src/load/index.ts new file mode 100644 index 00000000..be85238c --- /dev/null +++ b/packages/pipeline/src/load/index.ts @@ -0,0 +1,57 @@ +/** + * Load Module - Transform & Load extracted data to database + * + * Usage: + * import { transformNLLTeams, transformPLLPlayers } from "@laxdb/pipeline/load"; + */ + +// Transform types and utilities +export { + createTransformContext, + type TransformContext, + type TransformResult, + type UpsertTeamInput, + type UpsertPlayerInput, + type UpsertPlayerSeasonInput, + type UpsertGameInput, + type UpsertStandingsInput, + type UpsertSeasonInput, + type LeagueCode, +} from "./transform.types"; + +// NLL transforms +export { + transformNLLSeason, + transformNLLTeam, + transformNLLTeams, + transformNLLPlayer, + transformNLLPlayers, + transformNLLPlayerStats, + transformNLLBasicStats, + transformNLLPlayerSeason, + transformNLLGame, + transformNLLGames, + transformNLLStanding, + transformNLLStandings, +} from "./nll.transform"; + +// PLL transforms +export { + transformPLLSeason, + transformPLLTeam, + transformPLLTeams, + transformPLLPlayer, + transformPLLPlayers, + transformPLLPlayerStats, + transformPLLGoalieStats, + transformPLLTeamStats, + transformPLLPlayerSeason, + transformPLLGame, + transformPLLGames, + transformPLLStanding, + transformPLLStandings, +} from "./pll.transform"; + +// Load service +export { LoadService, makeLoadService, LoadError } from "./load.service"; +export type { LoadResult } from "./load.service"; diff --git a/packages/pipeline/src/load/load.service.ts b/packages/pipeline/src/load/load.service.ts new file mode 100644 index 00000000..1b981201 --- /dev/null +++ b/packages/pipeline/src/load/load.service.ts @@ -0,0 +1,493 @@ +/** + * Load Service + * + * Orchestrates loading extracted data into the pro_* tables. + * Handles the full workflow: read extracted files → transform → upsert. + */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +import type { ProLeagueService, LeagueCode } from "@laxdb/core/pro-league"; +import { Effect, Context } from "effect"; + +import type { + NLLTeam, + NLLPlayer, + NLLStanding, + NLLMatch, +} from "../nll/nll.schema"; +import type { + PLLTeam, + PLLPlayer, + PLLTeamStanding, + PLLEvent, +} from "../pll/pll.schema"; + +import { + transformNLLSeason, + transformNLLTeams, + transformNLLPlayers, + transformNLLPlayerSeason, + transformNLLGames, + transformNLLStandings, +} from "./nll.transform"; +import { + transformPLLSeason, + transformPLLTeams, + transformPLLPlayers, + transformPLLPlayerSeason, + transformPLLGames, + transformPLLStandings, +} from "./pll.transform"; +import { + createTransformContext, + type TransformContext, +} from "./transform.types"; + +// ============================================================================= +// SERVICE TAG +// ============================================================================= + +export class LoadService extends Context.Tag("LoadService")< + LoadService, + { + readonly loadNLLSeason: ( + seasonId: string, + outputDir: string, + ) => Effect.Effect; + readonly loadPLLSeason: ( + year: number, + outputDir: string, + ) => Effect.Effect; + } +>() {} + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface LoadResult { + leagueCode: LeagueCode; + seasonYear: number; + teamsLoaded: number; + playersLoaded: number; + playerSeasonsLoaded: number; + gamesLoaded: number; + standingsLoaded: number; +} + +export class LoadError extends Error { + readonly _tag = "LoadError"; + readonly errorCause?: unknown; + + constructor(message: string, cause?: unknown) { + super(message); + this.name = "LoadError"; + this.errorCause = cause; + } +} + +// ============================================================================= +// HELPERS +// ============================================================================= + +const readJsonFile = (filePath: string): Effect.Effect => + Effect.tryPromise({ + try: async () => { + const content = await fs.readFile(filePath, "utf-8"); + return JSON.parse(content) as T; + }, + catch: (error) => new LoadError(`Failed to read ${filePath}`, error), + }); + +const fileExists = (filePath: string): Effect.Effect => + Effect.promise(async () => { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + }); + +// ============================================================================= +// SERVICE IMPLEMENTATION +// ============================================================================= + +// Service interface type +interface ProLeagueServiceInterface { + upsertLeague: (input: { + code: LeagueCode; + name: string; + shortName?: string; + country?: string; + isActive?: boolean; + foundedYear?: number; + }) => Effect.Effect<{ id: number; code: LeagueCode }, unknown>; + upsertSeason: ( + input: ReturnType, + ) => Effect.Effect<{ id: number }, unknown>; + bulkUpsertTeams: ( + inputs: ReturnType, + ) => Effect.Effect, unknown>; + bulkUpsertPlayers: ( + inputs: ReturnType, + ) => Effect.Effect, unknown>; + bulkUpsertPlayerSeasons: ( + inputs: Array>>, + ) => Effect.Effect; + bulkUpsertGames: ( + inputs: ReturnType, + ) => Effect.Effect; + bulkUpsertStandings: ( + inputs: ReturnType, + ) => Effect.Effect; +} + +/** + * Create LoadService with ProLeagueService dependency + */ +export const makeLoadService = ( + proLeagueService: ProLeagueServiceInterface, +) => { + /** + * Load NLL season data from extracted files + */ + const loadNLLSeason = ( + seasonId: string, + outputDir: string, + ): Effect.Effect => + Effect.gen(function* () { + const leagueCode: LeagueCode = "nll"; + + // 1. Get or create league + const league = yield* proLeagueService + .upsertLeague({ + code: leagueCode, + name: "National Lacrosse League", + shortName: "NLL", + country: "US", + isActive: true, + foundedYear: 1986, + }) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert NLL league", e), + ), + ); + + // 2. Derive year from NLL season ID (225 = 2024-25, 224 = 2023-24, etc.) + const baseYear = 2000 + Math.floor(Number(seasonId) / 10); + const year = baseYear - 1; // Primary year is start year + + // 3. Create/update season + const seasonInput = transformNLLSeason(league.id, seasonId, year); + const season = yield* proLeagueService + .upsertSeason(seasonInput) + .pipe( + Effect.mapError((e) => new LoadError("Failed to upsert season", e)), + ); + + // 4. Initialize transform context + const ctx = createTransformContext( + league.id, + leagueCode, + season.id, + year, + seasonId, + ); + + // 5. Load teams + const teamsPath = path.join(outputDir, "teams.json"); + const teamsExist = yield* fileExists(teamsPath); + let teamsLoaded = 0; + + if (teamsExist) { + const teams = yield* readJsonFile(teamsPath); + const teamInputs = transformNLLTeams(teams, league.id); + const upsertedTeams = yield* proLeagueService + .bulkUpsertTeams(teamInputs) + .pipe( + Effect.mapError((e) => new LoadError("Failed to upsert teams", e)), + ); + + // Build team ID map + for (const team of upsertedTeams) { + ctx.teamIdMap.set(team.externalId, team.id); + } + teamsLoaded = upsertedTeams.length; + } + + // 6. Load players + const playersPath = path.join(outputDir, "players.json"); + const playersExist = yield* fileExists(playersPath); + let playersLoaded = 0; + + if (playersExist) { + const players = yield* readJsonFile(playersPath); + const playerInputs = transformNLLPlayers(players, league.id); + const upsertedPlayers = yield* proLeagueService + .bulkUpsertPlayers(playerInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert players", e), + ), + ); + + // Build player ID map + for (const player of upsertedPlayers) { + ctx.playerIdMap.set(player.externalId, player.id); + } + playersLoaded = upsertedPlayers.length; + + // 7. Load player seasons + const playerSeasonInputs = players + .map((p) => transformNLLPlayerSeason(p, ctx)) + .filter((ps): ps is NonNullable => ps !== null); + + if (playerSeasonInputs.length > 0) { + yield* proLeagueService + .bulkUpsertPlayerSeasons(playerSeasonInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert player seasons", e), + ), + ); + } + } + + // 8. Load games (schedule) + const schedulePath = path.join(outputDir, "schedule.json"); + const scheduleExist = yield* fileExists(schedulePath); + let gamesLoaded = 0; + + if (scheduleExist) { + const matches = yield* readJsonFile(schedulePath); + const gameInputs = transformNLLGames(matches, ctx); + + if (gameInputs.length > 0) { + const upsertedGames = yield* proLeagueService + .bulkUpsertGames(gameInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert games", e), + ), + ); + gamesLoaded = upsertedGames.length; + } + } + + // 9. Load standings + const standingsPath = path.join(outputDir, "standings.json"); + const standingsExist = yield* fileExists(standingsPath); + let standingsLoaded = 0; + + if (standingsExist) { + const standings = yield* readJsonFile(standingsPath); + const snapshotDate = new Date(); // Use current date as snapshot + const standingsInputs = transformNLLStandings( + standings, + ctx, + snapshotDate, + ); + + if (standingsInputs.length > 0) { + const upsertedStandings = yield* proLeagueService + .bulkUpsertStandings(standingsInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert standings", e), + ), + ); + standingsLoaded = upsertedStandings.length; + } + } + + return { + leagueCode, + seasonYear: year, + teamsLoaded, + playersLoaded, + playerSeasonsLoaded: playersLoaded, // Same as players for now + gamesLoaded, + standingsLoaded, + }; + }); + + /** + * Load PLL season data from extracted files + */ + const loadPLLSeason = ( + year: number, + outputDir: string, + ): Effect.Effect => + Effect.gen(function* () { + const leagueCode: LeagueCode = "pll"; + + // 1. Get or create league + const league = yield* proLeagueService + .upsertLeague({ + code: leagueCode, + name: "Premier Lacrosse League", + shortName: "PLL", + country: "US", + isActive: true, + foundedYear: 2019, + }) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert PLL league", e), + ), + ); + + // 2. Create/update season + const seasonInput = transformPLLSeason(league.id, year); + const season = yield* proLeagueService + .upsertSeason(seasonInput) + .pipe( + Effect.mapError((e) => new LoadError("Failed to upsert season", e)), + ); + + // 3. Initialize transform context + const ctx = createTransformContext( + league.id, + leagueCode, + season.id, + year, + String(year), + ); + + // 4. Load teams + const teamsPath = path.join(outputDir, "teams.json"); + const teamsExist = yield* fileExists(teamsPath); + let teamsLoaded = 0; + + if (teamsExist) { + const teamsResponse = yield* readJsonFile<{ allTeams: PLLTeam[] }>( + teamsPath, + ); + const teamInputs = transformPLLTeams(teamsResponse.allTeams, league.id); + const upsertedTeams = yield* proLeagueService + .bulkUpsertTeams(teamInputs) + .pipe( + Effect.mapError((e) => new LoadError("Failed to upsert teams", e)), + ); + + // Build team ID map + for (const team of upsertedTeams) { + ctx.teamIdMap.set(team.externalId, team.id); + } + teamsLoaded = upsertedTeams.length; + } + + // 5. Load players + const playersPath = path.join(outputDir, "players.json"); + const playersExist = yield* fileExists(playersPath); + let playersLoaded = 0; + + if (playersExist) { + const playersResponse = yield* readJsonFile<{ + allPlayers: PLLPlayer[]; + }>(playersPath); + const playerInputs = transformPLLPlayers( + playersResponse.allPlayers, + league.id, + ); + const upsertedPlayers = yield* proLeagueService + .bulkUpsertPlayers(playerInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert players", e), + ), + ); + + // Build player ID map + for (const player of upsertedPlayers) { + ctx.playerIdMap.set(player.externalId, player.id); + } + playersLoaded = upsertedPlayers.length; + + // 6. Load player seasons + const playerSeasonInputs = playersResponse.allPlayers + .map((p) => transformPLLPlayerSeason(p, ctx)) + .filter((ps): ps is NonNullable => ps !== null); + + if (playerSeasonInputs.length > 0) { + yield* proLeagueService + .bulkUpsertPlayerSeasons(playerSeasonInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert player seasons", e), + ), + ); + } + } + + // 7. Load games (events) + const eventsPath = path.join(outputDir, "events.json"); + const eventsExist = yield* fileExists(eventsPath); + let gamesLoaded = 0; + + if (eventsExist) { + const eventsResponse = yield* readJsonFile<{ + data: { items: PLLEvent[] }; + }>(eventsPath); + const gameInputs = transformPLLGames(eventsResponse.data.items, ctx); + + if (gameInputs.length > 0) { + const upsertedGames = yield* proLeagueService + .bulkUpsertGames(gameInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert games", e), + ), + ); + gamesLoaded = upsertedGames.length; + } + } + + // 8. Load standings + const standingsPath = path.join(outputDir, "standings.json"); + const standingsExist = yield* fileExists(standingsPath); + let standingsLoaded = 0; + + if (standingsExist) { + const standingsResponse = yield* readJsonFile<{ + data: { items: PLLTeamStanding[] }; + }>(standingsPath); + const snapshotDate = new Date(); + const standingsInputs = transformPLLStandings( + standingsResponse.data.items, + ctx, + snapshotDate, + ); + + if (standingsInputs.length > 0) { + const upsertedStandings = yield* proLeagueService + .bulkUpsertStandings(standingsInputs) + .pipe( + Effect.mapError( + (e) => new LoadError("Failed to upsert standings", e), + ), + ); + standingsLoaded = upsertedStandings.length; + } + } + + return { + leagueCode, + seasonYear: year, + teamsLoaded, + playersLoaded, + playerSeasonsLoaded: playersLoaded, + gamesLoaded, + standingsLoaded, + }; + }); + + return { + loadNLLSeason, + loadPLLSeason, + }; +}; diff --git a/packages/pipeline/src/load/nll.transform.ts b/packages/pipeline/src/load/nll.transform.ts new file mode 100644 index 00000000..22ff9431 --- /dev/null +++ b/packages/pipeline/src/load/nll.transform.ts @@ -0,0 +1,344 @@ +/** + * NLL Transform Functions + * + * Transform NLL extracted data into pro_* schema format. + */ + +import type { PlayerStats } from "@laxdb/core/pro-league"; + +import type { + NLLTeam, + NLLPlayer, + NLLStanding, + NLLMatch, + NLLPlayerStatsRow, +} from "../nll/nll.schema"; + +import type { + TransformContext, + UpsertTeamInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertGameInput, + UpsertStandingsInput, + UpsertSeasonInput, +} from "./transform.types"; + +// ============================================================================= +// SEASON TRANSFORM +// ============================================================================= + +/** + * Create season input from NLL season info + */ +export const transformNLLSeason = ( + leagueId: number, + seasonId: string, + year: number, +): UpsertSeasonInput => ({ + leagueId, + externalId: seasonId, + year, + displayName: `${year}-${String(year + 1).slice(2)}`, // "2024-25" + startDate: null, + endDate: null, + isCurrent: false, // Will be set based on extraction config +}); + +// ============================================================================= +// TEAM TRANSFORMS +// ============================================================================= + +/** + * Transform NLLTeam to UpsertTeamInput + */ +export const transformNLLTeam = ( + team: NLLTeam, + leagueId: number, +): UpsertTeamInput => ({ + leagueId, + externalId: team.id, + code: team.code, + name: team.displayName ?? team.name ?? team.code, + shortName: team.nickname, + city: team.team_city, + logoUrl: team.team_logo, + primaryColor: null, + secondaryColor: null, + websiteUrl: team.team_website_url, + isActive: true, + firstSeasonYear: null, + lastSeasonYear: null, +}); + +/** + * Transform array of NLLTeams + */ +export const transformNLLTeams = ( + teams: NLLTeam[], + leagueId: number, +): UpsertTeamInput[] => teams.map((team) => transformNLLTeam(team, leagueId)); + +// ============================================================================= +// PLAYER TRANSFORMS +// ============================================================================= + +/** + * Parse NLL date of birth string to Date + * Format: "YYYY-MM-DD" or similar + */ +const parseNLLDate = (dateStr: string | null): Date | null => { + if (!dateStr) return null; + const date = new Date(dateStr); + return Number.isNaN(date.getTime()) ? null : date; +}; + +/** + * Transform NLLPlayer to UpsertPlayerInput + */ +export const transformNLLPlayer = ( + player: NLLPlayer, + leagueId: number, +): UpsertPlayerInput => ({ + leagueId, + externalId: player.personId, + firstName: player.firstname, + lastName: player.surname ?? player.fullname ?? "Unknown", + fullName: player.fullname, + position: player.position, + dateOfBirth: parseNLLDate(player.dateOfBirth), + birthplace: null, + country: null, + height: player.height, + weight: player.weight ? Number.parseInt(player.weight, 10) : null, + handedness: null, + college: null, + highSchool: null, + profileUrl: null, + photoUrl: null, +}); + +/** + * Transform array of NLLPlayers + */ +export const transformNLLPlayers = ( + players: NLLPlayer[], + leagueId: number, +): UpsertPlayerInput[] => + players.map((player) => transformNLLPlayer(player, leagueId)); + +// ============================================================================= +// PLAYER SEASON / STATS TRANSFORMS +// ============================================================================= + +/** + * Check if player is a goalie based on position + */ +const isGoalie = (position: string | null): boolean => + position?.toUpperCase() === "G"; + +/** + * Transform NLLPlayerStatsRow to PlayerStats JSONB + */ +export const transformNLLPlayerStats = ( + statsRow: NLLPlayerStatsRow, +): PlayerStats => ({ + gamesPlayed: statsRow.games_played, + goals: statsRow.goals, + assists: statsRow.assists, + points: statsRow.points, + penaltyMinutes: statsRow.penalty_minutes, + powerPlayGoals: statsRow.ppg, + powerPlayAssists: statsRow.ppa, + shortHandedGoals: statsRow.shg, + looseBalls: statsRow.looseballs, + turnovers: statsRow.turnovers, + causedTurnovers: statsRow.caused_turnovers, + blockedShots: statsRow.blocked_shots, + shotsOnGoal: statsRow.shots_on_goal, +}); + +/** + * Transform NLLPlayer basic season stats (from roster response) + */ +export const transformNLLBasicStats = (player: NLLPlayer): PlayerStats => { + const matches = player.matches; + if (!matches) return { gamesPlayed: 0 }; + + return { + gamesPlayed: matches.games_played, + goals: matches.goals, + assists: matches.assists, + points: matches.points, + penaltyMinutes: matches.penalty_minutes, + }; +}; + +/** + * Transform NLLPlayer to UpsertPlayerSeasonInput + * Requires context with resolved player/team IDs + */ +export const transformNLLPlayerSeason = ( + player: NLLPlayer, + ctx: TransformContext, + statsRow?: NLLPlayerStatsRow, +): UpsertPlayerSeasonInput | null => { + const playerId = ctx.playerIdMap.get(player.personId); + if (!playerId) return null; + + const teamId = player.team_id ? ctx.teamIdMap.get(player.team_id) : null; + const position = player.position; + const isG = isGoalie(position); + + // Use detailed stats if available, otherwise basic stats + const stats = statsRow + ? transformNLLPlayerStats(statsRow) + : transformNLLBasicStats(player); + + // Cast stats to Record type expected by schema + // Safe because we control the stats structure + const statsRecord = isG + ? null + : (stats as unknown as Record); + const goalieRecord = isG + ? (stats as unknown as Record) + : null; + + return { + playerId, + seasonId: ctx.seasonId, + teamId: teamId ?? null, + jerseyNumber: player.jerseyNumber + ? Number.parseInt(player.jerseyNumber, 10) + : null, + position, + isCaptain: false, + stats: statsRecord, + postSeasonStats: null, + goalieStats: goalieRecord, + postSeasonGoalieStats: null, + gamesPlayed: stats.gamesPlayed ?? 0, + }; +}; + +// ============================================================================= +// GAME TRANSFORMS +// ============================================================================= + +/** + * Map NLL status to our game status + */ +const mapNLLGameStatus = ( + status: string | null, +): "scheduled" | "in_progress" | "final" | "postponed" | "cancelled" => { + if (!status) return "scheduled"; + const s = status.toLowerCase(); + if (s.includes("final") || s.includes("complete")) return "final"; + if (s.includes("live") || s.includes("progress")) return "in_progress"; + if (s.includes("postpone")) return "postponed"; + if (s.includes("cancel")) return "cancelled"; + return "scheduled"; +}; + +/** + * Parse NLL game date string to Date + */ +const parseNLLGameDate = (dateStr: string | null): Date => { + if (!dateStr) return new Date(); + const date = new Date(dateStr); + return Number.isNaN(date.getTime()) ? new Date() : date; +}; + +/** + * Transform NLLMatch to UpsertGameInput + */ +export const transformNLLGame = ( + match: NLLMatch, + ctx: TransformContext, +): UpsertGameInput | null => { + const homeTeamId = ctx.teamIdMap.get(match.squads.home.id); + const awayTeamId = ctx.teamIdMap.get(match.squads.away.id); + + if (!homeTeamId || !awayTeamId) return null; + + return { + seasonId: ctx.seasonId, + externalId: match.id, + homeTeamId, + awayTeamId, + gameDate: parseNLLGameDate(match.date), + week: null, + gameNumber: null, + venue: match.venue.name, + venueCity: match.venue.city, + status: mapNLLGameStatus(match.status), + homeScore: match.squads.home.score, + awayScore: match.squads.away.score, + isOvertime: false, + overtimePeriods: 0, + playByPlayUrl: null, + homeTeamStats: null, + awayTeamStats: null, + broadcaster: null, + streamUrl: null, + }; +}; + +/** + * Transform array of NLLMatches + */ +export const transformNLLGames = ( + matches: NLLMatch[], + ctx: TransformContext, +): UpsertGameInput[] => + matches + .map((match) => transformNLLGame(match, ctx)) + .filter((g): g is UpsertGameInput => g !== null); + +// ============================================================================= +// STANDINGS TRANSFORMS +// ============================================================================= + +/** + * Transform NLLStanding to UpsertStandingsInput + */ +export const transformNLLStanding = ( + standing: NLLStanding, + ctx: TransformContext, + snapshotDate: Date, +): UpsertStandingsInput | null => { + const teamId = ctx.teamIdMap.get(standing.team_id); + if (!teamId) return null; + + return { + seasonId: ctx.seasonId, + teamId, + snapshotDate, + position: standing.position, + wins: standing.wins, + losses: standing.losses, + ties: 0, + overtimeLosses: 0, + points: null, // NLL doesn't use points system + winPercentage: Math.round(standing.win_percentage * 1000), // Store as integer (750 = .750) + goalsFor: standing.goals_for, + goalsAgainst: standing.goals_against, + goalDifferential: standing.goal_diff, + conference: null, + division: null, + clinchStatus: null, + seed: null, + }; +}; + +/** + * Transform array of NLLStandings + */ +export const transformNLLStandings = ( + standings: NLLStanding[], + ctx: TransformContext, + snapshotDate: Date, +): UpsertStandingsInput[] => + standings + .map((s) => transformNLLStanding(s, ctx, snapshotDate)) + .filter((s): s is UpsertStandingsInput => s !== null); diff --git a/packages/pipeline/src/load/pll.transform.ts b/packages/pipeline/src/load/pll.transform.ts new file mode 100644 index 00000000..09f6f5da --- /dev/null +++ b/packages/pipeline/src/load/pll.transform.ts @@ -0,0 +1,486 @@ +/** + * PLL Transform Functions + * + * Transform PLL extracted data into pro_* schema format. + */ + +import type { + PLLTeam, + PLLPlayer, + PLLTeamStanding, + PLLEvent, + PLLPlayerStats, + PLLTeamStats, +} from "../pll/pll.schema"; + +// Note: Stats types are from @laxdb/core/pro-league but we return Record +// for JSONB compatibility with the Effect Schema +import type { + TransformContext, + UpsertTeamInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertGameInput, + UpsertStandingsInput, + UpsertSeasonInput, +} from "./transform.types"; + +// ============================================================================= +// SEASON TRANSFORM +// ============================================================================= + +/** + * Create season input from PLL season info + */ +export const transformPLLSeason = ( + leagueId: number, + year: number, +): UpsertSeasonInput => ({ + leagueId, + externalId: String(year), + year, + displayName: String(year), // PLL uses single year (e.g., "2024") + startDate: null, + endDate: null, + isCurrent: false, +}); + +// ============================================================================= +// TEAM TRANSFORMS +// ============================================================================= + +/** + * Transform PLLTeam to UpsertTeamInput + */ +export const transformPLLTeam = ( + team: PLLTeam, + leagueId: number, +): UpsertTeamInput => ({ + leagueId, + externalId: team.officialId, + code: team.locationCode, + name: team.fullName, + shortName: team.location, + city: team.location, + logoUrl: team.urlLogo, + primaryColor: null, + secondaryColor: null, + websiteUrl: null, + isActive: true, + firstSeasonYear: null, + lastSeasonYear: null, +}); + +/** + * Transform array of PLLTeams + */ +export const transformPLLTeams = ( + teams: PLLTeam[], + leagueId: number, +): UpsertTeamInput[] => teams.map((team) => transformPLLTeam(team, leagueId)); + +// ============================================================================= +// PLAYER TRANSFORMS +// ============================================================================= + +/** + * Transform PLLPlayer to UpsertPlayerInput + */ +export const transformPLLPlayer = ( + player: PLLPlayer, + leagueId: number, +): UpsertPlayerInput => { + // Get current team info for position + const currentTeam = player.allTeams.find( + (t) => t.year === Math.max(...player.allTeams.map((x) => x.year)), + ); + + return { + leagueId, + externalId: player.officialId, + firstName: player.firstName, + lastName: player.lastNameSuffix + ? `${player.lastName} ${player.lastNameSuffix}` + : player.lastName, + fullName: `${player.firstName} ${player.lastName}${player.lastNameSuffix ? ` ${player.lastNameSuffix}` : ""}`, + position: currentTeam?.position ?? null, + dateOfBirth: null, // Not available in PLL API + birthplace: null, + country: player.countryCode, + height: null, + weight: null, + handedness: player.handedness, + college: null, // Would need player detail call + highSchool: null, + profileUrl: player.profileUrl, + photoUrl: null, + }; +}; + +/** + * Transform array of PLLPlayers + */ +export const transformPLLPlayers = ( + players: PLLPlayer[], + leagueId: number, +): UpsertPlayerInput[] => + players.map((player) => transformPLLPlayer(player, leagueId)); + +// ============================================================================= +// STATS TRANSFORMS +// ============================================================================= + +/** + * Check if player is a goalie based on position + */ +const isGoalie = (position: string | null): boolean => + position?.toUpperCase() === "G"; + +/** + * Transform PLLPlayerStats to PlayerStats JSONB + * Returns a plain object that can be serialized as JSONB + */ +export const transformPLLPlayerStats = ( + stats: PLLPlayerStats, +): Record => { + const result: Record = { + gamesPlayed: stats.gamesPlayed, + goals: stats.goals, + twoPointGoals: stats.twoPointGoals, + assists: stats.assists, + points: stats.points, + scoringPoints: stats.scoringPoints, + shots: stats.shots, + shotPct: stats.shotPct, + shotsOnGoal: stats.shotsOnGoal, + shotsOnGoalPct: stats.shotsOnGoalPct, + twoPointShots: stats.twoPointShots, + twoPointShotPct: stats.twoPointShotPct, + groundBalls: stats.groundBalls, + turnovers: stats.turnovers, + causedTurnovers: stats.causedTurnovers, + faceoffsWon: stats.faceoffsWon, + faceoffsLost: stats.faceoffsLost, + faceoffs: stats.faceoffs, + faceoffPct: stats.faceoffPct, + plusMinus: stats.plusMinus, + }; + + // Add optional fields only if defined + if (stats.onePointGoals != null) result.onePointGoals = stats.onePointGoals; + if (stats.twoPointShotsOnGoal != null) + result.twoPointShotsOnGoal = stats.twoPointShotsOnGoal; + if (stats.twoPointShotsOnGoalPct != null) + result.twoPointShotsOnGoalPct = stats.twoPointShotsOnGoalPct; + if (stats.foRecord != null) result.foRecord = stats.foRecord; + if (stats.numPenalties != null) result.numPenalties = stats.numPenalties; + if (stats.pim != null) result.pim = stats.pim; + if (stats.pimValue != null) result.pimValue = stats.pimValue; + if (stats.powerPlayGoals != null) + result.powerPlayGoals = stats.powerPlayGoals; + if (stats.powerPlayShots != null) + result.powerPlayShots = stats.powerPlayShots; + if (stats.shortHandedGoals != null) + result.shortHandedGoals = stats.shortHandedGoals; + if (stats.shortHandedShots != null) + result.shortHandedShots = stats.shortHandedShots; + if (stats.tof != null) result.tof = stats.tof; + if (stats.touches != null) result.touches = stats.touches; + if (stats.totalPasses != null) result.totalPasses = stats.totalPasses; + if (stats.unassistedGoals != null) + result.unassistedGoals = stats.unassistedGoals; + if (stats.assistedGoals != null) result.assistedGoals = stats.assistedGoals; + if (stats.passRate != null) result.passRate = stats.passRate; + if (stats.shotRate != null) result.shotRate = stats.shotRate; + if (stats.goalRate != null) result.goalRate = stats.goalRate; + if (stats.assistRate != null) result.assistRate = stats.assistRate; + if (stats.turnoverRate != null) result.turnoverRate = stats.turnoverRate; + + return result; +}; + +/** + * Transform PLLPlayerStats to GoalieStats JSONB + */ +export const transformPLLGoalieStats = ( + stats: PLLPlayerStats, +): Record => { + const result: Record = { + gamesPlayed: stats.gamesPlayed, + saves: stats.saves, + savePct: stats.savePct, + goalsAgainst: stats.goalsAgainst, + goalsAgainstAvg: stats.GAA, + }; + + // Add optional fields only if defined + if (stats.goalieWins != null) result.wins = stats.goalieWins; + if (stats.goalieLosses != null) result.losses = stats.goalieLosses; + if (stats.goalieTies != null) result.ties = stats.goalieTies; + if (stats.twoPointGoalsAgainst != null) + result.twoPointGoalsAgainst = stats.twoPointGoalsAgainst; + if (stats.twoPtGaa != null) result.twoPtGaa = stats.twoPtGaa; + if (stats.scoresAgainst != null) result.scoresAgainst = stats.scoresAgainst; + if (stats.saa != null) result.saa = stats.saa; + if (stats.powerPlayGoalsAgainst != null) + result.powerPlayGoalsAgainst = stats.powerPlayGoalsAgainst; + if (stats.shortHandedGoalsAgainst != null) + result.shortHandedGoalsAgainst = stats.shortHandedGoalsAgainst; + + return result; +}; + +/** + * Transform PLLTeamStats to TeamStats JSONB + */ +export const transformPLLTeamStats = ( + stats: PLLTeamStats, +): Record => { + const result: Record = { + gamesPlayed: stats.gamesPlayed, + scores: stats.scores, + scoresAgainst: stats.scoresAgainst, + goals: stats.goals, + goalsAgainst: stats.goalsAgainst, + twoPointGoals: stats.twoPointGoals, + twoPointGoalsAgainst: stats.twoPointGoalsAgainst, + assists: stats.assists, + shots: stats.shots, + shotsOnGoal: stats.shotsOnGoal, + shotPct: stats.shotPct, + shotsOnGoalPct: stats.shotsOnGoalPct, + twoPointShots: stats.twoPointShots, + twoPointShotPct: stats.twoPointShotPct, + twoPointShotsOnGoal: stats.twoPointShotsOnGoal, + twoPointShotsOnGoalPct: stats.twoPointShotsOnGoalPct, + groundBalls: stats.groundBalls, + turnovers: stats.turnovers, + causedTurnovers: stats.causedTurnovers, + faceoffsWon: stats.faceoffsWon, + faceoffsLost: stats.faceoffsLost, + faceoffs: stats.faceoffs, + faceoffPct: stats.faceoffPct, + saves: stats.saves, + savePct: stats.savePct, + numPenalties: stats.numPenalties, + pim: stats.pim, + offsides: stats.offsides, + shotClockExpirations: stats.shotClockExpirations, + powerPlayGoals: stats.powerPlayGoals, + powerPlayShots: stats.powerPlayShots, + powerPlayPct: stats.powerPlayPct, + powerPlayGoalsAgainst: stats.powerPlayGoalsAgainst, + powerPlayShotsAgainst: stats.powerPlayShotsAgainst, + powerPlayGoalsAgainstPct: stats.powerPlayGoalsAgainstPct, + shortHandedGoals: stats.shortHandedGoals, + shortHandedShots: stats.shortHandedShots, + shortHandedPct: stats.shortHandedPct, + shortHandedShotsAgainst: stats.shortHandedShotsAgainst, + shortHandedGoalsAgainst: stats.shortHandedGoalsAgainst, + shortHandedGoalsAgainstPct: stats.shortHandedGoalsAgainstPct, + manDownPct: stats.manDownPct, + timesManUp: stats.timesManUp, + timesShortHanded: stats.timesShortHanded, + clears: stats.clears, + clearAttempts: stats.clearAttempts, + clearPct: stats.clearPct, + rides: stats.rides, + rideAttempts: stats.rideAttempts, + ridesPct: stats.ridesPct, + }; + + // Add optional fields only if defined + if (stats.onePointGoals != null) result.onePointGoals = stats.onePointGoals; + if (stats.saa != null) result.saa = stats.saa; + if (stats.scoresPG != null) result.scoresPG = stats.scoresPG; + if (stats.shotsPG != null) result.shotsPG = stats.shotsPG; + if (stats.touches != null) result.touches = stats.touches; + if (stats.totalPasses != null) result.totalPasses = stats.totalPasses; + + return result; +}; + +// ============================================================================= +// PLAYER SEASON TRANSFORMS +// ============================================================================= + +/** + * Transform PLLPlayer to UpsertPlayerSeasonInput + */ +export const transformPLLPlayerSeason = ( + player: PLLPlayer, + ctx: TransformContext, +): UpsertPlayerSeasonInput | null => { + const playerId = ctx.playerIdMap.get(player.officialId); + if (!playerId) return null; + + // Find team for this season + const seasonTeam = player.allTeams.find((t) => t.year === ctx.seasonYear); + const teamId = seasonTeam + ? ctx.teamIdMap.get(seasonTeam.officialId) + : undefined; + const position = seasonTeam?.position ?? null; + const isG = isGoalie(position); + + // Transform stats - returns Record for JSONB + const stats = player.stats ? transformPLLPlayerStats(player.stats) : null; + const postStats = player.postStats + ? transformPLLPlayerStats(player.postStats) + : null; + const goalieStats = + isG && player.stats ? transformPLLGoalieStats(player.stats) : null; + const postGoalieStats = + isG && player.postStats ? transformPLLGoalieStats(player.postStats) : null; + + return { + playerId, + seasonId: ctx.seasonId, + teamId: teamId ?? null, + jerseyNumber: seasonTeam?.jerseyNum ?? player.jerseyNum ?? null, + position, + isCaptain: player.isCaptain ?? false, + stats: isG ? null : stats, + postSeasonStats: isG ? null : postStats, + goalieStats: goalieStats, + postSeasonGoalieStats: postGoalieStats, + gamesPlayed: player.stats?.gamesPlayed ?? 0, + }; +}; + +// ============================================================================= +// GAME TRANSFORMS +// ============================================================================= + +/** + * Map PLL eventStatus to game status + * PLL: 0 = scheduled, 1 = in progress, 2 = final + */ +const mapPLLGameStatus = ( + eventStatus: number | null, + gameStatus: string | null, +): "scheduled" | "in_progress" | "final" | "postponed" | "cancelled" => { + if (eventStatus === 2) return "final"; + if (eventStatus === 1) return "in_progress"; + if (gameStatus?.toLowerCase().includes("postpone")) return "postponed"; + if (gameStatus?.toLowerCase().includes("cancel")) return "cancelled"; + return "scheduled"; +}; + +/** + * Parse PLL event startTime to Date + * Format: ISO timestamp string + */ +const parsePLLEventDate = (startTime: string | null): Date => { + if (!startTime) return new Date(); + const date = new Date(startTime); + return Number.isNaN(date.getTime()) ? new Date() : date; +}; + +/** + * Transform PLLEvent to UpsertGameInput + */ +export const transformPLLGame = ( + event: PLLEvent, + ctx: TransformContext, +): UpsertGameInput | null => { + if (!event.homeTeam || !event.awayTeam) return null; + + const homeTeamId = ctx.teamIdMap.get(event.homeTeam.officialId); + const awayTeamId = ctx.teamIdMap.get(event.awayTeam.officialId); + + if (!homeTeamId || !awayTeamId) return null; + + return { + seasonId: ctx.seasonId, + externalId: event.externalId ?? event.slugname ?? String(event.id), + homeTeamId, + awayTeamId, + gameDate: parsePLLEventDate(event.startTime), + week: event.week, + gameNumber: event.gameNumber, + venue: event.venue, + venueCity: event.venueLocation ?? event.location, + status: mapPLLGameStatus(event.eventStatus, event.gameStatus), + homeScore: event.homeScore, + awayScore: event.visitorScore, + isOvertime: false, // Not directly available in event list + overtimePeriods: 0, + playByPlayUrl: null, // Will be set during play-by-play upload + homeTeamStats: null, // Would need event detail + awayTeamStats: null, + broadcaster: + event.broadcaster && event.broadcaster.length > 0 + ? event.broadcaster.join(", ") + : null, + streamUrl: event.urlStreaming, + }; +}; + +/** + * Transform array of PLLEvents + */ +export const transformPLLGames = ( + events: PLLEvent[], + ctx: TransformContext, +): UpsertGameInput[] => + events + .map((event) => transformPLLGame(event, ctx)) + .filter((g): g is UpsertGameInput => g !== null); + +// ============================================================================= +// STANDINGS TRANSFORMS +// ============================================================================= + +/** + * Transform PLLTeamStanding to UpsertStandingsInput + */ +export const transformPLLStanding = ( + standing: PLLTeamStanding, + ctx: TransformContext, + snapshotDate: Date, +): UpsertStandingsInput | null => { + const teamId = ctx.teamIdMap.get(standing.teamId); + if (!teamId) return null; + + // PLL doesn't have explicit position in standings, derive from seed or wins + const position = standing.seed ?? 0; + + return { + seasonId: ctx.seasonId, + teamId, + snapshotDate, + position, + wins: standing.wins, + losses: standing.losses, + ties: standing.ties, + overtimeLosses: 0, // PLL doesn't track OT losses + points: null, // PLL doesn't use points system + winPercentage: + standing.wins + standing.losses > 0 + ? Math.round( + (standing.wins / + (standing.wins + standing.losses + standing.ties)) * + 1000, + ) + : 0, + goalsFor: standing.scores, + goalsAgainst: standing.scoresAgainst, + goalDifferential: standing.scoreDiff, + conference: standing.conference, + division: null, + clinchStatus: null, + seed: standing.seed, + }; +}; + +/** + * Transform array of PLLStandings + */ +export const transformPLLStandings = ( + standings: PLLTeamStanding[], + ctx: TransformContext, + snapshotDate: Date, +): UpsertStandingsInput[] => + standings + .map((s) => transformPLLStanding(s, ctx, snapshotDate)) + .filter((s): s is UpsertStandingsInput => s !== null); diff --git a/packages/pipeline/src/load/transform.types.ts b/packages/pipeline/src/load/transform.types.ts new file mode 100644 index 00000000..73545d46 --- /dev/null +++ b/packages/pipeline/src/load/transform.types.ts @@ -0,0 +1,85 @@ +/** + * Transform Layer Types + * + * Common types and utilities for transforming extracted data + * into the pro_* schema format. + */ + +import type { + UpsertTeamInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertGameInput, + UpsertStandingsInput, + UpsertSeasonInput, + LeagueCode, +} from "@laxdb/core/pro-league"; + +// ============================================================================= +// TRANSFORM CONTEXT +// ============================================================================= + +/** + * Context required for transforms - provides league and season info + * that needs to be resolved before transforming entities + */ +export interface TransformContext { + leagueId: number; + leagueCode: LeagueCode; + seasonId: number; + seasonYear: number; + seasonExternalId: string; + /** Map of team external ID -> internal team ID (resolved during team upsert) */ + teamIdMap: Map; + /** Map of player external ID -> internal player ID (resolved during player upsert) */ + playerIdMap: Map; +} + +/** + * Create empty context for a league/season + */ +export const createTransformContext = ( + leagueId: number, + leagueCode: LeagueCode, + seasonId: number, + seasonYear: number, + seasonExternalId: string, +): TransformContext => ({ + leagueId, + leagueCode, + seasonId, + seasonYear, + seasonExternalId, + teamIdMap: new Map(), + playerIdMap: new Map(), +}); + +// ============================================================================= +// TRANSFORM RESULTS +// ============================================================================= + +/** + * Result of transforming a full season's data + */ +export interface TransformResult { + season: UpsertSeasonInput; + teams: UpsertTeamInput[]; + players: UpsertPlayerInput[]; + playerSeasons: UpsertPlayerSeasonInput[]; + games: UpsertGameInput[]; + standings: UpsertStandingsInput[]; +} + +// ============================================================================= +// TYPE EXPORTS (re-export for convenience) +// ============================================================================= + +export type { + UpsertTeamInput, + UpsertPlayerInput, + UpsertPlayerSeasonInput, + UpsertGameInput, + UpsertStandingsInput, + UpsertSeasonInput, + LeagueCode, +};