import { readFile } from 'node:fs/promises'; import { drizzle } from 'drizzle-orm/postgres-js'; import { and, eq, isNull, sql } from 'drizzle-orm'; import postgres from 'postgres'; import { pokemon, games, pokemonGameEntries } from '../src/lib/server/db/schema/catch-em-all'; type Catalog = { source: { revision: string }; pokemon: (typeof pokemon.$inferInsert)[]; games: { sourceId: number; name: string; generation: number }[]; entries: [number, number, 'catchable' | 'unknown'][]; }; if (!process.env.DB_URL) throw new Error('DB_URL is required'); const catalog: Catalog = JSON.parse( await readFile(new URL('../data/pokemon-catalog.json', import.meta.url), 'utf8') ); const client = postgres(process.env.DB_URL, { max: 1 }); const db = drizzle({ client }); try { await db.transaction(async (tx) => { const pokemonIds = new Map(); const gameIds = new Map(); for (const row of catalog.pokemon) { const [saved] = await tx .insert(pokemon) .values(row) .onConflictDoUpdate({ target: pokemon.number, set: row }) .returning(); pokemonIds.set(saved.number, saved.id); } for (const row of catalog.games) { // Adopt older rows by exact name to preserve existing progress and game IDs. const [imported] = await tx.select().from(games).where(eq(games.sourceId, row.sourceId)); if (!imported) { const legacy = await tx .select() .from(games) .where(and(eq(games.name, row.name), isNull(games.sourceId))); if (legacy.length > 1) throw new Error(`Ambiguous legacy game: ${row.name}`); if (legacy[0]) await tx.update(games).set({ sourceId: row.sourceId }).where(eq(games.id, legacy[0].id)); } const [saved] = await tx .insert(games) .values(row) .onConflictDoUpdate({ target: games.sourceId, set: row }) .returning(); gameIds.set(row.sourceId, saved.id); } for (let offset = 0; offset < catalog.entries.length; offset += 1000) { const rows = catalog.entries .slice(offset, offset + 1000) .map(([number, version, availability]) => { const pokemonId = pokemonIds.get(number), gameId = gameIds.get(version); if (!pokemonId || !gameId) throw new Error(`Unknown catalog reference ${number}/${version}`); return { pokemonId, gameId, availability, note: availability === 'catchable' ? 'PokéAPI: documented encounter; conditions, location, or a specific form may be required.' : 'PokéAPI: Pokédex or game-index record only; catch, evolution, gift, trade, transfer, and event availability is not confirmed.' }; }); await tx .insert(pokemonGameEntries) .values(rows) .onConflictDoUpdate({ target: [pokemonGameEntries.pokemonId, pokemonGameEntries.gameId], set: { availability: sql`excluded.availability`, note: sql`excluded.note` }, // Preserve hand-maintained availability and notes. setWhere: sql`${pokemonGameEntries.note} like 'PokéAPI:%'` }); } }); console.log( `Seeded ${catalog.pokemon.length} species, ${catalog.games.length} versions, ${catalog.entries.length} entries from ${catalog.source.revision}` ); } finally { await client.end(); }