Files
personal-website/scripts/seed-catalog.ts
Stan Runge d0c7b6548b
Some checks failed
Test and deploy staging / staging (push) Has been cancelled
beeeeg update
2026-09-23 04:16:50 +00:00

85 lines
3.1 KiB
TypeScript

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<number, number>();
const gameIds = new Map<number, number>();
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();
}