- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta (lists excluded via schemaFilter; runtime list tables are managed by meta.*) - drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal) - db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy) - scripts/db-migrate.ts: migrator with --init to record the baseline and a guard against running the baseline on a non-empty, unrecorded database
183 lines
6.2 KiB
TypeScript
183 lines
6.2 KiB
TypeScript
import { boolean, index, integer, text, unique } from 'drizzle-orm/pg-core';
|
|
import { gamesSchema, refSchema } from './schemas';
|
|
import { availabilityValues } from '../../../availability';
|
|
import { user } from './auth';
|
|
import { id, timestamps } from './helpers';
|
|
|
|
export const availability = refSchema.enum('availability', availabilityValues);
|
|
|
|
// Where a catalog row came from. Seeding replaces `pokeapi` and `derived` rows
|
|
// but never touches `manual` ones, which are admin corrections.
|
|
export const catalogSources = ['pokeapi', 'derived', 'manual'] as const;
|
|
const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi');
|
|
|
|
export const pokemon = refSchema.table('pokemon', {
|
|
id: id(),
|
|
number: integer('number').notNull().unique(),
|
|
generation: integer('generation').notNull().default(1),
|
|
name: text('name').notNull(),
|
|
type1: text('type1').notNull(),
|
|
type2: text('type2'),
|
|
hp: integer('hp').notNull().default(0),
|
|
attack: integer('attack').notNull().default(0),
|
|
defense: integer('defense').notNull().default(0),
|
|
specialAttack: integer('special_attack').notNull().default(0),
|
|
specialDefense: integer('special_defense').notNull().default(0),
|
|
speed: integer('speed').notNull().default(0),
|
|
// national numbers, so the chain survives reseeding without id lookups
|
|
evolvesFrom: integer('evolves_from'),
|
|
evolutionChain: integer('evolution_chain'),
|
|
isBaby: boolean('is_baby').notNull().default(false),
|
|
isLegendary: boolean('is_legendary').notNull().default(false),
|
|
isMythical: boolean('is_mythical').notNull().default(false),
|
|
...timestamps
|
|
});
|
|
|
|
export const games = refSchema.table('pokemon_games', {
|
|
id: id(),
|
|
sourceId: integer('source_id').unique(),
|
|
slug: text('slug').unique(),
|
|
name: text('name').notNull(),
|
|
generation: integer('generation').notNull().default(1),
|
|
sortOrder: integer('sort_order').notNull().default(0),
|
|
breeding: boolean('breeding').notNull().default(true),
|
|
...timestamps
|
|
});
|
|
|
|
export const pokemonGameEntries = refSchema.table(
|
|
'pokemon_game_entries',
|
|
{
|
|
id: id(),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
availability: availability('availability').notNull().default('catchable'),
|
|
// how to obtain it, e.g. "Evolve Wartortle: Level 36+" or "Route 22, Viridian Forest"
|
|
note: text('note'),
|
|
source: source(),
|
|
...timestamps
|
|
},
|
|
(table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)]
|
|
);
|
|
|
|
export const pokemonLocations = refSchema.table('pokemon_locations', {
|
|
id: id(),
|
|
sourceId: integer('source_id').unique(),
|
|
slug: text('slug').notNull(),
|
|
name: text('name').notNull(),
|
|
region: text('region'),
|
|
description: text('description'),
|
|
// R2 object key of an admin-uploaded picture
|
|
imageKey: text('image_key'),
|
|
imageCredit: text('image_credit'),
|
|
source: source(),
|
|
...timestamps
|
|
});
|
|
|
|
export const pokemonEncounters = refSchema.table(
|
|
'pokemon_encounters',
|
|
{
|
|
id: id(),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
locationId: integer('location_id')
|
|
.notNull()
|
|
.references(() => pokemonLocations.id, { onDelete: 'cascade' }),
|
|
area: text('area'),
|
|
method: text('method').notNull(),
|
|
minLevel: integer('min_level'),
|
|
maxLevel: integer('max_level'),
|
|
// summed encounter-slot percentage; null for gifts and folded raids
|
|
rarity: integer('rarity'),
|
|
form: text('form'),
|
|
conditions: text('conditions').array().notNull().default([]),
|
|
note: text('note'),
|
|
source: source(),
|
|
...timestamps
|
|
},
|
|
(table) => [index().on(table.pokemonId, table.gameId), index().on(table.locationId)]
|
|
);
|
|
|
|
// The method that applies in each game, e.g. Magneton → Magnezone needs Mt.
|
|
// Coronet in Platinum but a Thunder Stone in Sword.
|
|
export const pokemonEvolutions = refSchema.table(
|
|
'pokemon_evolutions',
|
|
{
|
|
id: id(),
|
|
fromPokemonId: integer('from_pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
toPokemonId: integer('to_pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
method: text('method').notNull(),
|
|
source: source(),
|
|
...timestamps
|
|
},
|
|
(table) => [
|
|
unique().on(table.fromPokemonId, table.toPokemonId, table.gameId, table.method),
|
|
index().on(table.gameId)
|
|
]
|
|
);
|
|
|
|
export const userEntries = gamesSchema.table(
|
|
'catch_em_all_entries',
|
|
{
|
|
id: id(),
|
|
pokemonGameEntryId: integer('pokemon_game_entry_id')
|
|
.notNull()
|
|
.references(() => pokemonGameEntries.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
...timestamps
|
|
},
|
|
(table) => [unique().on(table.pokemonGameEntryId, table.userId), index().on(table.userId)]
|
|
);
|
|
|
|
// which games a user has chosen to track ("rules")
|
|
export const userGames = gamesSchema.table(
|
|
'catch_em_all_games',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
...timestamps
|
|
},
|
|
(table) => [unique().on(table.userId, table.gameId), index().on(table.userId)]
|
|
);
|
|
|
|
// A row distinguishes an explicit empty game selection from a new account.
|
|
export const catchEmAllSettings = gamesSchema.table('catch_em_all_settings', {
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.unique()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
...timestamps
|
|
});
|
|
|
|
export type Pokemon = typeof pokemon.$inferSelect;
|
|
export type Game = typeof games.$inferSelect;
|
|
export type PokemonGameEntry = typeof pokemonGameEntries.$inferSelect;
|
|
export type UserEntry = typeof userEntries.$inferSelect;
|
|
export type UserGame = typeof userGames.$inferSelect;
|
|
export type Availability = (typeof availability.enumValues)[number];
|
|
export type PokemonLocation = typeof pokemonLocations.$inferSelect;
|
|
export type PokemonEncounter = typeof pokemonEncounters.$inferSelect;
|
|
export type PokemonEvolution = typeof pokemonEvolutions.$inferSelect;
|