fix: repair defects surfaced by gating CI on lint and typecheck

`npm run check` reported 15 errors and `npm run lint` 20, all pre-existing, so
neither gate could pass. Fixing them turned up three real bugs:

- SignOut destructured `{ error }` off `.then(() => {})`, which resolves to
  undefined, so every sign-out threw a TypeError - after the signed-out event had
  already been emitted. Sign-out also left the user on the protected page they
  were on, still showing its content; it now returns them to the home page and
  re-runs the server loads.

- SignUp passed `redirectTo`, which is not a signUp option and was silently
  ignored, so the confirmation link has always used Supabase's configured site
  URL. Documented rather than changed, since pointing it elsewhere needs an
  absolute allow-listed URL.

- The Pokédex page tracked totalRecordsCreated but never passed it to the box
  view, so the "Processed N entries so far" progress message never rendered.

The rest is typing and dead code: cookie callback parameters in hooks.server.ts
and +layout.ts, the untyped supabase props, a query-builder type that made
PostgREST rows untyped downstream, an unused session destructure, and
`while (true)` paging loops rewritten as `for (;;)`.
This commit is contained in:
Josh Creek
2026-09-13 17:36:14 +01:00
parent 92d6460765
commit 3a2c18bbeb
13 changed files with 60 additions and 60 deletions
+4 -3
View File
@@ -1,21 +1,22 @@
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { createServerClient } from '@supabase/ssr'; import { createServerClient } from '@supabase/ssr';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
import type { CookieSerializeOptions } from 'cookie';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: { cookies: {
get: (key) => event.cookies.get(key), get: (key: string) => event.cookies.get(key),
/** /**
* Note: You have to add the `path` variable to the * Note: You have to add the `path` variable to the
* set and remove method due to sveltekit's cookie API * set and remove method due to sveltekit's cookie API
* requiring this to be set, setting the path to an empty string * requiring this to be set, setting the path to an empty string
* will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies) * will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
*/ */
set: (key, value, options) => { set: (key: string, value: string, options: CookieSerializeOptions) => {
event.cookies.set(key, value, { ...options, path: '/' }); event.cookies.set(key, value, { ...options, path: '/' });
}, },
remove: (key, options) => { remove: (key: string, options: CookieSerializeOptions) => {
event.cookies.delete(key, { ...options, path: '/' }); event.cookies.delete(key, { ...options, path: '/' });
} }
} }
+2 -2
View File
@@ -11,8 +11,8 @@
currentPage = Math.max(currentPage - 1, 1); currentPage = Math.max(currentPage - 1, 1);
} }
function setItemsPerPage(event: any) { function setItemsPerPage(event: Event) {
itemsPerPage = parseInt(event.target.value, 10); itemsPerPage = parseInt((event.target as HTMLSelectElement).value, 10);
} }
</script> </script>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { SupabaseClient } from '@supabase/supabase-js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -12,7 +13,7 @@
let errorMessage = ''; let errorMessage = '';
// Access the supabase client from the layout data // Access the supabase client from the layout data
export let supabase: any; export let supabase: SupabaseClient;
async function signInWithEmail() { async function signInWithEmail() {
isLoading = true; isLoading = true;
+10 -4
View File
@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { SupabaseClient } from '@supabase/supabase-js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function emitSignedOutEvent() { function emitSignedOutEvent() {
@@ -7,13 +9,17 @@
} }
// Access the supabase client from the layout data // Access the supabase client from the layout data
export let supabase: any; export let supabase: SupabaseClient;
async function signOut() { async function signOut() {
// TODO use the error from the response // `.then(() => {...})` resolved to undefined, so destructuring `error` off it threw a
const { error } = await supabase.auth.signOut().then(() => { // TypeError on every sign-out - after the event had already been emitted.
const { error } = await supabase.auth.signOut();
if (error) console.error('Sign out failed', error);
emitSignedOutEvent(); emitSignedOutEvent();
}); // Signing out used to leave the user sitting on the protected page they were on, still
// showing its content. Send them to the public home page and re-run the server loads.
await goto('/', { invalidateAll: true });
} }
</script> </script>
+6 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { SupabaseClient } from '@supabase/supabase-js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -6,17 +7,16 @@
let password = ''; let password = '';
// Access the supabase client from the layout data // Access the supabase client from the layout data
export let supabase: any; export let supabase: SupabaseClient;
async function signUpNewUser() { async function signUpNewUser() {
try { try {
// `redirectTo` is not a signUp option - it was silently ignored, so the confirmation
// link has always used Supabase's configured site URL. Sending the user to /welcome
// would need `emailRedirectTo` with an absolute, allow-listed URL.
const { data, error } = await supabase.auth.signUp({ const { data, error } = await supabase.auth.signUp({
email: email, email: email,
password: password, password: password
options: {
// Redirect URL after successful sign-up
redirectTo: '/welcome'
}
}); });
if (error) { if (error) {
@@ -12,11 +12,11 @@
export let creatingRecords = false; export let creatingRecords = false;
export let totalRecordsCreated = 0; export let totalRecordsCreated = 0;
export let failedToLoad = false; export let failedToLoad = false;
export let markBoxAsNotCaught = (boxNumber: number) => {}; export let markBoxAsNotCaught: (boxNumber: number) => void = () => {};
export let markBoxAsCaught = (boxNumber: number) => {}; export let markBoxAsCaught: (boxNumber: number) => void = () => {};
export let markBoxAsNeedsToEvolve = (boxNumber: number) => {}; export let markBoxAsNeedsToEvolve: (boxNumber: number) => void = () => {};
export let markBoxAsInHome = (boxNumber: number) => {}; export let markBoxAsInHome: (boxNumber: number) => void = () => {};
export let markBoxAsNotInHome = (boxNumber: number) => {}; export let markBoxAsNotInHome: (boxNumber: number) => void = () => {};
export let createCatchRecords = () => {}; export let createCatchRecords = () => {};
export let onPokemonClick: (pokemon: CombinedData) => void = () => {}; export let onPokemonClick: (pokemon: CombinedData) => void = () => {};
@@ -126,7 +126,7 @@ class CombinedDataRepository {
let start = 0; let start = 0;
const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST; const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST;
while (true) { for (;;) {
const end = start + maxRows - 1; const end = start + maxRows - 1;
let query = this.supabase.from('pokedex_entries').select('*').not('form', 'is', null); let query = this.supabase.from('pokedex_entries').select('*').not('form', 'is', null);
@@ -167,7 +167,7 @@ class CombinedDataRepository {
let start = 0; let start = 0;
const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST; const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST;
while (true) { for (;;) {
const end = start + maxRows - 1; const end = start + maxRows - 1;
const { data, error } = await this.buildDexEntriesQuery(dexScopes, enableForms, region).range( const { data, error } = await this.buildDexEntriesQuery(dexScopes, enableForms, region).range(
start, start,
@@ -312,7 +312,7 @@ class CombinedDataRepository {
let start = 0; let start = 0;
const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST; const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST;
while (true) { for (;;) {
const end = start + maxRows - 1; const end = start + maxRows - 1;
const { data, error } = await this.buildEntriesQuery(enableForms, region, game).range( const { data, error } = await this.buildEntriesQuery(enableForms, region, game).range(
start, start,
@@ -4,6 +4,10 @@ import type {
PokedexExportIntegrationDB PokedexExportIntegrationDB
} from '$lib/models/PokedexExportIntegration'; } from '$lib/models/PokedexExportIntegration';
// `from()` returns a table builder; only `select()` yields the filter builder that `eq`/`is`
// live on. Typing the scope helper with the table builder made `data` untyped downstream.
type IntegrationQuery = ReturnType<ReturnType<SupabaseClient['from']>['select']>;
class PokedexExportIntegrationRepository { class PokedexExportIntegrationRepository {
constructor( constructor(
private supabase: SupabaseClient, private supabase: SupabaseClient,
@@ -34,7 +38,7 @@ class PokedexExportIntegrationRepository {
return this.supabase.from('pokedex_export_integrations').select('*').eq('userId', this.userId); return this.supabase.from('pokedex_export_integrations').select('*').eq('userId', this.userId);
} }
private addPokedexScope(query: ReturnType<SupabaseClient['from']>) { private addPokedexScope(query: IntegrationQuery): IntegrationQuery {
if (this.pokedexId) { if (this.pokedexId) {
return query.eq('pokedexId', this.pokedexId); return query.eq('pokedexId', this.pokedexId);
} }
@@ -49,7 +53,8 @@ class PokedexExportIntegrationRepository {
throw new Error(`Failed to load export integrations: ${error.message}`); throw new Error(`Failed to load export integrations: ${error.message}`);
} }
if (!data) return []; if (!data) return [];
return data.map((row) => this.transform(row)); // PostgREST rows are untyped without generated database types.
return (data as PokedexExportIntegrationDB[]).map((row) => this.transform(row));
} }
async listAll(): Promise<PokedexExportIntegration[]> { async listAll(): Promise<PokedexExportIntegration[]> {
@@ -60,12 +65,12 @@ class PokedexExportIntegrationRepository {
throw new Error(`Failed to load export integrations: ${error.message}`); throw new Error(`Failed to load export integrations: ${error.message}`);
} }
if (!data) return []; if (!data) return [];
return data.map((row) => this.transform(row)); // PostgREST rows are untyped without generated database types.
return (data as PokedexExportIntegrationDB[]).map((row) => this.transform(row));
} }
async upsert( async upsert(
data: Partial<PokedexExportIntegrationDB> & data: Partial<PokedexExportIntegrationDB> & Pick<PokedexExportIntegrationDB, 'provider'>
Pick<PokedexExportIntegrationDB, 'provider'>
): Promise<PokedexExportIntegration> { ): Promise<PokedexExportIntegration> {
const payload: Partial<PokedexExportIntegrationDB> = { const payload: Partial<PokedexExportIntegrationDB> = {
userId: this.userId, userId: this.userId,
@@ -105,7 +110,8 @@ class PokedexExportIntegrationRepository {
throw new Error(`Failed to load export integrations: ${error.message}`); throw new Error(`Failed to load export integrations: ${error.message}`);
} }
if (!data) return []; if (!data) return [];
return data.map((row) => this.transform(row)); // PostgREST rows are untyped without generated database types.
return (data as PokedexExportIntegrationDB[]).map((row) => this.transform(row));
} }
async updateTokens( async updateTokens(
+1 -1
View File
@@ -21,7 +21,7 @@ export async function requireAuth(event: RequestEvent): Promise<string> {
*/ */
export async function getOptionalUserId(event: RequestEvent): Promise<string | null> { export async function getOptionalUserId(event: RequestEvent): Promise<string | null> {
try { try {
const { session, user } = await event.locals.safeGetSession(); const { user } = await event.locals.safeGetSession();
return user?.id || null; return user?.id || null;
} catch { } catch {
return null; return null;
-1
View File
@@ -13,7 +13,6 @@ export function calculateBoxPlacement(index: number): {
} { } {
const POKEMON_PER_BOX = 30; const POKEMON_PER_BOX = 30;
const COLUMNS_PER_BOX = 6; const COLUMNS_PER_BOX = 6;
const ROWS_PER_BOX = 5;
// Calculate which box this Pokémon belongs to (1-indexed) // Calculate which box this Pokémon belongs to (1-indexed)
const box = Math.floor(index / POKEMON_PER_BOX) + 1; const box = Math.floor(index / POKEMON_PER_BOX) + 1;
+1 -1
View File
@@ -10,7 +10,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
fetch fetch
}, },
cookies: { cookies: {
get(key) { get(key: string) {
if (!isBrowser()) { if (!isBrowser()) {
return JSON.stringify(data.session); return JSON.stringify(data.session);
} }
+2 -9
View File
@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import { user } from '$lib/stores/user.js';
import { type User } from '@supabase/auth-js';
import SignUp from '$lib/components/SignUp.svelte'; import SignUp from '$lib/components/SignUp.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@@ -9,12 +7,7 @@
let { supabase, stats } = data; let { supabase, stats } = data;
$: ({ supabase, stats } = data); $: ({ supabase, stats } = data);
let localUser: User | null; // Redirection is decided from the live session below, so the user store is not needed here.
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
let isCheckingSession = true; let isCheckingSession = true;
onMount(() => { onMount(() => {
+8 -14
View File
@@ -17,7 +17,6 @@
import type { Pokedex } from '$lib/models/Pokedex'; import type { Pokedex } from '$lib/models/Pokedex';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
// Get pokédex from server load (guarded for transient undefined during navigation/HMR) // Get pokédex from server load (guarded for transient undefined during navigation/HMR)
@@ -38,7 +37,10 @@
// Box view requires the full dataset for correct box numbering/placement. // Box view requires the full dataset for correct box numbering/placement.
// If/when a paginated list view is introduced, this can be lowered and paired with UI controls. // If/when a paginated list view is introduced, this can be lowered and paired with UI controls.
let itemsPerPage = 9999 as number; let itemsPerPage = 9999 as number;
let totalPages = 0 as number; type CatchUpdateEvent = CustomEvent<{
catchRecord: CatchRecord;
source: 'toggle' | 'notes' | 'notes-blur';
}>;
let creatingRecords = false; let creatingRecords = false;
let totalRecordsCreated = 0; let totalRecordsCreated = 0;
let failedToLoad = false; let failedToLoad = false;
@@ -128,7 +130,6 @@
}, 250); }, 250);
} }
// Derive from pokedex config // Derive from pokedex config
$: showOrigins = !!pokedex?.isOriginDex; $: showOrigins = !!pokedex?.isOriginDex;
$: showShiny = !!pokedex?.isShinyDex; $: showShiny = !!pokedex?.isShinyDex;
@@ -190,8 +191,6 @@
}); });
} }
function applyOptimisticCatchRecordUpdate(next: CatchRecord) { function applyOptimisticCatchRecordUpdate(next: CatchRecord) {
if (!combinedData) return; if (!combinedData) return;
const idx = combinedData.findIndex((cd) => cd.pokedexEntry._id === next.pokemonId); const idx = combinedData.findIndex((cd) => cd.pokedexEntry._id === next.pokemonId);
@@ -212,7 +211,7 @@
} }
} }
async function handleModalCatchUpdate(event: any) { async function handleModalCatchUpdate(event: CatchUpdateEvent) {
await updateACatch(event); await updateACatch(event);
} }
@@ -243,20 +242,16 @@
return; return;
} }
combinedData = fetchedData.combinedData; combinedData = fetchedData.combinedData;
totalPages = fetchedData.totalPages || 0;
// Always extract box numbers for box view // Always extract box numbers for box view
if (combinedData) { if (combinedData) {
boxNumbers = calculateBoxNumbers(combinedData.length); boxNumbers = calculateBoxNumbers(combinedData.length);
} }
} }
async function updateACatch(event: any) { async function updateACatch(event: CatchUpdateEvent) {
if (!pokedexId) return; if (!pokedexId) return;
ensureCatchWriteQueue(); ensureCatchWriteQueue();
const { catchRecord, source } = event.detail as { const { catchRecord, source } = event.detail;
catchRecord: CatchRecord;
source: 'toggle' | 'notes' | 'notes-blur';
};
// Enforce mutual exclusivity (should be impossible to have both true). // Enforce mutual exclusivity (should be impossible to have both true).
const sanitizedCatchRecord: CatchRecord = { ...catchRecord }; const sanitizedCatchRecord: CatchRecord = { ...catchRecord };
if (sanitizedCatchRecord.caught) { if (sanitizedCatchRecord.caught) {
@@ -464,7 +459,6 @@
window.clearInterval(reconcileInterval); window.clearInterval(reconcileInterval);
}; };
}); });
</script> </script>
<svelte:head> <svelte:head>
@@ -599,7 +593,6 @@
{#if pokedex.description} {#if pokedex.description}
<p class="text-sm text-base-content/70 mt-3">{pokedex.description}</p> <p class="text-sm text-base-content/70 mt-3">{pokedex.description}</p>
{/if} {/if}
</div> </div>
<!-- Right side: Actions --> <!-- Right side: Actions -->
@@ -637,6 +630,7 @@
bind:combinedData bind:combinedData
bind:boxNumbers bind:boxNumbers
bind:creatingRecords bind:creatingRecords
{totalRecordsCreated}
bind:failedToLoad bind:failedToLoad
{markBoxAsNotCaught} {markBoxAsNotCaught}
{markBoxAsCaught} {markBoxAsCaught}