Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3da27af036
commit
686ccd7188
22 changed files with 147 additions and 89 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
|
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
|
||||||
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
import { getTeamQueue } from "./draft-queue";
|
import { getTeamQueue } from "./draft-queue";
|
||||||
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
||||||
import { getParticipantsForSeasonWithSports } from "./participant";
|
import { getParticipantsForSeasonWithSports } from "./participant";
|
||||||
|
|
@ -12,11 +13,13 @@ import { getSocketIO } from "../../server/socket";
|
||||||
* Check if the next team has autodraft enabled and immediately execute their pick
|
* Check if the next team has autodraft enabled and immediately execute their pick
|
||||||
* This is called after a pick is made to chain autodraft picks
|
* This is called after a pick is made to chain autodraft picks
|
||||||
*/
|
*/
|
||||||
|
type DraftSlot = { teamId: string; draftOrder: number };
|
||||||
|
|
||||||
export async function checkAndTriggerNextAutodraft(params: {
|
export async function checkAndTriggerNextAutodraft(params: {
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
nextPickNumber: number;
|
nextPickNumber: number;
|
||||||
totalTeams: number;
|
totalTeams: number;
|
||||||
draftSlots: any[];
|
draftSlots: DraftSlot[];
|
||||||
db?: ReturnType<typeof database>;
|
db?: ReturnType<typeof database>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { seasonId, totalTeams, draftSlots, db: providedDb } = params;
|
const { seasonId, totalTeams, draftSlots, db: providedDb } = params;
|
||||||
|
|
@ -367,20 +370,26 @@ export function getTeamForPick(
|
||||||
* @param params.db - Database instance (optional, will use database() if not provided)
|
* @param params.db - Database instance (optional, will use database() if not provided)
|
||||||
* @returns Result object with success status and pick data
|
* @returns Result object with success status and pick data
|
||||||
*/
|
*/
|
||||||
|
type AutodraftSettings = InferSelectModel<typeof schema.autodraftSettings>;
|
||||||
|
|
||||||
export async function executeAutoPick(params: {
|
export async function executeAutoPick(params: {
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
pickNumber: number;
|
pickNumber: number;
|
||||||
triggeredBy: "commissioner" | "timer";
|
triggeredBy: "commissioner" | "timer";
|
||||||
commissionerUserId?: string;
|
commissionerUserId?: string;
|
||||||
autodraftSettings?: any;
|
autodraftSettings?: AutodraftSettings | null;
|
||||||
db?: ReturnType<typeof database>;
|
db?: ReturnType<typeof database>;
|
||||||
chainEnabled?: boolean; // Set to false when called from within the autodraft chain to prevent recursion
|
chainEnabled?: boolean; // Set to false when called from within the autodraft chain to prevent recursion
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
pick?: any;
|
pick?: InferSelectModel<typeof schema.draftPicks>;
|
||||||
participant?: any;
|
participant?: InferSelectModel<typeof schema.participants> & {
|
||||||
|
sportsSeason: InferSelectModel<typeof schema.sportsSeasons> & {
|
||||||
|
sport: InferSelectModel<typeof schema.sports>;
|
||||||
|
};
|
||||||
|
};
|
||||||
nextPickNumber?: number;
|
nextPickNumber?: number;
|
||||||
isDraftComplete?: boolean;
|
isDraftComplete?: boolean;
|
||||||
}> {
|
}> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import type { RouterContextProvider } from "react-router";
|
||||||
import { action } from "~/routes/api/draft.force-manual-pick";
|
import { action } from "~/routes/api/draft.force-manual-pick";
|
||||||
|
|
||||||
|
const ctx = {} as unknown as RouterContextProvider;
|
||||||
|
|
||||||
vi.mock("~/database/context");
|
vi.mock("~/database/context");
|
||||||
vi.mock("~/server/socket", () => ({
|
vi.mock("~/server/socket", () => ({
|
||||||
getSocketIO: vi.fn(),
|
getSocketIO: vi.fn(),
|
||||||
|
|
@ -183,7 +186,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
const { getAuth } = await import("@clerk/react-router/server");
|
const { getAuth } = await import("@clerk/react-router/server");
|
||||||
vi.mocked(getAuth).mockResolvedValue({ userId: null } as any);
|
vi.mocked(getAuth).mockResolvedValue({ userId: null } as any);
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
@ -191,7 +194,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
it("returns 403 when authenticated user is not a commissioner", async () => {
|
it("returns 403 when authenticated user is not a commissioner", async () => {
|
||||||
mockDb.query.commissioners.findFirst.mockResolvedValue(null);
|
mockDb.query.commissioners.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -206,7 +209,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
const response = await action({
|
const response = await action({
|
||||||
request: makeRequest({ seasonId: SEASON_ID }), // missing teamId, participantId, pickNumber
|
request: makeRequest({ seasonId: SEASON_ID }), // missing teamId, participantId, pickNumber
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
|
|
@ -215,18 +218,45 @@ describe("draft.force-manual-pick action", () => {
|
||||||
it("returns 404 when the season does not exist", async () => {
|
it("returns 404 when the season does not exist", async () => {
|
||||||
mockDb.query.seasons.findFirst.mockResolvedValue(null);
|
mockDb.query.seasons.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 400 when the draft is not active", async () => {
|
||||||
|
mockDb.query.seasons.findFirst.mockResolvedValue({ ...mockSeason, status: "pre_draft" });
|
||||||
|
|
||||||
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
const data = await response.json();
|
||||||
|
expect(data.error).toMatch(/not currently active/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 when a pick already exists at the requested slot", async () => {
|
||||||
|
mockDb.query.draftPicks.findFirst.mockResolvedValueOnce({
|
||||||
|
id: "existing-pick",
|
||||||
|
participantId: "other-participant",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
const data = await response.json();
|
||||||
|
expect(data.error).toMatch(/already exists at this slot/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns 400 when the participant is already drafted", async () => {
|
it("returns 400 when the participant is already drafted", async () => {
|
||||||
mockDb.query.draftPicks.findFirst.mockResolvedValue({
|
// First call: slot check (no pick at this slot number)
|
||||||
|
// Second call: participant check (participant already drafted elsewhere)
|
||||||
|
mockDb.query.draftPicks.findFirst
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
id: "existing-pick",
|
id: "existing-pick",
|
||||||
participantId: PARTICIPANT_ID,
|
participantId: PARTICIPANT_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -236,7 +266,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
it("returns 404 when the participant does not exist", async () => {
|
it("returns 404 when the participant does not exist", async () => {
|
||||||
mockDb.query.participants.findFirst.mockResolvedValue(null);
|
mockDb.query.participants.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
@ -248,7 +278,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
ineligibleReasons: { [SPORT_ID]: "Sport limit reached" },
|
ineligibleReasons: { [SPORT_ID]: "Sport limit reached" },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -260,7 +290,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
|
|
||||||
describe("successful pick", () => {
|
describe("successful pick", () => {
|
||||||
it("returns 200 with pick data and next pick number", async () => {
|
it("returns 200 with pick data and next pick number", async () => {
|
||||||
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
|
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -280,7 +310,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
pickNumber: "6",
|
pickNumber: "6",
|
||||||
}),
|
}),
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -288,7 +318,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits pick-made to the correct draft room", async () => {
|
it("emits pick-made to the correct draft room", async () => {
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${SEASON_ID}`);
|
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${SEASON_ID}`);
|
||||||
expect(mockSocketIO.emit).toHaveBeenCalledWith(
|
expect(mockSocketIO.emit).toHaveBeenCalledWith(
|
||||||
|
|
@ -298,7 +328,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the participant from all team queues in the season", async () => {
|
it("removes the participant from all team queues in the season", async () => {
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockDb.delete).toHaveBeenCalled();
|
expect(mockDb.delete).toHaveBeenCalled();
|
||||||
expect(mockSocketIO.emit).toHaveBeenCalledWith("participant-removed-from-queues", {
|
expect(mockSocketIO.emit).toHaveBeenCalledWith("participant-removed-from-queues", {
|
||||||
|
|
@ -324,7 +354,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
timeRemaining: 75,
|
timeRemaining: 75,
|
||||||
});
|
});
|
||||||
|
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockDb.set).toHaveBeenCalledWith(
|
expect(mockDb.set).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ timeRemaining: 105 })
|
expect.objectContaining({ timeRemaining: 105 })
|
||||||
|
|
@ -339,7 +369,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
timeRemaining: 75,
|
timeRemaining: 75,
|
||||||
});
|
});
|
||||||
|
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockSocketIO.emit).toHaveBeenCalledWith("timer-update", {
|
expect(mockSocketIO.emit).toHaveBeenCalledWith("timer-update", {
|
||||||
seasonId: SEASON_ID,
|
seasonId: SEASON_ID,
|
||||||
|
|
@ -352,7 +382,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
it("creates a new timer for the picking team if they have no existing timer", async () => {
|
it("creates a new timer for the picking team if they have no existing timer", async () => {
|
||||||
mockDb.query.draftTimers.findFirst.mockResolvedValue(null);
|
mockDb.query.draftTimers.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
// insert should be called for: (1) draft pick, (2) new timer (0 + 30 = 30s)
|
// insert should be called for: (1) draft pick, (2) new timer (0 + 30 = 30s)
|
||||||
expect(mockDb.insert).toHaveBeenCalledTimes(2);
|
expect(mockDb.insert).toHaveBeenCalledTimes(2);
|
||||||
|
|
@ -367,7 +397,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
timeRemaining: 100,
|
timeRemaining: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
// 100 + 30 = 130, not 120 (which would be a reset to initialTime)
|
// 100 + 30 = 130, not 120 (which would be a reset to initialTime)
|
||||||
expect(mockDb.set).toHaveBeenCalledWith(
|
expect(mockDb.set).toHaveBeenCalledWith(
|
||||||
|
|
@ -387,7 +417,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
// once to get the picking team's current balance, and
|
// once to get the picking team's current balance, and
|
||||||
// once to find (and then reset) the next team's timer.
|
// once to find (and then reset) the next team's timer.
|
||||||
// After the fix it should be called exactly once.
|
// After the fix it should be called exactly once.
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockDb.query.draftTimers.findFirst).toHaveBeenCalledTimes(1);
|
expect(mockDb.query.draftTimers.findFirst).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
@ -395,7 +425,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
it("REGRESSION: does not emit a timer-update for the next team", async () => {
|
it("REGRESSION: does not emit a timer-update for the next team", async () => {
|
||||||
// Before the fix, a timer-update was emitted for the next team with
|
// Before the fix, a timer-update was emitted for the next team with
|
||||||
// timeRemaining: draftInitialTime (120s), overwriting their actual bank.
|
// timeRemaining: draftInitialTime (120s), overwriting their actual bank.
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
const nextTeamTimerEmits = mockSocketIO.emit.mock.calls.filter(
|
const nextTeamTimerEmits = mockSocketIO.emit.mock.calls.filter(
|
||||||
([event, payload]: [string, any]) =>
|
([event, payload]: [string, any]) =>
|
||||||
|
|
@ -408,7 +438,7 @@ describe("draft.force-manual-pick action", () => {
|
||||||
// Verify that db.update is called exactly twice (timer increment + season
|
// Verify that db.update is called exactly twice (timer increment + season
|
||||||
// pick number), not three times (which would include resetting the next
|
// pick number), not three times (which would include resetting the next
|
||||||
// team's timer).
|
// team's timer).
|
||||||
await action({ request: defaultPickRequest(), params: {}, context: {} });
|
await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||||
|
|
||||||
expect(mockDb.update).toHaveBeenCalledTimes(2);
|
expect(mockDb.update).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { getSocketIO } from "~/server/socket";
|
import { getSocketIO } from "~/server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { executeAutoPick } from "~/models/draft-utils";
|
import { executeAutoPick } from "~/models/draft-utils";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/d
|
||||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
|
||||||
export async function action(args: any) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
@ -50,6 +50,22 @@ export async function action(args: any) {
|
||||||
return Response.json({ error: "Only commissioners can force a manual pick" }, { status: 403 });
|
return Response.json({ error: "Only commissioners can force a manual pick" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (season.status !== "draft") {
|
||||||
|
return Response.json({ error: "Draft is not currently active" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a pick already exists at this pick number slot (prevents duplicate picks at same slot)
|
||||||
|
const existingPickAtSlot = await db.query.draftPicks.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.draftPicks.seasonId, seasonId),
|
||||||
|
eq(schema.draftPicks.pickNumber, pickNumber)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPickAtSlot) {
|
||||||
|
return Response.json({ error: "A pick already exists at this slot" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// Check if participant is already drafted
|
// Check if participant is already drafted
|
||||||
const existingPick = await db.query.draftPicks.findFirst({
|
const existingPick = await db.query.draftPicks.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
@ -56,12 +56,11 @@ export async function action(args: any) {
|
||||||
|
|
||||||
const totalTeams = draftSlots.length;
|
const totalTeams = draftSlots.length;
|
||||||
const currentRound = Math.ceil(currentPickNumber / totalTeams);
|
const currentRound = Math.ceil(currentPickNumber / totalTeams);
|
||||||
const isSnakeDraft = true; // Assuming snake draft
|
|
||||||
const isEvenRound = currentRound % 2 === 0;
|
const isEvenRound = currentRound % 2 === 0;
|
||||||
|
|
||||||
// Calculate which team should pick
|
// Calculate which team should pick (snake draft)
|
||||||
let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1;
|
let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1;
|
||||||
if (isSnakeDraft && isEvenRound) {
|
if (isEvenRound) {
|
||||||
pickInRound = totalTeams - pickInRound + 1;
|
pickInRound = totalTeams - pickInRound + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq, and, gte } from "drizzle-orm";
|
import { eq, and, gte } from "drizzle-orm";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { getSocketIO } from "../../../server/socket";
|
import { getSocketIO } from "../../../server/socket";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { addToQueue, getTeamQueue, isParticipantInQueue } from "~/models/draft-queue";
|
import { addToQueue, getTeamQueue, isParticipantInQueue } from "~/models/draft-queue";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { clearTeamQueue } from "~/models/draft-queue";
|
import { clearTeamQueue } from "~/models/draft-queue";
|
||||||
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
|
||||||
export async function action(args: any) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { removeFromQueue, getTeamQueue } from "~/models/draft-queue";
|
import { removeFromQueue, getTeamQueue } from "~/models/draft-queue";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { reorderQueueWithSeason, getTeamQueue } from "~/models/draft-queue";
|
import { reorderQueueWithSeason, getTeamQueue } from "~/models/draft-queue";
|
||||||
|
|
||||||
export async function action(args: any) {
|
import type { ActionFunctionArgs } from "react-router";
|
||||||
|
export async function action(args: ActionFunctionArgs) {
|
||||||
const { request } = args;
|
const { request } = args;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import { DraftGrid } from "~/components/DraftGrid";
|
||||||
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { buildOwnerMap } from "~/lib/owner-map";
|
import { buildOwnerMap } from "~/lib/owner-map";
|
||||||
|
import type { Route } from "./+types/$leagueId.draft-board.$seasonId";
|
||||||
|
|
||||||
export async function loader(args: any) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
const { params } = args;
|
const { params } = args;
|
||||||
const { leagueId, seasonId } = params;
|
const { leagueId, seasonId } = params;
|
||||||
|
|
||||||
|
|
@ -38,8 +39,7 @@ export async function loader(args: any) {
|
||||||
// Check access: public boards are accessible to everyone without auth
|
// Check access: public boards are accessible to everyone without auth
|
||||||
if (!season.league.isPublicDraftBoard) {
|
if (!season.league.isPublicDraftBoard) {
|
||||||
// Not public - check if the user is a league member or commissioner
|
// Not public - check if the user is a league member or commissioner
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new Response("This draft board is not public", { status: 403 });
|
throw new Response("This draft board is not public", { status: 403 });
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
||||||
import { NotificationSettings } from "~/components/NotificationSettings";
|
import { NotificationSettings } from "~/components/NotificationSettings";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
||||||
|
import type { Route } from "./+types/$leagueId.draft.$seasonId";
|
||||||
|
|
||||||
export async function loader(args: any) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
const { params } = args;
|
const { params } = args;
|
||||||
const { seasonId, leagueId } = params;
|
const { seasonId, leagueId } = params;
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
|
|
||||||
if (!seasonId) {
|
if (!seasonId) {
|
||||||
throw new Response("Season ID is required", { status: 400 });
|
throw new Response("Season ID is required", { status: 400 });
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import {
|
||||||
findDraftSlotsBySeasonId,
|
findDraftSlotsBySeasonId,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
import { findCurrentSeasonWithSports } from "~/models/season";
|
import { findCurrentSeasonWithSports } from "~/models/season";
|
||||||
|
import type { Route } from "./+types/$leagueId";
|
||||||
|
|
||||||
export async function loader(args: any) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
const { userId } = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const { params } = args;
|
const { params } = args;
|
||||||
const { leagueId } = params;
|
const { leagueId } = params;
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import { StandingsTable } from "~/components/standings/StandingsTable";
|
||||||
import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings";
|
import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings";
|
||||||
import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server";
|
import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server";
|
||||||
import { PointProgressionChart } from "~/components/standings/PointProgressionChart";
|
import { PointProgressionChart } from "~/components/standings/PointProgressionChart";
|
||||||
|
import type { Route } from "./+types/$leagueId.standings.$seasonId";
|
||||||
|
|
||||||
export async function loader(args: any) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
try {
|
try {
|
||||||
const auth = await getAuth(args);
|
const { userId } = await getAuth(args);
|
||||||
const userId = (auth as any).userId as string | null;
|
|
||||||
const { params } = args;
|
const { params } = args;
|
||||||
const { leagueId, seasonId } = params;
|
const { leagueId, seasonId } = params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import type { RouterContextProvider } from "react-router";
|
||||||
import { action } from "~/routes/api/autodraft.update";
|
import { action } from "~/routes/api/autodraft.update";
|
||||||
|
|
||||||
|
const ctx = {} as unknown as RouterContextProvider;
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock("~/database/context");
|
vi.mock("~/database/context");
|
||||||
vi.mock("~/server/socket", () => ({
|
vi.mock("~/server/socket", () => ({
|
||||||
|
|
@ -94,7 +97,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response = await action({
|
const response = await action({
|
||||||
request,
|
request,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -163,7 +166,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response = await action({
|
const response = await action({
|
||||||
request,
|
request,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -206,7 +209,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response = await action({
|
const response = await action({
|
||||||
request,
|
request,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.status).toBe(403);
|
expect(response.status).toBe(403);
|
||||||
|
|
@ -227,7 +230,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response = await action({
|
const response = await action({
|
||||||
request,
|
request,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
|
|
@ -274,7 +277,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response1 = await action({
|
const response1 = await action({
|
||||||
request: request1,
|
request: request1,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data1 = await response1.json();
|
const data1 = await response1.json();
|
||||||
|
|
@ -306,7 +309,7 @@ describe("Autodraft Settings API", () => {
|
||||||
const response2 = await action({
|
const response2 = await action({
|
||||||
request: request2,
|
request: request2,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data2 = await response2.json();
|
const data2 = await response2.json();
|
||||||
|
|
@ -355,7 +358,7 @@ describe("Autodraft Settings API", () => {
|
||||||
await action({
|
await action({
|
||||||
request,
|
request,
|
||||||
params: {},
|
params: {},
|
||||||
context: {},
|
context: ctx,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
|
expect(mockSocketIO.to).toHaveBeenCalledWith(`draft-${seasonId}`);
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ interface ServerToClientEvents {
|
||||||
"team-connected": (data: { teamId: string }) => void;
|
"team-connected": (data: { teamId: string }) => void;
|
||||||
"team-disconnected": (data: { teamId: string }) => void;
|
"team-disconnected": (data: { teamId: string }) => void;
|
||||||
"connected-teams-list": (data: { teamIds: string[] }) => void;
|
"connected-teams-list": (data: { teamIds: string[] }) => void;
|
||||||
"draft-paused": () => void;
|
"draft-paused": (data: { seasonId: string; paused: boolean }) => void;
|
||||||
"draft-resumed": () => void;
|
"draft-resumed": (data: { seasonId: string; paused: boolean }) => void;
|
||||||
"participant-removed-from-queues": (data: { participantId: string }) => void;
|
"participant-removed-from-queues": (data: { participantId: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue