brackt/app/routes/api/draft.rollback.ts
Chris Parsons 686ccd7188
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>
2026-02-22 19:26:45 -08:00

105 lines
3 KiB
TypeScript

import { getAuth } from "@clerk/react-router/server";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, gte } from "drizzle-orm";
import { getSocketIO } from "../../../server/socket";
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await request.formData();
const seasonId = formData.get("seasonId") as string;
const pickNumber = parseInt(formData.get("pickNumber") as string);
if (!seasonId || isNaN(pickNumber) || pickNumber < 1) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
const db = database();
const season = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
});
if (!season) {
return Response.json({ error: "Season not found" }, { status: 404 });
}
const isCommissioner = await db.query.commissioners.findFirst({
where: and(
eq(schema.commissioners.leagueId, season.leagueId),
eq(schema.commissioners.userId, userId)
),
});
if (!isCommissioner) {
return Response.json({ error: "Only commissioners can roll back the draft" }, { status: 403 });
}
if (season.status !== "draft") {
return Response.json(
{ error: "Cannot roll back a draft that is not in progress" },
{ status: 400 }
);
}
const draftSlots = await db.query.draftSlots.findMany({
where: eq(schema.draftSlots.seasonId, seasonId),
orderBy: schema.draftSlots.draftOrder,
});
const totalTeams = draftSlots.length;
if (totalTeams === 0) {
return Response.json({ error: "No draft slots found for this season" }, { status: 400 });
}
// Calculate which team is on the clock at the rollback pick (snake draft)
const round = Math.ceil(pickNumber / totalTeams);
const isEvenRound = round % 2 === 0;
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
if (isEvenRound) {
pickInRound = totalTeams - pickInRound + 1;
}
const rollbackSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound);
// Delete picks from pickNumber onwards
await db
.delete(schema.draftPicks)
.where(
and(
eq(schema.draftPicks.seasonId, seasonId),
gte(schema.draftPicks.pickNumber, pickNumber)
)
);
// Update season: reset pick number and unpause
await db
.update(schema.seasons)
.set({
currentPickNumber: pickNumber,
draftPaused: false,
})
.where(eq(schema.seasons.id, seasonId));
// Emit socket events
try {
const io = getSocketIO();
io.to(`draft-${seasonId}`).emit("draft-rolled-back", {
seasonId,
pickNumber,
teamId: rollbackSlot?.teamId,
});
} catch (error) {
console.error("Socket.IO error:", error);
}
return Response.json({ success: true, pickNumber });
}