- 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
309 lines
8.9 KiB
TypeScript
309 lines
8.9 KiB
TypeScript
import {
|
|
boolean,
|
|
check,
|
|
date,
|
|
numeric,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
text,
|
|
unique
|
|
} from 'drizzle-orm/pg-core';
|
|
import { gamesSchema, refSchema } from './schemas';
|
|
import { sql } from 'drizzle-orm';
|
|
import { user } from './auth';
|
|
import { games, pokemon } from './catch-em-all';
|
|
import { trackerGames, trackerItems } from './game-tracker';
|
|
import { id, timestamps } from './helpers';
|
|
|
|
export const pokemonResearch = refSchema.table('pokemon_research', {
|
|
pokemonId: integer('pokemon_id')
|
|
.primaryKey()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
facts: jsonb('facts')
|
|
.$type<{
|
|
height: number;
|
|
weight: number;
|
|
baseExperience: number | null;
|
|
captureRate: number;
|
|
baseHappiness: number | null;
|
|
growthRate: string;
|
|
genderRate: number;
|
|
hatchCounter: number | null;
|
|
eggGroups: string[];
|
|
abilities: { name: string; hidden: boolean }[];
|
|
evYield: number[];
|
|
}>()
|
|
.notNull()
|
|
});
|
|
|
|
export const pokemonDexNumbers = refSchema.table(
|
|
'pokemon_dex_numbers',
|
|
{
|
|
id: id(),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
dex: text('dex').notNull(),
|
|
dexName: text('dex_name').notNull(),
|
|
number: integer('number').notNull()
|
|
},
|
|
(t) => [unique().on(t.pokemonId, t.gameId, t.dex), index().on(t.gameId)]
|
|
);
|
|
|
|
export const pokemonMoves = refSchema.table('pokemon_moves', {
|
|
id: integer('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
type: text('type').notNull(),
|
|
category: text('category').notNull(),
|
|
power: integer('power'),
|
|
accuracy: integer('accuracy'),
|
|
pp: integer('pp'),
|
|
priority: integer('priority').notNull(),
|
|
target: text('target').notNull()
|
|
});
|
|
|
|
export const pokemonLearnsets = refSchema.table(
|
|
'pokemon_learnsets',
|
|
{
|
|
id: id(),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
versionGroup: integer('version_group').notNull(),
|
|
moves: jsonb('moves').$type<[moveId: number, method: string, level: number][]>().notNull()
|
|
},
|
|
(t) => [unique().on(t.pokemonId, t.versionGroup)]
|
|
);
|
|
|
|
export const pokemonGameResearch = refSchema.table('pokemon_game_research', {
|
|
gameId: integer('game_id')
|
|
.primaryKey()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
versionGroup: integer('version_group').notNull(),
|
|
regions: text('regions').array().notNull()
|
|
});
|
|
|
|
export const pokemonJourneys = gamesSchema.table(
|
|
'pokemon_journeys',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
stage: text('stage', { enum: ['story', 'postgame', 'collection'] })
|
|
.notNull()
|
|
.default('story'),
|
|
location: text('location'),
|
|
completed: boolean('completed').notNull().default(false),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
unique().on(t.userId, t.gameId),
|
|
check('pokemon_journeys_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
|
|
]
|
|
);
|
|
|
|
export const pokemonSeen = gamesSchema.table(
|
|
'pokemon_seen',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
...timestamps
|
|
},
|
|
(t) => [unique().on(t.userId, t.gameId, t.pokemonId), index().on(t.userId)]
|
|
);
|
|
|
|
export const pokemonCatches = gamesSchema.table(
|
|
'pokemon_catches',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
pokemonId: integer('pokemon_id')
|
|
.notNull()
|
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
|
nickname: text('nickname').notNull().default(''),
|
|
level: integer('level').notNull().default(1),
|
|
moves: text('moves').array().notNull().default([]),
|
|
partySlot: integer('party_slot'),
|
|
shiny: boolean('shiny').notNull().default(false),
|
|
nature: text('nature'),
|
|
ability: text('ability'),
|
|
ball: text('ball'),
|
|
location: text('location'),
|
|
obtained: text('obtained').notNull().default('caught'),
|
|
stage: text('stage', { enum: ['story', 'postgame', 'collection'] })
|
|
.notNull()
|
|
.default('story'),
|
|
notes: text('notes').notNull().default(''),
|
|
caughtOn: date('caught_on'),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
index().on(t.userId, t.gameId, t.pokemonId),
|
|
unique().on(t.userId, t.gameId, t.partySlot),
|
|
check('pokemon_catches_level_check', sql`${t.level} between 1 and 100`),
|
|
check('pokemon_catches_party_slot_check', sql`${t.partySlot} between 1 and 6`),
|
|
check('pokemon_catches_moves_check', sql`cardinality(${t.moves}) <= 4`),
|
|
check('pokemon_catches_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
|
|
]
|
|
);
|
|
|
|
export const pokemonMilestones = gamesSchema.table(
|
|
'pokemon_milestones',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => games.id, { onDelete: 'cascade' }),
|
|
key: text('key').notNull(),
|
|
title: text('title').notNull(),
|
|
category: text('category').notNull(),
|
|
stage: text('stage').notNull().default('story'),
|
|
done: boolean('done').notNull().default(false),
|
|
notes: text('notes').notNull().default(''),
|
|
...timestamps
|
|
},
|
|
(t) => [unique().on(t.userId, t.gameId, t.key)]
|
|
);
|
|
|
|
export const trackerRanks = gamesSchema.table(
|
|
'tracker_ranks',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => trackerGames.id, { onDelete: 'cascade' }),
|
|
level: integer('level').notNull().default(1),
|
|
prestige: integer('prestige').notNull().default(0),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
unique().on(t.userId, t.gameId),
|
|
check('tracker_ranks_level_check', sql`${t.level} between 1 and 10000`),
|
|
check('tracker_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
|
|
]
|
|
);
|
|
|
|
export const trackerWeaponRanks = gamesSchema.table(
|
|
'tracker_weapon_ranks',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
itemId: integer('item_id')
|
|
.notNull()
|
|
.references(() => trackerItems.id, { onDelete: 'cascade' }),
|
|
level: integer('level').notNull().default(1),
|
|
prestige: integer('prestige').notNull().default(0),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
unique().on(t.userId, t.itemId),
|
|
check('tracker_weapon_ranks_level_check', sql`${t.level} between 1 and 10000`),
|
|
check('tracker_weapon_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
|
|
]
|
|
);
|
|
|
|
export const trackerChallenges = refSchema.table(
|
|
'tracker_challenges',
|
|
{
|
|
id: id(),
|
|
gameId: integer('game_id')
|
|
.notNull()
|
|
.references(() => trackerGames.id, { onDelete: 'cascade' }),
|
|
key: text('key').notNull(),
|
|
name: text('name').notNull(),
|
|
category: text('category').notNull(),
|
|
requirement: text('requirement').notNull(),
|
|
reward: text('reward'),
|
|
icon: text('icon'),
|
|
target: integer('target'),
|
|
sourceUrl: text('source_url').notNull()
|
|
},
|
|
(t) => [unique().on(t.gameId, t.key)]
|
|
);
|
|
|
|
export const trackerChallengeProgress = gamesSchema.table(
|
|
'tracker_challenge_progress',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
challengeId: integer('challenge_id')
|
|
.notNull()
|
|
.references(() => trackerChallenges.id, { onDelete: 'cascade' }),
|
|
current: integer('current').notNull().default(0),
|
|
done: boolean('done').notNull().default(false),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
unique().on(t.userId, t.challengeId),
|
|
check('tracker_challenge_progress_current_check', sql`${t.current} >= 0`)
|
|
]
|
|
);
|
|
|
|
export const osuEntries = gamesSchema.table(
|
|
'osu_entries',
|
|
{
|
|
id: id(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
kind: text('kind', { enum: ['score', 'goal', 'training'] }).notNull(),
|
|
title: text('title').notNull(),
|
|
mode: text('mode').notNull().default('osu'),
|
|
beatmapUrl: text('beatmap_url'),
|
|
scoreUrl: text('score_url'),
|
|
goal: text('goal').notNull().default('pass'),
|
|
skill: text('skill').notNull().default('stamina'),
|
|
bpm: integer('bpm'),
|
|
duration: integer('duration'),
|
|
accuracy: numeric('accuracy', { mode: 'number' }),
|
|
combo: integer('combo'),
|
|
misses: integer('misses'),
|
|
pp: numeric('pp', { mode: 'number' }),
|
|
mods: text('mods').notNull().default('NM'),
|
|
rate: numeric('rate', { precision: 4, scale: 2, mode: 'number' })
|
|
.notNull()
|
|
.default(sql`1.00`),
|
|
achieved: boolean('achieved').notNull().default(false),
|
|
playedOn: date('played_on').notNull(),
|
|
notes: text('notes').notNull().default(''),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
index().on(t.userId, t.kind),
|
|
check('osu_entries_kind_check', sql`${t.kind} IN ('score','goal','training')`)
|
|
]
|
|
);
|
|
|
|
export type PokemonCatch = typeof pokemonCatches.$inferSelect;
|