* Include participant EV/futures data in data sync export/import The export now includes the full participantExpectedValues records (placement probabilities, source, sourceOdds) alongside participants. On import, EV records are upserted in merge mode and created fresh in replace mode (cascade handles cleanup). Backward-compatible with older exports that lack the participantExpectedValues field. https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B * Address all data-sync code review findings - Wrap importSportsDataFromJSON in a db.transaction() so any mid-import failure rolls back cleanly instead of leaving partial data - Add zod validation of imported JSON before any DB writes, giving a clear error on malformed/incompatible files - Fix N+1 queries: build participantIdMap during participant import so the EV and results sections resolve IDs from memory, not extra queries - Fix merge mode silently skipping existing participants — now updates shortName, externalId, and expectedValue - Add participantResults to export/import (was deleted in replace mode but never exported, so results were permanently lost) - Restore calculatedAt timestamp on EV import instead of defaulting to now(); stored as ISO string in the export for portability - Document that onDelete:cascade covers participantExpectedValues and participantResults when participants is deleted; remove the now- redundant explicit participantResults delete from replace mode - Bump export version to 1.1; old v1.0 files still import cleanly since participantExpectedValues, participantResults, and calculatedAt are all optional in the zod schema - Collapse all six DB fetches in exportSportsDataToJSON into one Promise.all for parallel execution - Add countAllParticipants() and countAllParticipantEVs() model funcs - Show Participants and EV Records counts on the dashboard stat cards - Replace raw DOM form creation/submit in handleImport with useFetcher, giving proper loading state, no full-page reload, and type-safe data - Remove dead export intent handler from the action function https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B --------- Co-authored-by: Claude <noreply@anthropic.com>
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import { eq, inArray, count } 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 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;
|
|
}
|