diff --git a/src/lib/components/SignUp.svelte b/src/lib/components/SignUp.svelte index e28241f..df40179 100644 --- a/src/lib/components/SignUp.svelte +++ b/src/lib/components/SignUp.svelte @@ -1,4 +1,7 @@ @@ -54,6 +62,12 @@ />
- +
+ + diff --git a/src/lib/services/PokedexMappingService.ts b/src/lib/services/PokedexMappingService.ts new file mode 100644 index 0000000..80642a7 --- /dev/null +++ b/src/lib/services/PokedexMappingService.ts @@ -0,0 +1,122 @@ +import type { Pokedex } from '$lib/models/Pokedex'; +import type { SupabaseClient } from '@supabase/supabase-js'; + +/** + * Calculate expected pokedex entries based on pokedex configuration + * Applies the same filtering logic as CombinedDataRepository + * @throws Error if the Supabase query fails + */ +export async function calculateExpectedEntries( + supabase: SupabaseClient, + pokedex: Pokedex +): Promise { + let query = supabase.from('pokedex_entries').select('id'); + + // Apply form filter: if isFormDex is false, only include base forms + // Base forms have form IS NULL, except Unown which has no base form (use 'A') + if (!pokedex.isFormDex) { + query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)'); + } + + // Apply region filter: if gameScope is specified, filter by region + if (pokedex.gameScope) { + // Determine region from gameScope using region_game_mappings + const { data: regionData, error: regionError } = await supabase + .from('region_game_mappings') + .select('region') + .eq('game', pokedex.gameScope) + .maybeSingle(); + + if (regionError) { + console.error('Error looking up region for gameScope:', pokedex.gameScope, regionError); + throw new Error( + `Failed to lookup region for gameScope "${pokedex.gameScope}": ${regionError.message}` + ); + } + + if (regionData?.region) { + query = query.eq('regionToCatchIn', regionData.region); + } + } + + // Apply game filter: if gameScope is specified, filter by gamesToCatchIn array + if (pokedex.gameScope) { + query = query.contains('gamesToCatchIn', [pokedex.gameScope]); + } + + const { data, error } = await query; + + if (error) { + console.error('Error calculating expected entries:', error); + throw new Error(`Failed to calculate expected entries: ${error.message}`); + } + + return data?.map((entry) => entry.id) || []; +} + +/** + * Populate pokedex_entries_mapping table for a pokedex + * Uses chunked upsert to avoid UNIQUE constraint violations and request size limits + * @throws Error if the Supabase upsert fails + */ +export async function populatePokedexMappings( + supabase: SupabaseClient, + pokedexId: string, + pokedex: Pokedex +): Promise { + const expectedEntryIds = await calculateExpectedEntries(supabase, pokedex); + + if (expectedEntryIds.length === 0) { + console.warn(`No expected entries calculated for pokedex ${pokedexId}`); + return; + } + + const mappings = expectedEntryIds.map((pokedexEntryId) => ({ + pokedexId, + pokedexEntryId + })); + + // 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', + ignoreDuplicates: true + }); + + if (error) { + console.error('Error populating pokedex mappings:', error); + throw new Error(`Failed to populate pokedex mappings: ${error.message}`); + } + } +} + +/** + * Recalculate pokedex_entries_mapping table for a pokedex (used on update) + * Uses a single atomic RPC call to prevent orphaned pokedexes + * @throws Error if the Supabase RPC call fails + */ +export async function recalculatePokedexMappings( + supabase: SupabaseClient, + pokedexId: string, + pokedex: Pokedex +): Promise { + const expectedEntryIds = await calculateExpectedEntries(supabase, pokedex); + + if (expectedEntryIds.length === 0) { + console.warn(`No expected entries calculated for pokedex ${pokedexId}`); + return; + } + + // Call the atomic RPC function to delete old mappings and insert new ones + const { error } = await supabase.rpc('recalculate_pokedex_mappings', { + p_pokedex_id: pokedexId, + p_entry_ids: expectedEntryIds + }); + + if (error) { + console.error('Error recalculating pokedex mappings:', error); + throw new Error(`Failed to recalculate pokedex mappings: ${error.message}`); + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 63637d6..5ecdc77 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -24,6 +24,19 @@ onMount(async () => { await getUser(); + // Listen for auth state changes to keep the user store in sync + const { + data: { subscription } + } = supabase.auth.onAuthStateChange((event, session) => { + console.log('[Layout] Auth state changed:', event, session ? 'Session found' : 'No session'); + if (session) { + localUser = session.user; + } else { + localUser = null; + } + user.set(localUser); + }); + if (pwaInfo) { const { registerSW } = await import('virtual:pwa-register'); registerSW({ @@ -41,6 +54,10 @@ } }); } + + return () => { + subscription.unsubscribe(); + }; }); async function getUser() { @@ -91,7 +108,7 @@