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
454 lines
13 KiB
TypeScript
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 };
|
|
}
|