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
-52
View File
@@ -1,52 +0,0 @@
import { json } from '@sveltejs/kit';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async (event: RequestEvent) => {
const { url } = event;
const page = parseInt(url.searchParams.get('page') || '1', 10);
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const enableForms = url.searchParams.get('enableForms') === 'true';
const region = url.searchParams.get('region') || '';
const game = url.searchParams.get('game') || '';
try {
// Get userId if authenticated, null if not
const userId = await getOptionalUserId(event);
// If user is authenticated, set their session on the Supabase client
if (userId) {
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
}
const repo = new CombinedDataRepository(event.locals.supabase, userId || '');
const combinedData = await repo.findCombinedData(
userId || '',
page,
limit,
enableForms,
region,
game
);
// Return empty array instead of 404 for better UX
if (combinedData.length === 0) {
return json({ combinedData: [], totalPages: 0 });
}
const totalCount = await repo.countCombinedData(enableForms, region, game);
const totalPages = Math.ceil(totalCount / limit);
return json({ combinedData, totalPages });
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
@@ -1,36 +0,0 @@
import { json } from '@sveltejs/kit';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
export const GET = async (event: RequestEvent) => {
const { url } = event;
const enableForms = url.searchParams.get('enableForms') === 'true';
const region = url.searchParams.get('region') || '';
const game = url.searchParams.get('game') || '';
try {
// Get userId if authenticated, null if not
const userId = await getOptionalUserId(event);
// If user is authenticated, set their session on the Supabase client
if (userId) {
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
}
const repo = new CombinedDataRepository(event.locals.supabase, userId || '');
const combinedData = await repo.findAllCombinedData(userId || '', enableForms, region, game);
// Return empty array instead of 404 for better UX
return json(combinedData);
} catch (err) {
console.error(err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
+47
View File
@@ -0,0 +1,47 @@
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: List all pokedexes for user
export const GET = async (event: RequestEvent) => {
try {
const userId = await requireAuth(event);
const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedexes = await repo.findAll();
return json(pokedexes);
} catch (err) {
console.error('Error in GET /api/pokedexes:', err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
// POST: Create new pokedex
export const POST = async (event: RequestEvent) => {
try {
console.log('POST /api/pokedexes - Starting request');
const userId = await requireAuth(event);
console.log('POST /api/pokedexes - User authenticated:', userId);
const data: Partial<Pokedex> = await event.request.json();
console.log('POST /api/pokedexes - Request data:', data);
data.userId = userId;
const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await repo.create(data);
console.log('POST /api/pokedexes - Pokédex created:', pokedex._id);
return json(pokedex);
} catch (err) {
console.error('Error in POST /api/pokedexes:', err);
if (err && typeof err === 'object' && 'status' in err) {
throw err;
}
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
+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 });
}
};
@@ -1,22 +1,35 @@
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 });
}
// Get the user's session and set it on the Supabase client
const { session } = await event.locals.safeGetSession();
if (session) {
await event.locals.supabase.auth.setSession(session);
}
const repo = new CatchRecordRepository(event.locals.supabase, userId);
const catchData = await repo.findByUserId(userId);
// order by pokedexEntryId property, ascending
// 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)
);
@@ -30,24 +43,35 @@ export const GET = async (event: RequestEvent) => {
}
};
// PUT: Upsert single catch record for specific pokédex
export const PUT = async (event: RequestEvent) => {
try {
// Check if we can get a session first
const { session, user } = await event.locals.safeGetSession();
const userId = await requireAuth(event);
const { id: pokedexId } = event.params;
const data: Partial<CatchRecord> = await event.request.json();
// Get the user's session and set it on the Supabase client
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);
}
// Ensure the userId is set to the authenticated user
data.userId = userId;
// Verify user owns this pokédex
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await pokedexRepo.findById(pokedexId);
const repo = new CatchRecordRepository(event.locals.supabase, userId);
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) {
@@ -59,23 +83,37 @@ export const PUT = async (event: RequestEvent) => {
}
};
// 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();
// Get the user's session and set it on the Supabase client
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);
}
const repo = new CatchRecordRepository(event.locals.supabase, userId);
// 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 userId is set to the authenticated user
// 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);
}
@@ -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 });
}
};