- Add unique index on (sports_season_id, name) in participants table - findParticipantByName uses case-insensitive lower() comparison - Single add: check for existing name before insert, return clear error - Bulk add: load existing names once upfront (1 query vs N), dedup input case-insensitively, report skipped names in UI - Fix golf-skills and surface-elo routes which called createParticipant without any duplicate guard (would have thrown DB constraint errors) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
import { eq, inArray, count, and, sql } from "drizzle-orm";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
|
|
export type Participant = typeof schema.participants.$inferSelect;
|
|
export type NewParticipant = typeof schema.participants.$inferInsert;
|
|
|
|
export async function createParticipant(data: NewParticipant): Promise<Participant> {
|
|
const db = database();
|
|
const [participant] = await db
|
|
.insert(schema.participants)
|
|
.values(data)
|
|
.returning();
|
|
return participant;
|
|
}
|
|
|
|
export async function createManyParticipants(data: NewParticipant[]): Promise<Participant[]> {
|
|
const db = database();
|
|
return await db
|
|
.insert(schema.participants)
|
|
.values(data)
|
|
.returning();
|
|
}
|
|
|
|
export async function findParticipantById(id: string): Promise<Participant | undefined> {
|
|
const db = database();
|
|
return await db.query.participants.findFirst({
|
|
where: eq(schema.participants.id, id),
|
|
with: {
|
|
sportsSeason: {
|
|
with: {
|
|
sport: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function findParticipantByName(
|
|
sportsSeasonId: string,
|
|
name: string
|
|
): Promise<Participant | undefined> {
|
|
const db = database();
|
|
const results = await db
|
|
.select()
|
|
.from(schema.participants)
|
|
.where(
|
|
and(
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
sql`lower(${schema.participants.name}) = lower(${name})`
|
|
)
|
|
)
|
|
.limit(1);
|
|
return results[0];
|
|
}
|
|
|
|
export async function findParticipantsBySportsSeasonId(sportsSeasonId: string): Promise<Participant[]> {
|
|
const db = database();
|
|
return await db.query.participants.findMany({
|
|
where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
orderBy: (participants, { asc }) => [asc(participants.name)],
|
|
});
|
|
}
|
|
|
|
export async function findParticipantsByExternalId(
|
|
externalId: string
|
|
): Promise<Participant[]> {
|
|
const db = database();
|
|
return await db.query.participants.findMany({
|
|
where: eq(schema.participants.externalId, externalId),
|
|
with: {
|
|
sportsSeason: {
|
|
with: {
|
|
sport: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function updateParticipant(
|
|
id: string,
|
|
data: Partial<NewParticipant>
|
|
): Promise<Participant> {
|
|
const db = database();
|
|
const [participant] = await db
|
|
.update(schema.participants)
|
|
.set({ ...data, updatedAt: new Date() })
|
|
.where(eq(schema.participants.id, id))
|
|
.returning();
|
|
return participant;
|
|
}
|
|
|
|
export async function countAllParticipants(): Promise<number> {
|
|
const db = database();
|
|
const result = await db.select({ value: count() }).from(schema.participants);
|
|
return result[0].value;
|
|
}
|
|
|
|
export async function deleteParticipant(id: string): Promise<void> {
|
|
const db = database();
|
|
await db.delete(schema.participants).where(eq(schema.participants.id, id));
|
|
}
|
|
|
|
export async function copyParticipantsFromSeason(
|
|
sourceSportsSeasonId: string,
|
|
targetSportsSeasonId: string
|
|
): Promise<Participant[]> {
|
|
// Get all participants from source season
|
|
const sourceParticipants = await findParticipantsBySportsSeasonId(sourceSportsSeasonId);
|
|
|
|
// Insert them into the target season
|
|
if (sourceParticipants.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return await createManyParticipants(
|
|
sourceParticipants.map((p) => ({
|
|
sportsSeasonId: targetSportsSeasonId,
|
|
name: p.name,
|
|
shortName: p.shortName,
|
|
externalId: p.externalId,
|
|
expectedValue: p.expectedValue,
|
|
}))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all participants for a season with sport information
|
|
* Used for draft eligibility calculations
|
|
*/
|
|
export async function getParticipantsForSeasonWithSports(seasonId: string, providedDb?: ReturnType<typeof database>) {
|
|
const db = providedDb || database();
|
|
|
|
// First get all sports seasons for this season
|
|
const seasonSports = await db.query.seasonSports.findMany({
|
|
where: eq(schema.seasonSports.seasonId, seasonId),
|
|
});
|
|
|
|
if (seasonSports.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
|
|
|
// Get all participants for these sports seasons with sport info
|
|
const participants = await db
|
|
.select({
|
|
id: schema.participants.id,
|
|
name: schema.participants.name,
|
|
sport: {
|
|
id: schema.sports.id,
|
|
name: schema.sports.name,
|
|
},
|
|
})
|
|
.from(schema.participants)
|
|
.innerJoin(
|
|
schema.sportsSeasons,
|
|
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
|
)
|
|
.innerJoin(
|
|
schema.sports,
|
|
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
|
)
|
|
.where(
|
|
sportsSeasonIds.length === 1
|
|
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
|
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
|
|
);
|
|
|
|
return participants;
|
|
}
|