feat: add invite code system for league seasons with member access control
This commit is contained in:
parent
0999e1c980
commit
8c65ac9590
9 changed files with 1271 additions and 8 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { eq, desc } from "drizzle-orm";
|
||||
import { eq, desc, and } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
|
|
@ -88,3 +88,30 @@ export async function findLeaguesWithActiveSeasonsByUserId(
|
|||
|
||||
return leagues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is a member of a league (has a team in current season)
|
||||
*/
|
||||
export async function isUserLeagueMember(
|
||||
leagueId: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const db = database();
|
||||
|
||||
const league = await db.query.leagues.findFirst({
|
||||
where: eq(schema.leagues.id, leagueId),
|
||||
});
|
||||
|
||||
if (!league || !league.currentSeasonId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const team = await db.query.teams.findFirst({
|
||||
where: and(
|
||||
eq(schema.teams.seasonId, league.currentSeasonId),
|
||||
eq(schema.teams.ownerId, userId)
|
||||
),
|
||||
});
|
||||
|
||||
return !!team;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,16 @@ export interface SeasonWithSportsSeasons extends Season {
|
|||
|
||||
export async function createSeason(data: NewSeason): Promise<Season> {
|
||||
const db = database();
|
||||
|
||||
// Generate invite code if not provided
|
||||
const seasonData = {
|
||||
...data,
|
||||
inviteCode: data.inviteCode || generateInviteCode(),
|
||||
};
|
||||
|
||||
const [season] = await db
|
||||
.insert(schema.seasons)
|
||||
.values(data)
|
||||
.values(seasonData)
|
||||
.returning();
|
||||
return season;
|
||||
}
|
||||
|
|
@ -137,3 +144,41 @@ export async function findSeasonWithSportsSeasons(
|
|||
},
|
||||
}) as SeasonWithSportsSeasons | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a short, URL-safe invite code
|
||||
* Uses base62 encoding (alphanumeric) for readability
|
||||
*/
|
||||
export function generateInviteCode(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const length = 10;
|
||||
let result = '';
|
||||
const randomValues = new Uint8Array(length);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[randomValues[i] % chars.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a season by its invite code
|
||||
*/
|
||||
export async function findSeasonByInviteCode(
|
||||
inviteCode: string
|
||||
): Promise<Season | undefined> {
|
||||
const db = database();
|
||||
return await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.inviteCode, inviteCode),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate invite code for a season
|
||||
*/
|
||||
export async function regenerateInviteCode(seasonId: string): Promise<Season> {
|
||||
const newInviteCode = generateInviteCode();
|
||||
return await updateSeason(seasonId, { inviteCode: newInviteCode });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
|||
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
route("i/:inviteCode", "routes/i.$inviteCode.tsx"),
|
||||
route("leagues/new", "routes/leagues/new.tsx"),
|
||||
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
||||
route("leagues/:leagueId/settings", "routes/leagues/$leagueId.settings.tsx"),
|
||||
|
|
|
|||
160
app/routes/i.$inviteCode.tsx
Normal file
160
app/routes/i.$inviteCode.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { Form, redirect } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { SignInButton } from "@clerk/react-router";
|
||||
import type { Route } from "./+types/i.$inviteCode";
|
||||
import {
|
||||
findSeasonByInviteCode,
|
||||
findLeagueById,
|
||||
findCommissionersByLeagueId,
|
||||
findAvailableTeams,
|
||||
assignTeamOwner,
|
||||
isUserLeagueMember,
|
||||
} from "~/models";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const { params } = args;
|
||||
const { inviteCode } = params;
|
||||
const { userId } = await getAuth(args);
|
||||
|
||||
// Find season by invite code
|
||||
const season = await findSeasonByInviteCode(inviteCode);
|
||||
|
||||
if (!season) {
|
||||
throw new Response("Invalid invite code", { status: 404 });
|
||||
}
|
||||
|
||||
// Find the league
|
||||
const league = await findLeagueById(season.leagueId);
|
||||
|
||||
if (!league) {
|
||||
throw new Response("League not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Find commissioners to get the commissioner's name
|
||||
const commissioners = await findCommissionersByLeagueId(league.id);
|
||||
const firstCommissioner = commissioners[0];
|
||||
|
||||
// Check if user is already a member
|
||||
let isAlreadyMember = false;
|
||||
if (userId) {
|
||||
isAlreadyMember = await isUserLeagueMember(league.id, userId);
|
||||
}
|
||||
|
||||
return {
|
||||
league,
|
||||
season,
|
||||
inviteCode,
|
||||
firstCommissioner,
|
||||
isAlreadyMember,
|
||||
isLoggedIn: !!userId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function action(args: Route.ActionArgs) {
|
||||
const { params } = args;
|
||||
const { inviteCode } = params;
|
||||
const { userId } = await getAuth(args);
|
||||
|
||||
if (!userId) {
|
||||
throw new Response("You must be logged in to join a league", { status: 401 });
|
||||
}
|
||||
|
||||
// Find season by invite code
|
||||
const season = await findSeasonByInviteCode(inviteCode);
|
||||
|
||||
if (!season) {
|
||||
throw new Response("Invalid invite code", { status: 404 });
|
||||
}
|
||||
|
||||
// Check if user is already a member
|
||||
const isAlreadyMember = await isUserLeagueMember(season.leagueId, userId);
|
||||
|
||||
if (isAlreadyMember) {
|
||||
// Already a member, redirect to league page
|
||||
return redirect(`/leagues/${season.leagueId}`);
|
||||
}
|
||||
|
||||
// Find an available team
|
||||
const availableTeams = await findAvailableTeams(season.id);
|
||||
|
||||
if (availableTeams.length === 0) {
|
||||
throw new Response("No available teams in this league", { status: 400 });
|
||||
}
|
||||
|
||||
// Assign the first available team to the user
|
||||
await assignTeamOwner(availableTeams[0].id, userId);
|
||||
|
||||
// Redirect to league page
|
||||
return redirect(`/leagues/${season.leagueId}?joined=true`);
|
||||
}
|
||||
|
||||
export default function InvitePage({ loaderData }: Route.ComponentProps) {
|
||||
const { league, firstCommissioner, isAlreadyMember, isLoggedIn } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-16 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">You're Invited!</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{firstCommissioner ? "A commissioner" : "Someone"} has invited you to join{" "}
|
||||
<span className="font-semibold">{league.name}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">League Details</h3>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<span className="font-medium text-foreground">League:</span> {league.name}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium text-foreground">Created:</span>{" "}
|
||||
{new Date(league.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAlreadyMember ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You're already a member of this league!
|
||||
</p>
|
||||
<Button asChild className="w-full">
|
||||
<a href={`/leagues/${league.id}`}>Go to League</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : isLoggedIn ? (
|
||||
<Form method="post" className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click the button below to join this league and claim your team.
|
||||
</p>
|
||||
<Button type="submit" className="w-full">
|
||||
Join League
|
||||
</Button>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You'll need to sign in or create an account to join this league.
|
||||
</p>
|
||||
<SignInButton mode="modal" forceRedirectUrl={`/i/${loaderData.inviteCode}`}>
|
||||
<Button className="w-full">Sign In to Join</Button>
|
||||
</SignInButton>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
findCurrentSeason,
|
||||
findTeamsBySeasonId,
|
||||
findCommissionersByLeagueId,
|
||||
isUserLeagueMember,
|
||||
isCommissioner,
|
||||
} from "~/models";
|
||||
import type { Route } from "./+types/$leagueId";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
|
@ -33,14 +35,27 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
// Fetch current season
|
||||
const season = await findCurrentSeason(leagueId);
|
||||
|
||||
// Fetch teams for current season
|
||||
const teams = season ? await findTeamsBySeasonId(season.id) : [];
|
||||
|
||||
// Fetch commissioners
|
||||
const commissioners = await findCommissionersByLeagueId(leagueId);
|
||||
|
||||
// Check if current user is a commissioner
|
||||
const isUserCommissioner = commissioners.some(c => c.userId === userId);
|
||||
const isUserCommissioner = userId ? await isCommissioner(leagueId, userId) : false;
|
||||
|
||||
// Check if user is a member (has a team in current season)
|
||||
const isUserMember = userId ? await isUserLeagueMember(leagueId, userId) : false;
|
||||
|
||||
// Check access: user must be a commissioner, a member, or an admin
|
||||
// If not logged in or not authorized, throw 403
|
||||
if (!userId) {
|
||||
throw new Response("You must be logged in to view this league", { status: 401 });
|
||||
}
|
||||
|
||||
if (!isUserCommissioner && !isUserMember) {
|
||||
throw new Response("You do not have access to this league", { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch teams for current season
|
||||
const teams = season ? await findTeamsBySeasonId(season.id) : [];
|
||||
|
||||
return {
|
||||
league,
|
||||
|
|
@ -55,15 +70,33 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||
const { league, season, teams, commissioners, currentUserId, isUserCommissioner } = loaderData;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("updated") === "true") {
|
||||
toast.success("League updated successfully");
|
||||
// Remove the query parameter
|
||||
setSearchParams({});
|
||||
}
|
||||
if (searchParams.get("joined") === "true") {
|
||||
toast.success("Welcome to the league! You've been assigned a team.");
|
||||
setSearchParams({});
|
||||
}
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const handleCopyInviteLink = async () => {
|
||||
if (!season?.inviteCode) return;
|
||||
|
||||
const inviteUrl = `${window.location.origin}/i/${season.inviteCode}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteUrl);
|
||||
setCopied(true);
|
||||
toast.success("Invite link copied to clipboard!");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
toast.error("Failed to copy invite link");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
|
|
@ -154,6 +187,38 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{isUserCommissioner && season && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invite Link</CardTitle>
|
||||
<CardDescription>
|
||||
Share this link to invite people to join your league
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={`${typeof window !== "undefined" ? window.location.origin : ""}/i/${season.inviteCode}`}
|
||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCopyInviteLink}
|
||||
variant={copied ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Anyone with this link can join your league
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Commissioners</CardTitle>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export const seasons = pgTable("seasons", {
|
|||
templateId: uuid("template_id")
|
||||
.references(() => seasonTemplates.id, { onDelete: "set null" }),
|
||||
flexSpots: integer("flex_spots").notNull().default(0),
|
||||
inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
10
drizzle/0010_complex_the_stranger.sql
Normal file
10
drizzle/0010_complex_the_stranger.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Add invite_code column without NOT NULL constraint first
|
||||
ALTER TABLE "seasons" ADD COLUMN "invite_code" varchar(20);--> statement-breakpoint
|
||||
|
||||
-- Generate unique invite codes for existing seasons using UUID
|
||||
-- Takes first 10 characters of UUID without dashes for a short, unique code
|
||||
UPDATE "seasons" SET "invite_code" = substring(replace(gen_random_uuid()::text, '-', ''), 1, 10) WHERE "invite_code" IS NULL;--> statement-breakpoint
|
||||
|
||||
-- Now make it NOT NULL and add unique constraint
|
||||
ALTER TABLE "seasons" ALTER COLUMN "invite_code" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "seasons" ADD CONSTRAINT "seasons_invite_code_unique" UNIQUE("invite_code");
|
||||
947
drizzle/meta/0010_snapshot.json
Normal file
947
drizzle/meta/0010_snapshot.json
Normal file
|
|
@ -0,0 +1,947 @@
|
|||
{
|
||||
"id": "682234f1-8997-43d8-a434-46aead4ef32f",
|
||||
"prevId": "d7b8574f-85c3-45ec-99cb-a49e2bec7e18",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.commissioners": {
|
||||
"name": "commissioners",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"league_id": {
|
||||
"name": "league_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"commissioners_league_id_leagues_id_fk": {
|
||||
"name": "commissioners_league_id_leagues_id_fk",
|
||||
"tableFrom": "commissioners",
|
||||
"tableTo": "leagues",
|
||||
"columnsFrom": [
|
||||
"league_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.leagues": {
|
||||
"name": "leagues",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"current_season_id": {
|
||||
"name": "current_season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.participant_results": {
|
||||
"name": "participant_results",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"participant_id": {
|
||||
"name": "participant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sports_season_id": {
|
||||
"name": "sports_season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"final_position": {
|
||||
"name": "final_position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"points_awarded": {
|
||||
"name": "points_awarded",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"qualifying_points": {
|
||||
"name": "qualifying_points",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"participant_results_participant_id_participants_id_fk": {
|
||||
"name": "participant_results_participant_id_participants_id_fk",
|
||||
"tableFrom": "participant_results",
|
||||
"tableTo": "participants",
|
||||
"columnsFrom": [
|
||||
"participant_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"participant_results_sports_season_id_sports_seasons_id_fk": {
|
||||
"name": "participant_results_sports_season_id_sports_seasons_id_fk",
|
||||
"tableFrom": "participant_results",
|
||||
"tableTo": "sports_seasons",
|
||||
"columnsFrom": [
|
||||
"sports_season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.participants": {
|
||||
"name": "participants",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"sports_season_id": {
|
||||
"name": "sports_season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"short_name": {
|
||||
"name": "short_name",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"participants_sports_season_id_sports_seasons_id_fk": {
|
||||
"name": "participants_sports_season_id_sports_seasons_id_fk",
|
||||
"tableFrom": "participants",
|
||||
"tableTo": "sports_seasons",
|
||||
"columnsFrom": [
|
||||
"sports_season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.season_sports": {
|
||||
"name": "season_sports",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"season_id": {
|
||||
"name": "season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sports_season_id": {
|
||||
"name": "sports_season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"season_sports_season_id_seasons_id_fk": {
|
||||
"name": "season_sports_season_id_seasons_id_fk",
|
||||
"tableFrom": "season_sports",
|
||||
"tableTo": "seasons",
|
||||
"columnsFrom": [
|
||||
"season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"season_sports_sports_season_id_sports_seasons_id_fk": {
|
||||
"name": "season_sports_sports_season_id_sports_seasons_id_fk",
|
||||
"tableFrom": "season_sports",
|
||||
"tableTo": "sports_seasons",
|
||||
"columnsFrom": [
|
||||
"sports_season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.season_template_sports": {
|
||||
"name": "season_template_sports",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"template_id": {
|
||||
"name": "template_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"sports_season_id": {
|
||||
"name": "sports_season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"season_template_sports_template_id_season_templates_id_fk": {
|
||||
"name": "season_template_sports_template_id_season_templates_id_fk",
|
||||
"tableFrom": "season_template_sports",
|
||||
"tableTo": "season_templates",
|
||||
"columnsFrom": [
|
||||
"template_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"season_template_sports_sports_season_id_sports_seasons_id_fk": {
|
||||
"name": "season_template_sports_sports_season_id_sports_seasons_id_fk",
|
||||
"tableFrom": "season_template_sports",
|
||||
"tableTo": "sports_seasons",
|
||||
"columnsFrom": [
|
||||
"sports_season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.season_templates": {
|
||||
"name": "season_templates",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"year": {
|
||||
"name": "year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.seasons": {
|
||||
"name": "seasons",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"league_id": {
|
||||
"name": "league_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"year": {
|
||||
"name": "year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "season_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pre_draft'"
|
||||
},
|
||||
"template_id": {
|
||||
"name": "template_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"flex_spots": {
|
||||
"name": "flex_spots",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"invite_code": {
|
||||
"name": "invite_code",
|
||||
"type": "varchar(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"seasons_league_id_leagues_id_fk": {
|
||||
"name": "seasons_league_id_leagues_id_fk",
|
||||
"tableFrom": "seasons",
|
||||
"tableTo": "leagues",
|
||||
"columnsFrom": [
|
||||
"league_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"seasons_template_id_season_templates_id_fk": {
|
||||
"name": "seasons_template_id_season_templates_id_fk",
|
||||
"tableFrom": "seasons",
|
||||
"tableTo": "season_templates",
|
||||
"columnsFrom": [
|
||||
"template_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"seasons_invite_code_unique": {
|
||||
"name": "seasons_invite_code_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"invite_code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sports": {
|
||||
"name": "sports",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "sport_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"icon_url": {
|
||||
"name": "icon_url",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sports_slug_unique": {
|
||||
"name": "sports_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sports_seasons": {
|
||||
"name": "sports_seasons",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"sport_id": {
|
||||
"name": "sport_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"year": {
|
||||
"name": "year",
|
||||
"type": "integer",
|
||||
"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
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "sports_season_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'upcoming'"
|
||||
},
|
||||
"scoring_type": {
|
||||
"name": "scoring_type",
|
||||
"type": "scoring_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sports_seasons_sport_id_sports_id_fk": {
|
||||
"name": "sports_seasons_sport_id_sports_id_fk",
|
||||
"tableFrom": "sports_seasons",
|
||||
"tableTo": "sports",
|
||||
"columnsFrom": [
|
||||
"sport_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"season_id": {
|
||||
"name": "season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"teams_season_id_seasons_id_fk": {
|
||||
"name": "teams_season_id_seasons_id_fk",
|
||||
"tableFrom": "teams",
|
||||
"tableTo": "seasons",
|
||||
"columnsFrom": [
|
||||
"season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"clerk_id": {
|
||||
"name": "clerk_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"first_name": {
|
||||
"name": "first_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_name": {
|
||||
"name": "last_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "varchar(512)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_admin": {
|
||||
"name": "is_admin",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_clerk_id_unique": {
|
||||
"name": "users_clerk_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"clerk_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.scoring_type": {
|
||||
"name": "scoring_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"playoffs",
|
||||
"regular_season",
|
||||
"majors"
|
||||
]
|
||||
},
|
||||
"public.season_status": {
|
||||
"name": "season_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"pre_draft",
|
||||
"draft",
|
||||
"active",
|
||||
"completed"
|
||||
]
|
||||
},
|
||||
"public.sport_type": {
|
||||
"name": "sport_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"team",
|
||||
"individual"
|
||||
]
|
||||
},
|
||||
"public.sports_season_status": {
|
||||
"name": "sports_season_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"upcoming",
|
||||
"active",
|
||||
"completed"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +71,13 @@
|
|||
"when": 1760392647508,
|
||||
"tag": "0009_fuzzy_korvac",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1760469105799,
|
||||
"tag": "0010_complex_the_stranger",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue