feat(#67): Add support for multiple pokedexes per user

This commit is contained in:
Josh Creek
2026-01-04 22:06:36 +00:00
parent 578df57a45
commit 19a571e315
28 changed files with 1401 additions and 215 deletions
+107
View File
@@ -0,0 +1,107 @@
import { json } from '@sveltejs/kit';
import type { Pokedex } from '$lib/models/Pokedex';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
// GET: Get single pokedex by ID
export const GET = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id } = event.params;
if (!id) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await repo.findById(id);
if (!pokedex) {
// RLS ensures user can only access own pokédexes
// Return 404 whether it doesn't exist or user doesn't own it (don't leak info)
return json({ error: 'Pokedex not found' }, { status: 404 });
}
return json(pokedex);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
// PUT: Update pokedex
export const PUT = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id } = event.params;
const data: Partial<Pokedex> = await event.request.json();
if (!id) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await repo.update(id, data);
if (!pokedex) {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
return json(pokedex);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
// DELETE: Delete pokedex
export const DELETE = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id } = event.params;
if (!id) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
const repo = new PokedexRepository(event.locals.supabase, userId);
// Verify pokedex exists and user owns it
const pokedex = await repo.findById(id);
if (!pokedex) {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
await repo.delete(id);
return json({ success: true });
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
@@ -0,0 +1,129 @@
import { json } from '@sveltejs/kit';
import { type CatchRecord } from '$lib/models/CatchRecord';
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
// GET: Get catch records for specific pokédex
export const GET = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id: pokedexId } = event.params;
if (!pokedexId) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
// Verify user owns this pokédex (RLS will also check, but explicit verification is better UX)
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await pokedexRepo.findById(pokedexId);
if (!pokedex) {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
const catchData = await repo.findAll();
const sortedData = catchData.sort(
(a, b) => Number(a.pokedexEntryId) - Number(b.pokedexEntryId)
);
return json(sortedData);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
// PUT: Upsert single catch record for specific pokédex
export const PUT = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id: pokedexId } = event.params;
const data: Partial<CatchRecord> = await event.request.json();
if (!pokedexId) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
// Verify user owns this pokédex
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await pokedexRepo.findById(pokedexId);
if (!pokedex) {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
// Ensure the data is scoped to this pokédex and user
data.userId = userId;
data.pokedexId = pokedexId;
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
const upsertedRecord = await repo.upsert(data);
return json(upsertedRecord);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
// POST: Bulk upsert catch records for specific pokédex
export const POST = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const { id: pokedexId } = event.params;
const records: Partial<CatchRecord>[] = await event.request.json();
if (!pokedexId) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
// Verify user owns this pokédex
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await pokedexRepo.findById(pokedexId);
if (!pokedex) {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
const insertedRecords = [];
for (const record of records) {
// Ensure the data is scoped to this pokédex and user
record.userId = userId;
record.pokedexId = pokedexId;
const upsertedRecord = await repo.upsert(record);
insertedRecords.push(upsertedRecord);
}
return json(insertedRecords);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
@@ -0,0 +1,72 @@
import { json } from '@sveltejs/kit';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
// GET: Get combined data (pokédex entries + catch records) for specific pokédex
export const GET = async (event: RequestEvent) => {
try {
const userId = await getOptionalUserId(event);
const { id: pokedexId } = event.params;
if (!pokedexId) {
return json({ error: 'Pokedex ID is required' }, { status: 400 });
}
// Parse query parameters
const url = new URL(event.request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '20');
const enableForms = url.searchParams.get('enableForms') === 'true';
const region = url.searchParams.get('region') || '';
const game = url.searchParams.get('game') || '';
// If authenticated, verify user owns this pokédex and get its gameScope
let pokedex;
if (userId) {
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
pokedex = await pokedexRepo.findById(pokedexId);
if (!pokedex) {
// User is authenticated but doesn't own this pokédex (or it doesn't exist)
return json({ error: 'Pokedex not found' }, { status: 404 });
}
} else {
// Anonymous users cannot view pokédexes
return json({ error: 'Unauthorized' }, { status: 401 });
}
// Use pokédex's gameScope as default filter if no manual game filter is set
const effectiveGame = game || pokedex.gameScope || '';
const repo = new CombinedDataRepository(event.locals.supabase, userId, pokedexId);
// Get paginated combined data
const combinedData = await repo.findCombinedData(
userId!,
page,
limit,
enableForms,
region,
effectiveGame
);
// Get total count for pagination
const totalCount = await repo.countCombinedData(enableForms, region, effectiveGame);
const totalPages = Math.ceil(totalCount / limit);
return json({
combinedData,
totalPages,
currentPage: page,
totalCount
});
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};