brackt/app/utils/sports-data-sync.server.ts
Claude dd5072a2c2
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
2026-02-21 03:45:00 +00:00

454 lines
13 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
interface ExportData {
version: string;
exportedAt: string;
sports: Array<{
name: string;
type: string;
slug: string;
description: string | null;
}>;
sportsSeasons: Array<{
sportSlug: string;
name: string;
year: number;
startDate: string | null;
endDate: string | null;
status: string;
scoringType: string;
}>;
participants: Array<{
sportSlug: string;
seasonName: string;
seasonYear: number;
name: string;
shortName: string | null;
externalId: string | null;
expectedValue: number | string;
}>;
participantExpectedValues?: Array<{
sportSlug: string;
seasonName: string;
seasonYear: number;
participantName: string;
probFirst: string;
probSecond: string;
probThird: string;
probFourth: string;
probFifth: string;
probSixth: string;
probSeventh: string;
probEighth: string;
expectedValue: string;
source: string | null;
sourceOdds: number | null;
}>;
seasonTemplates: Array<{
name: string;
year: number;
description: string | null;
isActive: boolean;
sports: Array<{
sportSlug: string;
seasonName: string;
seasonYear: number;
}>;
}>;
}
export async function exportSportsDataToJSON(): Promise<ExportData> {
const db = database();
// Export sports
const sports = await db.query.sports.findMany({
orderBy: (sports, { asc }) => [asc(sports.name)],
});
// Export sports seasons with sport relation
const sportsSeasons = await db.query.sportsSeasons.findMany({
with: {
sport: true,
},
orderBy: (sportsSeasons, { desc, asc }) => [
desc(sportsSeasons.year),
asc(sportsSeasons.name),
],
});
// Export participants with relations
const participants = await db.query.participants.findMany({
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
orderBy: (participants, { asc }) => [asc(participants.name)],
});
// Export participant expected values with relations
const participantEVs = await db.query.participantExpectedValues.findMany({
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
// Export season templates with sports
const seasonTemplates = await db.query.seasonTemplates.findMany({
with: {
seasonTemplateSports: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
orderBy: (templates, { desc }) => [desc(templates.year)],
});
// Build export data structure
return {
version: "1.0",
exportedAt: new Date().toISOString(),
sports: sports.map((sport) => ({
name: sport.name,
type: sport.type,
slug: sport.slug,
description: sport.description,
})),
sportsSeasons: sportsSeasons.map((season) => ({
sportSlug: season.sport.slug,
name: season.name,
year: season.year,
startDate: season.startDate,
endDate: season.endDate,
status: season.status,
scoringType: season.scoringType,
})),
participants: participants.map((participant) => ({
sportSlug: participant.sportsSeason.sport.slug,
seasonName: participant.sportsSeason.name,
seasonYear: participant.sportsSeason.year,
name: participant.name,
shortName: participant.shortName,
externalId: participant.externalId,
expectedValue: participant.expectedValue,
})),
participantExpectedValues: participantEVs.map((ev) => ({
sportSlug: ev.participant.sportsSeason.sport.slug,
seasonName: ev.participant.sportsSeason.name,
seasonYear: ev.participant.sportsSeason.year,
participantName: ev.participant.name,
probFirst: ev.probFirst,
probSecond: ev.probSecond,
probThird: ev.probThird,
probFourth: ev.probFourth,
probFifth: ev.probFifth,
probSixth: ev.probSixth,
probSeventh: ev.probSeventh,
probEighth: ev.probEighth,
expectedValue: ev.expectedValue,
source: ev.source,
sourceOdds: ev.sourceOdds,
})),
seasonTemplates: seasonTemplates.map((template) => ({
name: template.name,
year: template.year,
description: template.description,
isActive: template.isActive,
sports: template.seasonTemplateSports.map((ts) => ({
sportSlug: ts.sportsSeason.sport.slug,
seasonName: ts.sportsSeason.name,
seasonYear: ts.sportsSeason.year,
})),
})),
};
}
export async function importSportsDataFromJSON(
jsonData: string,
mode: "merge" | "replace" = "merge"
): Promise<{ created: number; updated: number; skipped: number }> {
const db = database();
const importData: ExportData = JSON.parse(jsonData);
let created = 0;
let updated = 0;
let skipped = 0;
if (mode === "replace") {
// Delete in reverse order of dependencies
await db.delete(schema.seasonTemplateSports);
await db.delete(schema.seasonTemplates);
await db.delete(schema.participantResults);
await db.delete(schema.participants);
await db.delete(schema.seasonSports);
await db.delete(schema.sportsSeasons);
await db.delete(schema.sports);
}
// Track created IDs for relationships
const sportIdMap = new Map<string, string>();
const sportsSeasonIdMap = new Map<string, string>();
// Import sports
for (const sport of importData.sports) {
const existing = await db.query.sports.findFirst({
where: eq(schema.sports.slug, sport.slug),
});
if (existing) {
if (mode === "merge") {
await db
.update(schema.sports)
.set({
name: sport.name,
type: sport.type as "team" | "individual",
description: sport.description,
})
.where(eq(schema.sports.slug, sport.slug));
sportIdMap.set(sport.slug, existing.id);
updated++;
}
} else {
const [createdSport] = await db
.insert(schema.sports)
.values({
name: sport.name,
type: sport.type as "team" | "individual",
slug: sport.slug,
description: sport.description,
})
.returning();
sportIdMap.set(sport.slug, createdSport.id);
created++;
}
}
// Import sports seasons
for (const season of importData.sportsSeasons) {
const sportId = sportIdMap.get(season.sportSlug);
if (!sportId) {
skipped++;
continue;
}
const existing = await db.query.sportsSeasons.findFirst({
where: and(
eq(schema.sportsSeasons.sportId, sportId),
eq(schema.sportsSeasons.name, season.name),
eq(schema.sportsSeasons.year, season.year)
),
});
const seasonKey = `${season.sportSlug}:${season.name}:${season.year}`;
if (existing) {
if (mode === "merge") {
await db
.update(schema.sportsSeasons)
.set({
startDate: season.startDate,
endDate: season.endDate,
status: season.status as "upcoming" | "active" | "completed",
scoringType: season.scoringType as "playoffs" | "regular_season" | "majors",
})
.where(eq(schema.sportsSeasons.id, existing.id));
sportsSeasonIdMap.set(seasonKey, existing.id);
updated++;
}
} else {
const [createdSeason] = await db
.insert(schema.sportsSeasons)
.values({
sportId,
name: season.name,
year: season.year,
startDate: season.startDate,
endDate: season.endDate,
status: season.status as "upcoming" | "active" | "completed",
scoringType: season.scoringType as "playoffs" | "regular_season" | "majors",
})
.returning();
sportsSeasonIdMap.set(seasonKey, createdSeason.id);
created++;
}
}
// Import participants
for (const participant of importData.participants) {
const seasonKey = `${participant.sportSlug}:${participant.seasonName}:${participant.seasonYear}`;
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
if (!sportsSeasonId) {
skipped++;
continue;
}
const existing = await db.query.participants.findFirst({
where: and(
eq(schema.participants.sportsSeasonId, sportsSeasonId),
eq(schema.participants.name, participant.name)
),
});
if (!existing) {
await db.insert(schema.participants).values({
sportsSeasonId,
name: participant.name,
shortName: participant.shortName,
externalId: participant.externalId,
expectedValue: String(participant.expectedValue ?? 0),
});
created++;
} else {
skipped++;
}
}
// Import participant expected values (optional field for backward compatibility)
if (importData.participantExpectedValues) {
for (const ev of importData.participantExpectedValues) {
const seasonKey = `${ev.sportSlug}:${ev.seasonName}:${ev.seasonYear}`;
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
if (!sportsSeasonId) {
skipped++;
continue;
}
const participant = await db.query.participants.findFirst({
where: and(
eq(schema.participants.sportsSeasonId, sportsSeasonId),
eq(schema.participants.name, ev.participantName)
),
});
if (!participant) {
skipped++;
continue;
}
const existingEV = await db.query.participantExpectedValues.findFirst({
where: and(
eq(schema.participantExpectedValues.participantId, participant.id),
eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)
),
});
const evValues = {
probFirst: ev.probFirst,
probSecond: ev.probSecond,
probThird: ev.probThird,
probFourth: ev.probFourth,
probFifth: ev.probFifth,
probSixth: ev.probSixth,
probSeventh: ev.probSeventh,
probEighth: ev.probEighth,
expectedValue: ev.expectedValue,
source: ev.source as "manual" | "futures_odds" | "elo_simulation" | "performance_model" | null,
sourceOdds: ev.sourceOdds,
};
if (existingEV) {
if (mode === "merge") {
await db
.update(schema.participantExpectedValues)
.set({ ...evValues, updatedAt: new Date() })
.where(eq(schema.participantExpectedValues.id, existingEV.id));
await db
.update(schema.participants)
.set({ expectedValue: ev.expectedValue })
.where(eq(schema.participants.id, participant.id));
updated++;
}
} else {
await db.insert(schema.participantExpectedValues).values({
participantId: participant.id,
sportsSeasonId,
...evValues,
});
await db
.update(schema.participants)
.set({ expectedValue: ev.expectedValue })
.where(eq(schema.participants.id, participant.id));
created++;
}
}
}
// Import season templates
for (const template of importData.seasonTemplates) {
const existing = await db.query.seasonTemplates.findFirst({
where: and(
eq(schema.seasonTemplates.name, template.name),
eq(schema.seasonTemplates.year, template.year)
),
});
let templateId: string;
if (existing) {
if (mode === "merge") {
await db
.update(schema.seasonTemplates)
.set({
description: template.description,
isActive: template.isActive,
})
.where(eq(schema.seasonTemplates.id, existing.id));
templateId = existing.id;
updated++;
// Remove existing template sports to re-add them
await db
.delete(schema.seasonTemplateSports)
.where(eq(schema.seasonTemplateSports.templateId, templateId));
} else {
continue;
}
} else {
const [createdTemplate] = await db
.insert(schema.seasonTemplates)
.values({
name: template.name,
year: template.year,
description: template.description,
isActive: template.isActive,
})
.returning();
templateId = createdTemplate.id;
created++;
}
// Add template sports
for (const templateSport of template.sports) {
const seasonKey = `${templateSport.sportSlug}:${templateSport.seasonName}:${templateSport.seasonYear}`;
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
if (sportsSeasonId) {
await db.insert(schema.seasonTemplateSports).values({
templateId,
sportsSeasonId,
});
}
}
}
return { created, updated, skipped };
}