- Export: Downloads all sports data as a JSON file that you can
- save, version control, or transfer to another environment.
+ Export: Downloads all sports data as a JSON file
+ that you can save, version control, or transfer to another
+ environment.
- Import (Merge): Updates existing records and adds new ones.
- Safe to use - won't delete anything.
+ Import (Merge): Updates existing records and adds
+ new ones. Safe to use — won't delete anything.
- Import (Replace): Deletes all existing sports data and recreates
- it from the file. Use this for a clean slate.
+ Import (Replace): Deletes all existing sports
+ data and recreates it from the file. Use this for a clean slate.
Typical workflow:
diff --git a/app/utils/sports-data-sync.server.ts b/app/utils/sports-data-sync.server.ts
index 0aa99c5..dc64eea 100644
--- a/app/utils/sports-data-sync.server.ts
+++ b/app/utils/sports-data-sync.server.ts
@@ -1,128 +1,204 @@
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
+import { z } from "zod";
-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;
- }>;
- seasonTemplates: Array<{
- name: string;
- year: number;
- description: string | null;
- isActive: boolean;
- sports: Array<{
- sportSlug: string;
- seasonName: string;
- seasonYear: number;
- }>;
- }>;
-}
+// ─── Validation schema ────────────────────────────────────────────────────────
+// Used both for runtime validation of imported files and as the canonical shape
+// for the exported JSON (fixes: zod validation, calculatedAt, participantResults)
+
+const ImportDataSchema = z.object({
+ version: z.string(),
+ exportedAt: z.string(),
+ sports: z.array(
+ z.object({
+ name: z.string(),
+ type: z.string(),
+ slug: z.string(),
+ description: z.string().nullable(),
+ })
+ ),
+ sportsSeasons: z.array(
+ z.object({
+ sportSlug: z.string(),
+ name: z.string(),
+ year: z.number(),
+ startDate: z.string().nullable(),
+ endDate: z.string().nullable(),
+ status: z.string(),
+ scoringType: z.string(),
+ })
+ ),
+ participants: z.array(
+ z.object({
+ sportSlug: z.string(),
+ seasonName: z.string(),
+ seasonYear: z.number(),
+ name: z.string(),
+ shortName: z.string().nullable(),
+ externalId: z.string().nullable(),
+ expectedValue: z.union([z.number(), z.string()]),
+ })
+ ),
+ // Optional for backward compatibility with v1.0 exports
+ participantExpectedValues: z
+ .array(
+ z.object({
+ sportSlug: z.string(),
+ seasonName: z.string(),
+ seasonYear: z.number(),
+ participantName: z.string(),
+ probFirst: z.string(),
+ probSecond: z.string(),
+ probThird: z.string(),
+ probFourth: z.string(),
+ probFifth: z.string(),
+ probSixth: z.string(),
+ probSeventh: z.string(),
+ probEighth: z.string(),
+ expectedValue: z.string(),
+ source: z.string().nullable(),
+ sourceOdds: z.number().nullable(),
+ calculatedAt: z.string().optional(), // optional for backward compat
+ })
+ )
+ .optional(),
+ participantResults: z
+ .array(
+ z.object({
+ sportSlug: z.string(),
+ seasonName: z.string(),
+ seasonYear: z.number(),
+ participantName: z.string(),
+ finalPosition: z.number().nullable(),
+ qualifyingPoints: z.string().nullable(),
+ notes: z.string().nullable(),
+ })
+ )
+ .optional(),
+ seasonTemplates: z.array(
+ z.object({
+ name: z.string(),
+ year: z.number(),
+ description: z.string().nullable(),
+ isActive: z.boolean(),
+ sports: z.array(
+ z.object({
+ sportSlug: z.string(),
+ seasonName: z.string(),
+ seasonYear: z.number(),
+ })
+ ),
+ })
+ ),
+});
+
+type ExportData = z.infer;
+
+// ─── Export ───────────────────────────────────────────────────────────────────
export async function exportSportsDataToJSON(): Promise {
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,
+ const [
+ sports,
+ sportsSeasons,
+ participants,
+ participantEVs,
+ participantResultsData,
+ seasonTemplates,
+ ] = await Promise.all([
+ db.query.sports.findMany({
+ orderBy: (s, { asc }) => [asc(s.name)],
+ }),
+ db.query.sportsSeasons.findMany({
+ with: { sport: true },
+ orderBy: (s, { desc, asc }) => [desc(s.year), asc(s.name)],
+ }),
+ db.query.participants.findMany({
+ with: { sportsSeason: { with: { sport: true } } },
+ orderBy: (p, { asc }) => [asc(p.name)],
+ }),
+ db.query.participantExpectedValues.findMany({
+ with: {
+ participant: { with: { sportsSeason: { with: { sport: true } } } },
+ },
+ }),
+ db.query.participantResults.findMany({
+ with: {
+ participant: { with: { sportsSeason: { with: { sport: true } } } },
+ },
+ }),
+ db.query.seasonTemplates.findMany({
+ with: {
+ seasonTemplateSports: {
+ with: { sportsSeason: { with: { sport: true } } },
},
},
- },
- orderBy: (participants, { asc }) => [asc(participants.name)],
- });
+ orderBy: (t, { desc }) => [desc(t.year)],
+ }),
+ ]);
- // 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",
+ version: "1.1",
exportedAt: new Date().toISOString(),
- sports: sports.map((sport) => ({
- name: sport.name,
- type: sport.type,
- slug: sport.slug,
- description: sport.description,
+ sports: sports.map((s) => ({
+ name: s.name,
+ type: s.type,
+ slug: s.slug,
+ description: s.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,
+ sportsSeasons: sportsSeasons.map((s) => ({
+ sportSlug: s.sport.slug,
+ name: s.name,
+ year: s.year,
+ startDate: s.startDate,
+ endDate: s.endDate,
+ status: s.status,
+ scoringType: s.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,
+ participants: participants.map((p) => ({
+ sportSlug: p.sportsSeason.sport.slug,
+ seasonName: p.sportsSeason.name,
+ seasonYear: p.sportsSeason.year,
+ name: p.name,
+ shortName: p.shortName,
+ externalId: p.externalId,
+ expectedValue: p.expectedValue,
})),
- seasonTemplates: seasonTemplates.map((template) => ({
- name: template.name,
- year: template.year,
- description: template.description,
- isActive: template.isActive,
- sports: template.seasonTemplateSports.map((ts) => ({
+ 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,
+ calculatedAt: ev.calculatedAt.toISOString(),
+ })),
+ participantResults: participantResultsData.map((r) => ({
+ sportSlug: r.participant.sportsSeason.sport.slug,
+ seasonName: r.participant.sportsSeason.name,
+ seasonYear: r.participant.sportsSeason.year,
+ participantName: r.participant.name,
+ finalPosition: r.finalPosition,
+ qualifyingPoints: r.qualifyingPoints,
+ notes: r.notes,
+ })),
+ seasonTemplates: seasonTemplates.map((t) => ({
+ name: t.name,
+ year: t.year,
+ description: t.description,
+ isActive: t.isActive,
+ sports: t.seasonTemplateSports.map((ts) => ({
sportSlug: ts.sportsSeason.sport.slug,
seasonName: ts.sportsSeason.name,
seasonYear: ts.sportsSeason.year,
@@ -131,204 +207,372 @@ export async function exportSportsDataToJSON(): Promise {
};
}
+// ─── Import ───────────────────────────────────────────────────────────────────
+
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);
+ // Validate before touching the database — gives a clear error on bad files
+ let importData: ExportData;
+ try {
+ importData = ImportDataSchema.parse(JSON.parse(jsonData));
+ } catch (err) {
+ throw new Error(
+ `Invalid import file: ${err instanceof Error ? err.message : "unknown validation error"}`
+ );
}
- // Track created IDs for relationships
- const sportIdMap = new Map();
- const sportsSeasonIdMap = new Map();
+ // Wrap everything in a transaction so a mid-import failure rolls back cleanly
+ return await db.transaction(async (tx) => {
+ let created = 0;
+ let updated = 0;
+ let skipped = 0;
- // Import sports
- for (const sport of importData.sports) {
- const existing = await db.query.sports.findFirst({
- where: eq(schema.sports.slug, sport.slug),
- });
+ if (mode === "replace") {
+ // Delete in reverse dependency order.
+ // participantExpectedValues and participantResults both carry
+ // onDelete: "cascade" on their participantId FK, so they are
+ // automatically removed when participants is deleted.
+ await tx.delete(schema.seasonTemplateSports);
+ await tx.delete(schema.seasonTemplates);
+ await tx.delete(schema.participants); // cascades to EVs + results
+ await tx.delete(schema.seasonSports);
+ await tx.delete(schema.sportsSeasons);
+ await tx.delete(schema.sports);
+ }
- if (existing) {
- if (mode === "merge") {
- await db
- .update(schema.sports)
- .set({
+ // ID maps used to resolve relationships without further queries
+ const sportIdMap = new Map();
+ const sportsSeasonIdMap = new Map();
+ // key: `${sportsSeasonId}:${participantName}` — built during participant
+ // import so EV/result sections avoid a per-record lookup (fixes N+1)
+ const participantIdMap = new Map();
+
+ // ── Sports ──────────────────────────────────────────────────────────────
+
+ for (const sport of importData.sports) {
+ const existing = await tx.query.sports.findFirst({
+ where: eq(schema.sports.slug, sport.slug),
+ });
+
+ if (existing) {
+ if (mode === "merge") {
+ await tx
+ .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 [created_] = await tx
+ .insert(schema.sports)
+ .values({
name: sport.name,
type: sport.type as "team" | "individual",
+ slug: sport.slug,
description: sport.description,
})
- .where(eq(schema.sports.slug, sport.slug));
- sportIdMap.set(sport.slug, existing.id);
- updated++;
+ .returning();
+ sportIdMap.set(sport.slug, created_.id);
+ created++;
}
- } 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)
- ),
- });
+ // ── Sports seasons ───────────────────────────────────────────────────────
- const seasonKey = `${season.sportSlug}:${season.name}:${season.year}`;
+ for (const season of importData.sportsSeasons) {
+ const sportId = sportIdMap.get(season.sportSlug);
+ if (!sportId) {
+ skipped++;
+ continue;
+ }
- if (existing) {
- if (mode === "merge") {
- await db
- .update(schema.sportsSeasons)
- .set({
+ const seasonKey = `${season.sportSlug}:${season.name}:${season.year}`;
+
+ const existing = await tx.query.sportsSeasons.findFirst({
+ where: and(
+ eq(schema.sportsSeasons.sportId, sportId),
+ eq(schema.sportsSeasons.name, season.name),
+ eq(schema.sportsSeasons.year, season.year)
+ ),
+ });
+
+ if (existing) {
+ if (mode === "merge") {
+ await tx
+ .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 [created_] = await tx
+ .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",
+ scoringType: season.scoringType as
+ | "playoffs"
+ | "regular_season"
+ | "majors",
})
- .where(eq(schema.sportsSeasons.id, existing.id));
- sportsSeasonIdMap.set(seasonKey, existing.id);
- updated++;
+ .returning();
+ sportsSeasonIdMap.set(seasonKey, created_.id);
+ created++;
}
- } 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)
- ),
- });
+ // ── Participants ─────────────────────────────────────────────────────────
- if (!existing) {
- await db.insert(schema.participants).values({
- sportsSeasonId,
- name: participant.name,
- shortName: participant.shortName,
- externalId: participant.externalId,
- expectedValue: String(participant.expectedValue ?? 0),
+ 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 tx.query.participants.findFirst({
+ where: and(
+ eq(schema.participants.sportsSeasonId, sportsSeasonId),
+ eq(schema.participants.name, participant.name)
+ ),
});
- created++;
- } else {
- skipped++;
- }
- }
- // 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)
+ if (existing) {
+ // Update all mutable fields (fixes: merge mode was silently skipping)
+ await tx
+ .update(schema.participants)
.set({
+ shortName: participant.shortName,
+ externalId: participant.externalId,
+ expectedValue: String(participant.expectedValue ?? 0),
+ })
+ .where(eq(schema.participants.id, existing.id));
+ participantIdMap.set(`${sportsSeasonId}:${participant.name}`, existing.id);
+ updated++;
+ } else {
+ const [created_] = await tx
+ .insert(schema.participants)
+ .values({
+ sportsSeasonId,
+ name: participant.name,
+ shortName: participant.shortName,
+ externalId: participant.externalId,
+ expectedValue: String(participant.expectedValue ?? 0),
+ })
+ .returning();
+ participantIdMap.set(`${sportsSeasonId}:${participant.name}`, created_.id);
+ created++;
+ }
+ }
+
+ // ── Participant expected values ──────────────────────────────────────────
+
+ 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;
+ }
+
+ // Use map built during participant import — no extra query needed
+ const participantId = participantIdMap.get(
+ `${sportsSeasonId}:${ev.participantName}`
+ );
+ if (!participantId) {
+ skipped++;
+ continue;
+ }
+
+ const existingEV = await tx.query.participantExpectedValues.findFirst({
+ where: and(
+ eq(schema.participantExpectedValues.participantId, participantId),
+ 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,
+ // Restore original timestamp when present (fixes: calculatedAt lost)
+ ...(ev.calculatedAt
+ ? { calculatedAt: new Date(ev.calculatedAt) }
+ : {}),
+ };
+
+ if (existingEV) {
+ if (mode === "merge") {
+ await tx
+ .update(schema.participantExpectedValues)
+ .set({ ...evValues, updatedAt: new Date() })
+ .where(eq(schema.participantExpectedValues.id, existingEV.id));
+ await tx
+ .update(schema.participants)
+ .set({ expectedValue: ev.expectedValue })
+ .where(eq(schema.participants.id, participantId));
+ updated++;
+ }
+ } else {
+ await tx.insert(schema.participantExpectedValues).values({
+ participantId,
+ sportsSeasonId,
+ ...evValues,
+ });
+ await tx
+ .update(schema.participants)
+ .set({ expectedValue: ev.expectedValue })
+ .where(eq(schema.participants.id, participantId));
+ created++;
+ }
+ }
+ }
+
+ // ── Participant results ──────────────────────────────────────────────────
+
+ if (importData.participantResults) {
+ for (const result of importData.participantResults) {
+ const seasonKey = `${result.sportSlug}:${result.seasonName}:${result.seasonYear}`;
+ const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
+ if (!sportsSeasonId) {
+ skipped++;
+ continue;
+ }
+
+ const participantId = participantIdMap.get(
+ `${sportsSeasonId}:${result.participantName}`
+ );
+ if (!participantId) {
+ skipped++;
+ continue;
+ }
+
+ const existingResult = await tx.query.participantResults.findFirst({
+ where: and(
+ eq(schema.participantResults.participantId, participantId),
+ eq(schema.participantResults.sportsSeasonId, sportsSeasonId)
+ ),
+ });
+
+ const resultValues = {
+ finalPosition: result.finalPosition,
+ qualifyingPoints: result.qualifyingPoints,
+ notes: result.notes,
+ };
+
+ if (existingResult) {
+ if (mode === "merge") {
+ await tx
+ .update(schema.participantResults)
+ .set({ ...resultValues, updatedAt: new Date() })
+ .where(eq(schema.participantResults.id, existingResult.id));
+ updated++;
+ }
+ } else {
+ await tx.insert(schema.participantResults).values({
+ participantId,
+ sportsSeasonId,
+ ...resultValues,
+ });
+ created++;
+ }
+ }
+ }
+
+ // ── Season templates ─────────────────────────────────────────────────────
+
+ for (const template of importData.seasonTemplates) {
+ const existing = await tx.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 tx
+ .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 cleanly
+ await tx
+ .delete(schema.seasonTemplateSports)
+ .where(eq(schema.seasonTemplateSports.templateId, templateId));
+ } else {
+ continue;
+ }
+ } else {
+ const [created_] = await tx
+ .insert(schema.seasonTemplates)
+ .values({
+ name: template.name,
+ year: template.year,
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;
+ .returning();
+ templateId = created_.id;
+ created++;
}
- } 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,
- });
+ for (const templateSport of template.sports) {
+ const seasonKey = `${templateSport.sportSlug}:${templateSport.seasonName}:${templateSport.seasonYear}`;
+ const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
+ if (sportsSeasonId) {
+ await tx.insert(schema.seasonTemplateSports).values({
+ templateId,
+ sportsSeasonId,
+ });
+ }
}
}
- }
- return { created, updated, skipped };
+ return { created, updated, skipped };
+ });
}
diff --git a/package-lock.json b/package-lock.json
index 8c07495..98d89ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,7 +48,8 @@
"socket.io-client": "^4.8.1",
"sonner": "^2.0.7",
"svix": "^1.77.0",
- "tailwind-merge": "^3.3.1"
+ "tailwind-merge": "^3.3.1",
+ "zod": "^4.3.6"
},
"devDependencies": {
"@react-router/dev": "^7.7.1",
@@ -2457,6 +2458,16 @@
"node": ">=18"
}
},
+ "node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@mswjs/interceptors": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
@@ -12144,6 +12155,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/shadcn/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -14090,23 +14111,22 @@
}
},
"node_modules/zod": {
- "version": "3.25.76",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
- "dev": true,
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
- "version": "3.24.6",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
- "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
- "zod": "^3.24.1"
+ "zod": "^3.25 || ^4"
}
}
}
diff --git a/package.json b/package.json
index d1306dd..dc28c0c 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,8 @@
"socket.io-client": "^4.8.1",
"sonner": "^2.0.7",
"svix": "^1.77.0",
- "tailwind-merge": "^3.3.1"
+ "tailwind-merge": "^3.3.1",
+ "zod": "^4.3.6"
},
"devDependencies": {
"@react-router/dev": "^7.7.1",