import { readFileSync } from "node:fs"; import path from "node:path"; import { z } from "zod"; import { AchievementSeedSchema, ChallengeSeedSchema, GroupSeedSchema, MatchSeedSchema, MemorySeedSchema, PredictionSeedSchema, QuizSeedSchema, StadiumSeedSchema, StickerSeedSchema, TeamSeedSchema, TournamentSeedSchema, type EntityRef, type Requirement } from "./schema.js"; function seedPath(fileName: string): string { return path.join(process.cwd(), "data", "seeds", fileName); } function readJsonFile(fileName: string): unknown { const fullPath = seedPath(fileName); try { return JSON.parse(readFileSync(fullPath, "utf8")); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Unable to read or parse ${fileName}: ${message}`); } } function formatZodError(fileName: string, error: z.ZodError): string { const details = error.issues .map((issue) => { const location = issue.path.length > 0 ? issue.path.join(".") : "(root)"; return ` - ${location}: ${issue.message}`; }) .join("\n"); return `${fileName} failed schema validation:\n${details}`; } function parseSeed(fileName: string, schema: z.ZodType): T { const raw = readJsonFile(fileName); const result = schema.safeParse(raw); if (!result.success) { throw new Error(formatZodError(fileName, result.error)); } return result.data; } function toIdSet(items: Array<{ id: string }>): Set { return new Set(items.map((item) => item.id)); } function requireUnique(label: string, values: string[], errors: string[]): void { const seen = new Map(); values.forEach((value, index) => { const firstIndex = seen.get(value); if (firstIndex !== undefined) { errors.push(`${label} contains duplicate ID "${value}" at indexes ${firstIndex} and ${index}.`); return; } seen.set(value, index); }); } function checkRequirementTarget( requirement: Requirement, owner: string, idSets: Record>, errors: string[] ): void { const target = requirement.target; if (!target?.entityId) { return; } const set = idSets[target.entityType]; if (!set) { errors.push(`${owner} requirement target has unsupported entityType "${target.entityType}".`); return; } if (!set.has(target.entityId)) { errors.push( `${owner} requirement target references missing ${target.entityType} "${target.entityId}".` ); } } function checkEntityRef( ref: EntityRef, owner: string, idSets: Record>, errors: string[] ): void { const set = idSets[ref.type]; if (!set) { errors.push(`${owner} references unsupported entity type "${ref.type}".`); return; } if (!set.has(ref.id)) { errors.push(`${owner} references missing ${ref.type} "${ref.id}".`); } } function main(): void { const errors: string[] = []; const tournament = parseSeed("tournament.json", TournamentSeedSchema); const stadiums = parseSeed("stadiums.json", StadiumSeedSchema); const teams = parseSeed("teams.json", TeamSeedSchema); const groups = parseSeed("groups.json", GroupSeedSchema); const matches = parseSeed("matches.json", MatchSeedSchema); const stickerSeed = parseSeed("stickers.json", StickerSeedSchema); const memories = parseSeed("memories.json", MemorySeedSchema); const quizzes = parseSeed("quizzes.json", QuizSeedSchema); const predictions = parseSeed("predictions.json", PredictionSeedSchema); const achievements = parseSeed("achievements.json", AchievementSeedSchema); const challenges = parseSeed("challenges.json", ChallengeSeedSchema); const stickerSets = stickerSeed.sets; const stickers = stickerSeed.stickers; requireUnique("stadiums.json", stadiums.map((item) => item.id), errors); requireUnique("teams.json", teams.map((item) => item.id), errors); requireUnique("groups.json", groups.map((item) => item.id), errors); requireUnique("matches.json IDs", matches.map((item) => item.id), errors); requireUnique("matches.json matchNumber", matches.map((item) => String(item.matchNumber)), errors); requireUnique("stickers.json sets", stickerSets.map((item) => item.id), errors); requireUnique("stickers.json stickers", stickers.map((item) => item.id), errors); requireUnique("memories.json", memories.map((item) => item.id), errors); requireUnique("quizzes.json", quizzes.map((item) => item.id), errors); requireUnique("predictions.json", predictions.map((item) => item.id), errors); requireUnique("achievements.json", achievements.map((item) => item.id), errors); requireUnique("challenges.json", challenges.map((item) => item.id), errors); const stadiumIds = toIdSet(stadiums); const teamIds = toIdSet(teams); const groupIds = toIdSet(groups); const matchIds = toIdSet(matches); const stickerSetIds = toIdSet(stickerSets); const stickerIds = toIdSet(stickers); const memoryIds = toIdSet(memories); const quizIds = toIdSet(quizzes); const predictionIds = toIdSet(predictions); const achievementIds = toIdSet(achievements); const challengeIds = toIdSet(challenges); const groupSlotIds = new Set(); for (const group of groups) { if (group.tournamentId !== tournament.id) { errors.push(`${group.id} references tournament "${group.tournamentId}" instead of "${tournament.id}".`); } requireUnique(`${group.id} slot IDs`, group.slots.map((slot) => slot.slotId), errors); requireUnique(`${group.id} draw labels`, group.slots.map((slot) => slot.drawLabel), errors); for (const slot of group.slots) { groupSlotIds.add(slot.slotId); if (slot.teamId !== null && !teamIds.has(slot.teamId)) { errors.push(`${group.id}/${slot.slotId} references missing team "${slot.teamId}".`); } } } const idSets: Record> = { tournament: new Set([tournament.id]), stadium: stadiumIds, team: teamIds, group: groupIds, match: matchIds, "sticker-set": stickerSetIds, sticker: stickerIds, memory: memoryIds, quiz: quizIds, prediction: predictionIds, achievement: achievementIds, challenge: challengeIds }; for (const match of matches) { if (match.tournamentId !== tournament.id) { errors.push(`${match.id} references tournament "${match.tournamentId}" instead of "${tournament.id}".`); } if (!stadiumIds.has(match.venueId)) { errors.push(`${match.id} references missing stadium "${match.venueId}".`); } if (match.stage === "group") { if (!match.groupId || !groupIds.has(match.groupId)) { errors.push(`${match.id} is a group-stage match with invalid groupId "${match.groupId ?? "null"}".`); } } for (const sideName of ["home", "away"] as const) { const side = match[sideName]; if (side.teamId && !teamIds.has(side.teamId)) { errors.push(`${match.id}.${sideName} references missing team "${side.teamId}".`); } if (side.slotId && !groupSlotIds.has(side.slotId)) { errors.push(`${match.id}.${sideName} references missing group slot "${side.slotId}".`); } if (side.advancesFrom) { const [kind, referencedMatchId] = side.advancesFrom.split(":"); if ((kind === "winner" || kind === "loser") && referencedMatchId && !matchIds.has(referencedMatchId)) { errors.push( `${match.id}.${sideName} advancesFrom references missing match "${referencedMatchId}".` ); } } } } for (const set of stickerSets) { for (const featuredStickerId of set.featuredStickerIds) { if (!stickerIds.has(featuredStickerId)) { errors.push(`${set.id} features missing sticker "${featuredStickerId}".`); } } } for (const sticker of stickers) { if (!stickerSetIds.has(sticker.setId)) { errors.push(`${sticker.id} references missing sticker set "${sticker.setId}".`); } if (sticker.entityRef) { checkEntityRef(sticker.entityRef, sticker.id, idSets, errors); } for (const requirement of sticker.dropRules) { checkRequirementTarget(requirement, `${sticker.id} drop rule`, idSets, errors); } } for (const memory of memories) { checkRequirementTarget(memory.unlockRule, `${memory.id} unlockRule`, idSets, errors); for (const ref of memory.relatedEntityRefs) { checkEntityRef(ref, memory.id, idSets, errors); } } for (const quiz of quizzes) { requireUnique(`${quiz.id} question IDs`, quiz.questions.map((question) => question.id), errors); for (const question of quiz.questions) { const optionIds = new Set(question.options.map((option) => option.id)); if (!optionIds.has(question.correctOptionId)) { errors.push( `${quiz.id}/${question.id} correctOptionId "${question.correctOptionId}" is not in the options list.` ); } } } for (const prediction of predictions) { for (const ref of prediction.entityRefs) { checkEntityRef(ref, prediction.id, idSets, errors); } requireUnique(`${prediction.id} option IDs`, prediction.options.map((option) => option.id), errors); for (const option of prediction.options) { if (option.entityRef) { checkEntityRef(option.entityRef, `${prediction.id}/${option.id}`, idSets, errors); } } for (const winningOptionId of prediction.resolution.winningOptionIds) { if (!prediction.options.some((option) => option.id === winningOptionId)) { errors.push(`${prediction.id} resolution references missing option "${winningOptionId}".`); } } } for (const achievement of achievements) { for (const requirement of achievement.triggerRequirements) { checkRequirementTarget(requirement, `${achievement.id} trigger`, idSets, errors); } } for (const challenge of challenges) { for (const achievementId of challenge.linkedAchievementIds) { if (!achievementIds.has(achievementId)) { errors.push(`${challenge.id} links missing achievement "${achievementId}".`); } } for (const requirement of challenge.requirements) { checkRequirementTarget(requirement, `${challenge.id} requirement`, idSets, errors); } } if (errors.length > 0) { console.error(`Seed validation failed with ${errors.length} error(s):`); for (const error of errors) { console.error(`- ${error}`); } process.exit(1); } console.log( [ "Seed validation passed:", ` tournament: ${tournament.id}`, ` stadiums: ${stadiums.length}`, ` teams: ${teams.length}`, ` groups: ${groups.length}`, ` matches: ${matches.length}`, ` sticker sets: ${stickerSets.length}`, ` stickers: ${stickers.length}`, ` memories: ${memories.length}`, ` quizzes: ${quizzes.length}`, ` predictions: ${predictions.length}`, ` achievements: ${achievements.length}`, ` challenges: ${challenges.length}` ].join("\n") ); } main();