Fixes #262 * Replace isDraftable boolean with draftOn/draftOff date fields on sport seasons Admins can now schedule when a sport season becomes available for drafting by setting explicit open and close dates instead of a manual toggle. https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc * Address code review feedback on draft window implementation - sports-data-sync: use unambiguous past placeholder dates for synced seasons, with a comment explaining the intent - sports-season model: remove redundant top-level lte/gte imports (callback form already supplies them) - _journal.json: add missing trailing newline - sports-season test: mock ~/database/schema instead of stubbing all drizzle builder functions, matching the established pattern in the codebase - admin list: compute today once in component body instead of per-row https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc * Fix: restore lte/gte imports removed in review cleanup The relational query where-callback in this version of Drizzle does not expose lte/gte as helper args, so they must be imported from drizzle-orm. Removing them broke CI TypeScript. https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc --------- Co-authored-by: Claude <noreply@anthropic.com>
210 lines
5.2 KiB
TypeScript
210 lines
5.2 KiB
TypeScript
import { eq, and, sql } from "drizzle-orm";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
|
|
export type Season = typeof schema.seasons.$inferSelect;
|
|
export type NewSeason = Omit<typeof schema.seasons.$inferInsert, 'inviteCode'> & {
|
|
inviteCode?: string;
|
|
};
|
|
export type SeasonStatus = "pre_draft" | "draft" | "active" | "completed";
|
|
|
|
export interface SeasonWithSportsSeasons extends Season {
|
|
seasonSports: Array<{
|
|
id: string;
|
|
sportsSeason: {
|
|
id: string;
|
|
name: string;
|
|
year: number;
|
|
status: string;
|
|
scoringType: string;
|
|
scoringPattern: string | null;
|
|
draftOn: string; // 'YYYY-MM-DD'
|
|
draftOff: string; // 'YYYY-MM-DD'
|
|
sport: {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
slug: string;
|
|
};
|
|
};
|
|
}>;
|
|
}
|
|
|
|
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(seasonData)
|
|
.returning();
|
|
return season;
|
|
}
|
|
|
|
export async function findSeasonById(id: string): Promise<Season | undefined> {
|
|
const db = database();
|
|
return await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, id),
|
|
});
|
|
}
|
|
|
|
export async function findSeasonsByLeagueId(leagueId: string): Promise<Season[]> {
|
|
const db = database();
|
|
return await db.query.seasons.findMany({
|
|
where: eq(schema.seasons.leagueId, leagueId),
|
|
orderBy: (seasons, { desc }) => [desc(seasons.year)],
|
|
});
|
|
}
|
|
|
|
export async function findCurrentSeason(
|
|
leagueId: string
|
|
): Promise<Season | undefined> {
|
|
const db = database();
|
|
return await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.leagueId, leagueId),
|
|
orderBy: (seasons, { desc }) => [desc(seasons.year)],
|
|
});
|
|
}
|
|
|
|
export async function findCurrentSeasonWithSports(
|
|
leagueId: string
|
|
): Promise<SeasonWithSportsSeasons | undefined> {
|
|
const db = database();
|
|
return await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.leagueId, leagueId),
|
|
orderBy: (seasons, { desc }) => [desc(seasons.year)],
|
|
with: {
|
|
seasonSports: {
|
|
with: {
|
|
sportsSeason: {
|
|
with: {
|
|
sport: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}) as SeasonWithSportsSeasons | undefined;
|
|
}
|
|
|
|
export async function findSeasonByLeagueAndYear(
|
|
leagueId: string,
|
|
year: number
|
|
): Promise<Season | undefined> {
|
|
const db = database();
|
|
return await db.query.seasons.findFirst({
|
|
where: and(
|
|
eq(schema.seasons.leagueId, leagueId),
|
|
eq(schema.seasons.year, year)
|
|
),
|
|
});
|
|
}
|
|
|
|
export async function updateSeason(
|
|
id: string,
|
|
data: Partial<NewSeason>
|
|
): Promise<Season> {
|
|
const db = database();
|
|
const [season] = await db
|
|
.update(schema.seasons)
|
|
.set({ ...data, updatedAt: new Date() })
|
|
.where(eq(schema.seasons.id, id))
|
|
.returning();
|
|
return season;
|
|
}
|
|
|
|
export async function updateSeasonStatus(
|
|
id: string,
|
|
status: SeasonStatus
|
|
): Promise<Season> {
|
|
return await updateSeason(id, { status });
|
|
}
|
|
|
|
export async function deleteSeason(id: string): Promise<void> {
|
|
const db = database();
|
|
await db.delete(schema.seasons).where(eq(schema.seasons.id, id));
|
|
}
|
|
|
|
export async function findSeasonWithSportsSeasons(
|
|
id: string
|
|
): Promise<SeasonWithSportsSeasons | undefined> {
|
|
const db = database();
|
|
return await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, id),
|
|
with: {
|
|
seasonSports: {
|
|
with: {
|
|
sportsSeason: {
|
|
with: {
|
|
sport: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}) 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 });
|
|
}
|
|
|
|
export async function countSeasonsByStatus(): Promise<Record<SeasonStatus, number>> {
|
|
const db = database();
|
|
const rows = await db
|
|
.select({ status: schema.seasons.status, count: sql<number>`count(*)::int` })
|
|
.from(schema.seasons)
|
|
.groupBy(schema.seasons.status);
|
|
|
|
const result: Record<SeasonStatus, number> = {
|
|
pre_draft: 0,
|
|
draft: 0,
|
|
active: 0,
|
|
completed: 0,
|
|
};
|
|
for (const row of rows) {
|
|
if (row.status in result) {
|
|
result[row.status as SeasonStatus] = row.count;
|
|
}
|
|
}
|
|
return result;
|
|
}
|