feat(*): Change how data is set up

This commit is contained in:
Josh Creek
2026-01-16 21:11:21 +00:00
parent 3f0003b006
commit ee73c01afe
22 changed files with 8253 additions and 1159 deletions
+3 -3
View File
@@ -2,8 +2,7 @@
export interface CatchRecord {
_id: string; // Maps to Supabase 'id' field for frontend compatibility
userId: string;
pokedexEntryId: string; // String representation of the foreign key
pokedexId: string; // NEW: Foreign key to pokedexes table
pokemonId: string;
haveToEvolve: boolean;
caught: boolean;
inHome: boolean;
@@ -15,7 +14,8 @@ export interface CatchRecord {
export interface CatchRecordDB {
id: string;
userId: string;
pokedexEntryId: number; // Numeric foreign key in database
/** Numeric foreign key in database (references `pokemon.id`) */
pokemonId: number;
pokedexId: string; // NEW: Foreign key to pokedexes table
haveToEvolve: boolean;
caught: boolean;
+16 -21
View File
@@ -15,8 +15,7 @@ class CatchRecordRepository {
return {
_id: record.id,
userId: record.userId,
pokedexEntryId: record.pokedexEntryId.toString(),
pokedexId: record.pokedexId,
pokemonId: record.pokemonId.toString(),
haveToEvolve: record.haveToEvolve,
caught: record.caught,
inHome: record.inHome,
@@ -28,12 +27,12 @@ class CatchRecordRepository {
// Transform frontend data to database format (minimal transformation)
private transformToDatabase(data: Partial<CatchRecord>): Partial<CatchRecordDB> {
const dbData: Partial<CatchRecordDB> = {};
if (data.pokedexEntryId !== undefined && data.pokedexEntryId !== null) {
const numericId = Number(data.pokedexEntryId);
if (data.pokemonId !== undefined && data.pokemonId !== null) {
const numericId = Number(data.pokemonId);
if (isNaN(numericId)) {
throw new Error(`Invalid pokedexEntryId: ${data.pokedexEntryId}`);
throw new Error(`Invalid pokemonId: ${data.pokemonId}`);
}
dbData.pokedexEntryId = numericId;
dbData.pokemonId = numericId;
}
if (data.pokedexId !== undefined) dbData.pokedexId = data.pokedexId;
if (data.haveToEvolve !== undefined) dbData.haveToEvolve = data.haveToEvolve;
@@ -53,14 +52,14 @@ class CatchRecordRepository {
* catch the error and fall back to per-record upserts.
*/
async bulkUpsert(
records: Array<PartialExcept<CatchRecord, 'pokedexEntryId'>>
records: Array<PartialExcept<CatchRecord, 'pokemonId'>>
): Promise<CatchRecord[]> {
if (records.length === 0) return [];
const dbRows: Partial<CatchRecordDB>[] = records.map((r, index) => {
if (r.pokedexEntryId === undefined || r.pokedexEntryId === null || r.pokedexEntryId === '') {
if (r.pokemonId === undefined || r.pokemonId === null || r.pokemonId === '') {
throw new Error(
`CatchRecordRepository.bulkUpsert: record at index ${index} is missing required field "pokedexEntryId"`
`CatchRecordRepository.bulkUpsert: record at index ${index} is missing required field "pokemonId"`
);
}
@@ -81,9 +80,9 @@ class CatchRecordRepository {
mapped.userId = this.userId;
mapped.pokedexId = this.pokedexId;
if (mapped.pokedexEntryId === undefined || mapped.pokedexEntryId === null) {
if (mapped.pokemonId === undefined || mapped.pokemonId === null) {
throw new Error(
`CatchRecordRepository.bulkUpsert: record at index ${index} produced no pokedexEntryId after transform`
`CatchRecordRepository.bulkUpsert: record at index ${index} produced no pokemonId after transform`
);
}
@@ -93,7 +92,7 @@ class CatchRecordRepository {
const { data: result, error } = await this.supabase
.from('catch_records')
.upsert(dbRows, {
onConflict: '"userId","pokedexId","pokedexEntryId"'
onConflict: '"userId","pokedexId","pokemonId"'
})
.select();
@@ -198,13 +197,9 @@ class CatchRecordRepository {
async upsert(data: Partial<CatchRecord>): Promise<CatchRecord | null> {
if (data._id) {
return this.update(data._id, data);
} else if (data.pokedexEntryId) {
} else if (data.pokemonId) {
// Try to find existing record for this user, pokemon, and pokedex
const existing = await this.findByUserAndPokemon(
this.userId,
data.pokedexEntryId,
this.pokedexId
);
const existing = await this.findByUserAndPokemon(this.userId, data.pokemonId, this.pokedexId);
if (existing) {
return this.update(existing._id, data);
} else {
@@ -216,10 +211,10 @@ class CatchRecordRepository {
async findByUserAndPokemon(
userId: string,
pokedexEntryId: string,
pokemonId: string,
pokedexId: string
): Promise<CatchRecord | null> {
const numericId = Number(pokedexEntryId);
const numericId = Number(pokemonId);
if (isNaN(numericId)) {
return null;
}
@@ -227,7 +222,7 @@ class CatchRecordRepository {
.from('catch_records')
.select('*')
.eq('userId', userId)
.eq('pokedexEntryId', numericId)
.eq('pokemonId', numericId)
.eq('pokedexId', pokedexId)
.single();
@@ -31,7 +31,8 @@ class CombinedDataRepository {
return {
_id: record.id,
userId: record.userId,
pokedexEntryId: record.pokedexEntryId.toString(),
// Backwards-compatible field name; DB column is pokemonId
pokedexEntryId: record.pokemonId.toString(),
pokedexId: record.pokedexId,
haveToEvolve: record.haveToEvolve,
caught: record.caught,
@@ -87,7 +88,7 @@ class CombinedDataRepository {
.select('*')
.eq('userId', userId)
.eq('pokedexId', this.pokedexId)
.in('pokedexEntryId', entryIds);
.in('pokemonId', entryIds);
if (!recordsError && records) {
catchRecords = records;
@@ -96,8 +97,7 @@ class CombinedDataRepository {
// Combine the data exactly like master branch
const combinedData = entries.map((entry) => {
const userCatchRecord =
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
const userCatchRecord = catchRecords.find((record) => record.pokemonId === entry.id) || null;
const transformedEntry = this.transformPokedexEntry(entry);
const transformedCatchRecord = userCatchRecord
@@ -165,7 +165,7 @@ class CombinedDataRepository {
.select('*')
.eq('userId', userId)
.eq('pokedexId', this.pokedexId)
.in('pokedexEntryId', entryIds);
.in('pokemonId', entryIds);
if (!recordsError && records) {
catchRecords = records;
@@ -174,8 +174,7 @@ class CombinedDataRepository {
// Combine the data exactly like master branch
const combinedData = entries.map((entry) => {
const userCatchRecord =
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
const userCatchRecord = catchRecords.find((record) => record.pokemonId === entry.id) || null;
const transformedEntry = this.transformPokedexEntry(entry);
const transformedCatchRecord = userCatchRecord
+9 -7
View File
@@ -20,11 +20,12 @@ export async function calculateExpectedEntries(
// Apply region filter: if gameScope is specified, filter by region
if (pokedex.gameScope) {
// Determine region from gameScope using region_game_mappings
// Determine region from gameScope using games table.
// We keep games.region as a denormalized convenience column.
const { data: regionData, error: regionError } = await supabase
.from('region_game_mappings')
.from('games')
.select('region')
.eq('game', pokedex.gameScope)
.eq('displayName', pokedex.gameScope)
.maybeSingle();
if (regionError) {
@@ -71,17 +72,18 @@ export async function populatePokedexMappings(
return;
}
const mappings = expectedEntryIds.map((pokedexEntryId) => ({
// New schema writes to pokedex_pokemon_mapping; column is pokemonId
const mappings = expectedEntryIds.map((pokemonId) => ({
pokedexId,
pokedexEntryId
pokemonId
}));
// Process in chunks to avoid request size limits
const CHUNK_SIZE = 500;
for (let i = 0; i < mappings.length; i += CHUNK_SIZE) {
const chunk = mappings.slice(i, i + CHUNK_SIZE);
const { error } = await supabase.from('pokedex_entries_mapping').upsert(chunk, {
onConflict: 'pokedexId,pokedexEntryId',
const { error } = await supabase.from('pokedex_pokemon_mapping').upsert(chunk, {
onConflict: 'pokedexId,pokemonId',
ignoreDuplicates: true
});
+1 -1
View File
@@ -53,7 +53,7 @@ export type FlushOptions = {
};
function keyFor(record: CatchRecord): string {
return `${record.userId}:${record.pokedexId}:${record.pokedexEntryId}`;
return `${record.userId}:${record.pokedexId}:${record.pokemonId}`;
}
function backoffMs(attempts: number): number {