Enforce unique team names within a season #49
7 changed files with 6240 additions and 4 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq, and, isNull } from "drizzle-orm";
|
import { eq, and, isNull, ne, sql } from "drizzle-orm";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
import { generateFlagConfig } from "~/lib/flag-generator";
|
import { generateFlagConfig } from "~/lib/flag-generator";
|
||||||
|
|
@ -61,6 +61,21 @@ export async function findTeamByOwnerAndSeason(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findTeamByNameInSeason(
|
||||||
|
seasonId: string,
|
||||||
|
name: string,
|
||||||
|
excludeTeamId?: string
|
||||||
|
): Promise<Team | undefined> {
|
||||||
|
const db = database();
|
||||||
|
return await db.query.teams.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.teams.seasonId, seasonId),
|
||||||
|
sql`lower(${schema.teams.name}) = lower(${name})`,
|
||||||
|
excludeTeamId ? ne(schema.teams.id, excludeTeamId) : undefined
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function findAvailableTeams(seasonId: string): Promise<Team[]> {
|
export async function findAvailableTeams(seasonId: string): Promise<Team[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.teams.findMany({
|
return await db.query.teams.findMany({
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { deleteSeasonTimers } from "~/models/draft-timer";
|
||||||
import { findCurrentSeasonWithSports, type NewSeason, updateSeason } from "~/models/season";
|
import { findCurrentSeasonWithSports, type NewSeason, updateSeason } from "~/models/season";
|
||||||
import { linkMultipleSportsToSeason, unlinkSportFromSeason } from "~/models/season-sport";
|
import { linkMultipleSportsToSeason, unlinkSportFromSeason } from "~/models/season-sport";
|
||||||
import { findDraftableSportsSeasons, findSportsSeasonsByIds } from "~/models/sports-season";
|
import { findDraftableSportsSeasons, findSportsSeasonsByIds } from "~/models/sports-season";
|
||||||
import { claimTeam, deleteTeam, findTeamById, findTeamsBySeasonId, removeTeamOwner, renameTeam } from "~/models/team";
|
import { claimTeam, deleteTeam, findTeamById, findTeamByNameInSeason, findTeamsBySeasonId, removeTeamOwner, renameTeam } from "~/models/team";
|
||||||
import { findUserById, findUsersByIds, getUserDisplayName, isUserAdmin } from "~/models/user";
|
import { findUserById, findUsersByIds, getUserDisplayName, isUserAdmin } from "~/models/user";
|
||||||
import { logCommissionerAction } from "~/models/audit-log";
|
import { logCommissionerAction } from "~/models/audit-log";
|
||||||
import { parseDraftSpeed } from "~/lib/draft-timer";
|
import { parseDraftSpeed } from "~/lib/draft-timer";
|
||||||
|
|
@ -417,6 +417,10 @@ export async function action(args: Route.ActionArgs) {
|
||||||
if (username) {
|
if (username) {
|
||||||
const strippedName = stripOwnerFromTeamName(team.name, username);
|
const strippedName = stripOwnerFromTeamName(team.name, username);
|
||||||
if (strippedName !== team.name) {
|
if (strippedName !== team.name) {
|
||||||
|
const nameConflict = await findTeamByNameInSeason(team.seasonId, strippedName, teamId);
|
||||||
|
if (nameConflict) {
|
||||||
|
return { error: "Cannot remove owner: the resulting team name is already taken in this season." };
|
||||||
|
}
|
||||||
await renameTeam(teamId, strippedName);
|
await renameTeam(teamId, strippedName);
|
||||||
}
|
}
|
||||||
await removeTeamOwner(teamId);
|
await removeTeamOwner(teamId);
|
||||||
|
|
@ -478,6 +482,10 @@ export async function action(args: Route.ActionArgs) {
|
||||||
targetTeam.name,
|
targetTeam.name,
|
||||||
assignedUser.username ?? assignedUser.displayName ?? "Member"
|
assignedUser.username ?? assignedUser.displayName ?? "Member"
|
||||||
);
|
);
|
||||||
|
const nameConflict = await findTeamByNameInSeason(season.id, teamName, teamId);
|
||||||
|
if (nameConflict) {
|
||||||
|
return { error: "Cannot assign owner: the resulting team name is already taken in this season." };
|
||||||
|
}
|
||||||
await claimTeam(teamId, assignedUserId, teamName);
|
await claimTeam(teamId, assignedUserId, teamName);
|
||||||
|
|
||||||
return { success: true, message: "Owner assigned successfully" };
|
return { success: true, message: "Owner assigned successfully" };
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { Route } from "./+types/$teamId.settings";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findTeamById,
|
findTeamById,
|
||||||
|
findTeamByNameInSeason,
|
||||||
updateTeam,
|
updateTeam,
|
||||||
removeTeamOwner,
|
removeTeamOwner,
|
||||||
} from "~/models/team";
|
} from "~/models/team";
|
||||||
|
|
@ -156,8 +157,17 @@ export async function action(args: Route.ActionArgs) {
|
||||||
return { error: "Team name is required" };
|
return { error: "Team name is required" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
if (!trimmedName) {
|
||||||
|
return { error: "Team name is required" };
|
||||||
|
}
|
||||||
|
const conflict = await findTeamByNameInSeason(team.seasonId, trimmedName, teamId);
|
||||||
|
if (conflict) {
|
||||||
|
return { error: "A team with that name already exists in this season." };
|
||||||
|
}
|
||||||
|
|
||||||
await updateTeam(teamId, {
|
await updateTeam(teamId, {
|
||||||
name: name.trim(),
|
name: trimmedName,
|
||||||
});
|
});
|
||||||
await syncPrivateBracktParticipants(team.seasonId);
|
await syncPrivateBracktParticipants(team.seasonId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,9 @@ export const teams = pgTable("teams", {
|
||||||
ownerId: varchar("owner_id", { length: 255 }), // UUID → users.id, nullable
|
ownerId: varchar("owner_id", { length: 255 }), // UUID → users.id, nullable
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
}, (t) => [
|
||||||
|
uniqueIndex("teams_season_id_lower_name_unique").on(t.seasonId, sql`lower(${t.name})`),
|
||||||
|
]);
|
||||||
|
|
||||||
export const draftSlots = pgTable("draft_slots", {
|
export const draftSlots = pgTable("draft_slots", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
|
||||||
1
drizzle/0113_bent_banshee.sql
Normal file
1
drizzle/0113_bent_banshee.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "teams_season_id_lower_name_unique" ON "teams" USING btree ("season_id",lower("name"));
|
||||||
6193
drizzle/meta/0113_snapshot.json
Normal file
6193
drizzle/meta/0113_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -792,6 +792,13 @@
|
||||||
"when": 1779318579169,
|
"when": 1779318579169,
|
||||||
"tag": "0112_married_captain_midlands",
|
"tag": "0112_married_captain_midlands",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 113,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779579877169,
|
||||||
|
"tag": "0113_bent_banshee",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue