refactor(*):Address PR comments

This commit is contained in:
Josh Creek
2025-07-26 19:42:04 +01:00
parent bc9ce84615
commit fa38fa69ca
22 changed files with 749 additions and 412 deletions
+1 -2
View File
@@ -1,5 +1,5 @@
{ {
"name": "LivingDexTracker", "name": "livingdextracker",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -29,7 +29,6 @@
"supabase:reset": "supabase db reset", "supabase:reset": "supabase db reset",
"supabase:studio": "supabase studio", "supabase:studio": "supabase studio",
"migrate:convert-tsv": "node scripts/convert-tsv-to-sql.js", "migrate:convert-tsv": "node scripts/convert-tsv-to-sql.js",
"migrate:from-mongo": "echo 'MongoDB to Supabase migration complete! Use new supabase endpoints.'",
"dev:local": "./scripts/dev-local.sh && npm run dev", "dev:local": "./scripts/dev-local.sh && npm run dev",
"dev:supabase": "supabase start && npm run dev" "dev:supabase": "supabase start && npm run dev"
}, },
+16 -6
View File
@@ -13,15 +13,25 @@
export let supabase: any; export let supabase: any;
async function signInWithEmail() { async function signInWithEmail() {
// TODO use the data and error from the response try {
const { data, error } = await supabase.auth const { data, error } = await supabase.auth.signInWithPassword({
.signInWithPassword({
email: email, email: email,
password: password password: password
})
.then(() => {
emitSignedInEvent();
}); });
if (error) {
console.error('Sign in error:', error);
alert(`Sign in failed: ${error.message}`);
return;
}
if (data) {
emitSignedInEvent();
}
} catch (err) {
console.error('Sign in error:', err);
alert('Sign in failed');
}
} }
</script> </script>
@@ -12,8 +12,8 @@
// Create a working copy for editing // Create a working copy for editing
$: workingCatchRecord = catchRecord || { $: workingCatchRecord = catchRecord || {
_id: '', _id: `temp-${Date.now()}-${pokedexEntry._id}`, // Generate temporary ID
userId: '', userId: '', // TODO: Get from session context
pokedexEntryId: pokedexEntry._id, pokedexEntryId: pokedexEntry._id,
haveToEvolve: false, haveToEvolve: false,
caught: false, caught: false,
@@ -136,8 +136,9 @@
</div> </div>
{/if} {/if}
<p> <p>
<label class="block font-bold mb-1" for={`personalNotesInput-${catchRecord?._id || pokedexEntry._id}`} <label
>Notes:</label class="block font-bold mb-1"
for={`personalNotesInput-${catchRecord?._id || pokedexEntry._id}`}>Notes:</label
> >
<textarea <textarea
bind:value={workingCatchRecord.personalNotes} bind:value={workingCatchRecord.personalNotes}
+17 -16
View File
@@ -1,8 +1,8 @@
import mongoose, { Schema, Document, Types } from 'mongoose'; // Supabase-based CatchRecord interface
export interface CatchRecord {
export interface CatchRecord extends Document { _id: string; // Maps to Supabase 'id' field for frontend compatibility
userId: string; userId: string;
pokedexEntryId: Types.ObjectId; pokedexEntryId: string; // String representation of the foreign key
haveToEvolve: boolean; haveToEvolve: boolean;
caught: boolean; caught: boolean;
inHome: boolean; inHome: boolean;
@@ -10,15 +10,16 @@ export interface CatchRecord extends Document {
personalNotes: string; personalNotes: string;
} }
const catchRecordSchema = new Schema<CatchRecord>({ // Database record type for Supabase
userId: String, export interface CatchRecordDB {
pokedexEntryId: { type: Schema.Types.ObjectId, ref: 'PokedexEntry', required: true }, id: string;
haveToEvolve: { type: Boolean, default: false }, userId: string;
caught: { type: Boolean, default: false }, pokedexEntryId: number; // Numeric foreign key in database
inHome: { type: Boolean, default: false }, haveToEvolve: boolean;
hasGigantamaxed: { type: Boolean, default: false }, caught: boolean;
personalNotes: String inHome: boolean;
}); hasGigantamaxed: boolean;
personalNotes: string;
const CatchRecordModel = mongoose.model<CatchRecord>('CatchRecord', catchRecordSchema); createdAt: string;
export default CatchRecordModel; updatedAt: string;
}
+24 -38
View File
@@ -1,6 +1,6 @@
import mongoose, { Schema, Document } from 'mongoose'; // Supabase-based PokedexEntry interface
export interface PokedexEntry {
export interface PokedexEntry extends Document { _id: string; // Maps to Supabase 'id' field for frontend compatibility
pokedexNumber: number; pokedexNumber: number;
boxPlacement: { box: number; row: number; column: number }; boxPlacement: { box: number; row: number; column: number };
boxPlacementForms: { box: number; row: number; column: number }; boxPlacementForms: { box: number; row: number; column: number };
@@ -11,41 +11,27 @@ export interface PokedexEntry extends Document {
gamesToCatchIn: string[]; gamesToCatchIn: string[];
regionToEvolveIn: string; regionToEvolveIn: string;
evolutionInformation: string; evolutionInformation: string;
catchInformation: [ catchInformation: string[];
{
game: string;
location: string;
notes: string;
}
];
} }
const pokedexEntrySchema = new Schema<PokedexEntry>({ // Database record type for Supabase
pokedexNumber: Number, export interface PokedexEntryDB {
boxPlacement: { id: number;
box: Number, pokedexNumber: number;
row: Number, pokemon: string;
column: Number form: string | null;
}, canGigantamax: boolean;
boxPlacementForms: { regionToCatchIn: string | null;
box: Number, gamesToCatchIn: string[] | null;
row: Number, regionToEvolveIn: string | null;
column: Number evolutionInformation: string | null;
}, catchInformation: string[] | null;
pokemon: String, boxPlacementFormsBox: number | null;
form: String, boxPlacementFormsRow: number | null;
canGigantamax: Boolean, boxPlacementFormsColumn: number | null;
regionToCatchIn: String, boxPlacementBox: number | null;
gamesToCatchIn: Array<string>, boxPlacementRow: number | null;
regionToEvolveIn: String, boxPlacementColumn: number | null;
evolutionInformation: String, createdAt: string;
catchInformation: [ updatedAt: string;
{
game: String,
location: String,
notes: String
} }
]
});
export default mongoose.model<PokedexEntry>('PokedexEntry', pokedexEntrySchema);
+46 -28
View File
@@ -1,34 +1,41 @@
import type { CatchRecord } from '$lib/models/CatchRecord'; import type { CatchRecord, CatchRecordDB } from '$lib/models/CatchRecord';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
class CatchRecordRepository { class CatchRecordRepository {
constructor(private supabase: SupabaseClient, private userId: string) {} constructor(
private supabase: SupabaseClient,
private userId: string
) {}
// Transform Supabase snake_case to frontend camelCase // Transform Supabase data to frontend format (minimal transformation)
private transformCatchRecord(record: any): CatchRecord { private transformCatchRecord(record: CatchRecordDB): CatchRecord {
return { return {
_id: record.id, _id: record.id,
userId: record.user_id, userId: record.userId,
pokedexEntryId: record.pokedex_entry_id.toString(), pokedexEntryId: record.pokedexEntryId.toString(),
haveToEvolve: record.have_to_evolve, haveToEvolve: record.haveToEvolve,
caught: record.caught, caught: record.caught,
inHome: record.in_home, inHome: record.inHome,
hasGigantamaxed: record.has_gigantamaxed, hasGigantamaxed: record.hasGigantamaxed,
personalNotes: record.personal_notes personalNotes: record.personalNotes
}; };
} }
// Transform frontend camelCase to Supabase snake_case // Transform frontend data to database format (minimal transformation)
private transformToDatabase(data: Partial<CatchRecord>): any { private transformToDatabase(data: Partial<CatchRecord>): Partial<CatchRecordDB> {
const dbData: any = {}; const dbData: Partial<CatchRecordDB> = {};
if (data.pokedexEntryId !== undefined && data.pokedexEntryId !== null) { if (data.pokedexEntryId !== undefined && data.pokedexEntryId !== null) {
dbData.pokedex_entry_id = Number(data.pokedexEntryId); const numericId = Number(data.pokedexEntryId);
if (isNaN(numericId)) {
throw new Error(`Invalid pokedexEntryId: ${data.pokedexEntryId}`);
} }
if (data.haveToEvolve !== undefined) dbData.have_to_evolve = data.haveToEvolve; dbData.pokedexEntryId = numericId;
}
if (data.haveToEvolve !== undefined) dbData.haveToEvolve = data.haveToEvolve;
if (data.caught !== undefined) dbData.caught = data.caught; if (data.caught !== undefined) dbData.caught = data.caught;
if (data.inHome !== undefined) dbData.in_home = data.inHome; if (data.inHome !== undefined) dbData.inHome = data.inHome;
if (data.hasGigantamaxed !== undefined) dbData.has_gigantamaxed = data.hasGigantamaxed; if (data.hasGigantamaxed !== undefined) dbData.hasGigantamaxed = data.hasGigantamaxed;
if (data.personalNotes !== undefined) dbData.personal_notes = data.personalNotes; if (data.personalNotes !== undefined) dbData.personalNotes = data.personalNotes;
return dbData; return dbData;
} }
@@ -38,7 +45,7 @@ class CatchRecordRepository {
.from('catch_records') .from('catch_records')
.select('*') .select('*')
.eq('id', id) .eq('id', id)
.eq('user_id', this.userId) .eq('userId', this.userId)
.single(); .single();
if (error || !data) return null; if (error || !data) return null;
@@ -49,19 +56,25 @@ class CatchRecordRepository {
const { data, error } = await this.supabase const { data, error } = await this.supabase
.from('catch_records') .from('catch_records')
.select('*') .select('*')
.eq('user_id', this.userId); .eq('userId', this.userId);
if (error || !data) return []; if (error || !data) return [];
return data.map(record => this.transformCatchRecord(record)); return data.map((record) => this.transformCatchRecord(record));
} }
async findByUserId(userId: string): Promise<CatchRecord[]> { async findByUserId(userId: string): Promise<CatchRecord[]> {
return this.findAll(); // Already filtered by user in constructor const { data, error } = await this.supabase
.from('catch_records')
.select('*')
.eq('userId', userId);
if (error || !data) return [];
return data.map((record) => this.transformCatchRecord(record));
} }
async create(data: Partial<CatchRecord>): Promise<CatchRecord> { async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
const dbData = { const dbData = {
user_id: this.userId, userId: this.userId,
...this.transformToDatabase(data) ...this.transformToDatabase(data)
}; };
@@ -90,7 +103,7 @@ class CatchRecordRepository {
.from('catch_records') .from('catch_records')
.update(dbData) .update(dbData)
.eq('id', id) .eq('id', id)
.eq('user_id', this.userId) .eq('userId', this.userId)
.select() .select()
.single(); .single();
@@ -99,11 +112,16 @@ class CatchRecordRepository {
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.supabase const { error } = await this.supabase
.from('catch_records') .from('catch_records')
.delete() .delete()
.eq('id', id) .eq('id', id)
.eq('user_id', this.userId); .eq('userId', this.userId);
if (error) {
console.error('Error deleting catch record:', error);
throw new Error(`Failed to delete catch record: ${error.message}`);
}
} }
async upsert(data: Partial<CatchRecord>): Promise<CatchRecord | null> { async upsert(data: Partial<CatchRecord>): Promise<CatchRecord | null> {
@@ -125,8 +143,8 @@ class CatchRecordRepository {
const { data, error } = await this.supabase const { data, error } = await this.supabase
.from('catch_records') .from('catch_records')
.select('*') .select('*')
.eq('user_id', userId) .eq('userId', userId)
.eq('pokedex_entry_id', pokedexEntryId) .eq('pokedexEntryId', pokedexEntryId)
.single(); .single();
if (error || !data) return null; if (error || !data) return null;
+64 -59
View File
@@ -1,47 +1,50 @@
import { type PokedexEntry } from '$lib/models/PokedexEntry'; import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry';
import { type CatchRecord } from '$lib/models/CatchRecord'; import { type CatchRecord, type CatchRecordDB } from '$lib/models/CatchRecord';
import { type CombinedData } from '$lib/models/CombinedData'; import { type CombinedData } from '$lib/models/CombinedData';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
class CombinedDataRepository { class CombinedDataRepository {
constructor(private supabase: SupabaseClient, private userId: string) {} constructor(
private supabase: SupabaseClient,
private userId: string
) {}
// Transform Supabase data to match frontend expectations // Transform Supabase data to match frontend expectations (minimal transformation)
private transformPokedexEntry(entry: any): PokedexEntry { private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry {
return { return {
_id: entry.id.toString(), _id: entry.id.toString(),
pokedexNumber: entry.pokedex_number, pokedexNumber: entry.pokedexNumber,
pokemon: entry.pokemon, pokemon: entry.pokemon,
form: entry.form, form: entry.form || '',
canGigantamax: entry.can_gigantamax, canGigantamax: entry.canGigantamax,
regionToCatchIn: entry.region_to_catch_in, regionToCatchIn: entry.regionToCatchIn || '',
gamesToCatchIn: entry.games_to_catch_in || [], gamesToCatchIn: entry.gamesToCatchIn || [],
regionToEvolveIn: entry.region_to_evolve_in, regionToEvolveIn: entry.regionToEvolveIn || '',
evolutionInformation: entry.evolution_information, evolutionInformation: entry.evolutionInformation || '',
catchInformation: entry.catch_information || [], catchInformation: entry.catchInformation || [],
boxPlacementForms: { boxPlacementForms: {
box: entry.box_placement_forms_box, box: entry.boxPlacementFormsBox || 0,
row: entry.box_placement_forms_row, row: entry.boxPlacementFormsRow || 0,
column: entry.box_placement_forms_column column: entry.boxPlacementFormsColumn || 0
}, },
boxPlacement: { boxPlacement: {
box: entry.box_placement_box, box: entry.boxPlacementBox || 0,
row: entry.box_placement_row, row: entry.boxPlacementRow || 0,
column: entry.box_placement_column column: entry.boxPlacementColumn || 0
} }
}; };
} }
private transformCatchRecord(record: any): CatchRecord { private transformCatchRecord(record: CatchRecordDB): CatchRecord {
return { return {
_id: record.id, _id: record.id,
userId: record.user_id, userId: record.userId,
pokedexEntryId: record.pokedex_entry_id.toString(), pokedexEntryId: record.pokedexEntryId.toString(),
haveToEvolve: record.have_to_evolve, haveToEvolve: record.haveToEvolve,
caught: record.caught, caught: record.caught,
inHome: record.in_home, inHome: record.inHome,
hasGigantamaxed: record.has_gigantamaxed, hasGigantamaxed: record.hasGigantamaxed,
personalNotes: record.personal_notes personalNotes: record.personalNotes
}; };
} }
@@ -51,35 +54,35 @@ class CombinedDataRepository {
region: string = '', region: string = '',
game: string = '' game: string = ''
): Promise<CombinedData[]> { ): Promise<CombinedData[]> {
let query = this.supabase let query = this.supabase.from('pokedex_entries').select(`
.from('pokedex_entries')
.select(`
*, *,
catch_records(*) catch_records(*)
`); `);
// Apply filters // Apply filters
if (!enableForms) { if (!enableForms) {
query = query.not('box_placement_box', 'is', null); query = query.not('boxPlacementBox', 'is', null);
} }
if (region) { if (region) {
query = query.eq('region_to_catch_in', region); query = query.eq('regionToCatchIn', region);
} }
if (game) { if (game) {
query = query.contains('games_to_catch_in', [game]); query = query.contains('gamesToCatchIn', [game]);
} }
// Order by box placement // Order by box placement
if (enableForms) { if (enableForms) {
query = query.order('box_placement_forms_box', { ascending: true }) query = query
.order('box_placement_forms_row', { ascending: true }) .order('boxPlacementFormsBox', { ascending: true })
.order('box_placement_forms_column', { ascending: true }); .order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
} else { } else {
query = query.order('box_placement_box', { ascending: true }) query = query
.order('box_placement_row', { ascending: true }) .order('boxPlacementBox', { ascending: true })
.order('box_placement_column', { ascending: true }); .order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
} }
const { data, error } = await query; const { data, error } = await query;
@@ -90,9 +93,9 @@ class CombinedDataRepository {
} }
// Transform the data and filter catch records by user // Transform the data and filter catch records by user
return (data || []).map(entry => { return (data || []).map((entry) => {
const userCatchRecord = Array.isArray(entry.catch_records) const userCatchRecord = Array.isArray(entry.catch_records)
? entry.catch_records.find((record: any) => record.user_id === userId) ? entry.catch_records.find((record: CatchRecordDB) => record.userId === userId)
: null; : null;
const transformedEntry = this.transformPokedexEntry(entry); const transformedEntry = this.transformPokedexEntry(entry);
@@ -120,34 +123,38 @@ class CombinedDataRepository {
let query = this.supabase let query = this.supabase
.from('pokedex_entries') .from('pokedex_entries')
.select(` .select(
`
*, *,
catch_records(*) catch_records(*)
`) `
)
.range(from, to); .range(from, to);
// Apply filters // Apply filters
if (!enableForms) { if (!enableForms) {
query = query.not('box_placement_box', 'is', null); query = query.not('boxPlacementBox', 'is', null);
} }
if (region) { if (region) {
query = query.eq('region_to_catch_in', region); query = query.eq('regionToCatchIn', region);
} }
if (game) { if (game) {
query = query.contains('games_to_catch_in', [game]); query = query.contains('gamesToCatchIn', [game]);
} }
// Order by box placement // Order by box placement
if (enableForms) { if (enableForms) {
query = query.order('box_placement_forms_box', { ascending: true }) query = query
.order('box_placement_forms_row', { ascending: true }) .order('boxPlacementFormsBox', { ascending: true })
.order('box_placement_forms_column', { ascending: true }); .order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
} else { } else {
query = query.order('box_placement_box', { ascending: true }) query = query
.order('box_placement_row', { ascending: true }) .order('boxPlacementBox', { ascending: true })
.order('box_placement_column', { ascending: true }); .order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
} }
const { data, error } = await query; const { data, error } = await query;
@@ -158,9 +165,9 @@ class CombinedDataRepository {
} }
// Transform the data and filter catch records by user // Transform the data and filter catch records by user
return (data || []).map(entry => { return (data || []).map((entry) => {
const userCatchRecord = Array.isArray(entry.catch_records) const userCatchRecord = Array.isArray(entry.catch_records)
? entry.catch_records.find((record: any) => record.user_id === userId) ? entry.catch_records.find((record: CatchRecordDB) => record.userId === userId)
: null; : null;
const transformedEntry = this.transformPokedexEntry(entry); const transformedEntry = this.transformPokedexEntry(entry);
@@ -181,21 +188,19 @@ class CombinedDataRepository {
region: string, region: string,
game: string game: string
): Promise<number> { ): Promise<number> {
let query = this.supabase let query = this.supabase.from('pokedex_entries').select('id', { count: 'exact', head: true });
.from('pokedex_entries')
.select('id', { count: 'exact', head: true });
// Apply same filters as in findCombinedData // Apply same filters as in findCombinedData
if (!enableForms) { if (!enableForms) {
query = query.not('box_placement_box', 'is', null); query = query.not('boxPlacementBox', 'is', null);
} }
if (region) { if (region) {
query = query.eq('region_to_catch_in', region); query = query.eq('regionToCatchIn', region);
} }
if (game) { if (game) {
query = query.contains('games_to_catch_in', [game]); query = query.contains('gamesToCatchIn', [game]);
} }
const { count, error } = await query; const { count, error } = await query;
+43 -41
View File
@@ -1,31 +1,31 @@
import { type PokedexEntry } from '$lib/models/PokedexEntry'; import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
class PokedexEntryRepository { class PokedexEntryRepository {
constructor(private supabase: SupabaseClient) {} constructor(private supabase: SupabaseClient) {}
// Transform Supabase data to match frontend expectations // Transform Supabase data to match frontend expectations (minimal transformation)
private transformPokedexEntry(entry: any): PokedexEntry { private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry {
return { return {
_id: entry.id.toString(), _id: entry.id.toString(),
pokedexNumber: entry.pokedex_number, pokedexNumber: entry.pokedexNumber,
pokemon: entry.pokemon, pokemon: entry.pokemon,
form: entry.form, form: entry.form || '',
canGigantamax: entry.can_gigantamax, canGigantamax: entry.canGigantamax,
regionToCatchIn: entry.region_to_catch_in, regionToCatchIn: entry.regionToCatchIn || '',
gamesToCatchIn: entry.games_to_catch_in || [], gamesToCatchIn: entry.gamesToCatchIn || [],
regionToEvolveIn: entry.region_to_evolve_in, regionToEvolveIn: entry.regionToEvolveIn || '',
evolutionInformation: entry.evolution_information, evolutionInformation: entry.evolutionInformation || '',
catchInformation: entry.catch_information || [], catchInformation: entry.catchInformation || [],
boxPlacementForms: { boxPlacementForms: {
box: entry.box_placement_forms_box, box: entry.boxPlacementFormsBox || 0,
row: entry.box_placement_forms_row, row: entry.boxPlacementFormsRow || 0,
column: entry.box_placement_forms_column column: entry.boxPlacementFormsColumn || 0
}, },
boxPlacement: { boxPlacement: {
box: entry.box_placement_box, box: entry.boxPlacementBox || 0,
row: entry.box_placement_row, row: entry.boxPlacementRow || 0,
column: entry.box_placement_column column: entry.boxPlacementColumn || 0
} }
}; };
} }
@@ -45,30 +45,35 @@ class PokedexEntryRepository {
const { data, error } = await this.supabase const { data, error } = await this.supabase
.from('pokedex_entries') .from('pokedex_entries')
.select('*') .select('*')
.order('pokedex_number', { ascending: true }); .order('pokedexNumber', { ascending: true });
if (error || !data) return []; if (error || !data) return [];
return data.map(entry => this.transformPokedexEntry(entry)); return data.map((entry) => this.transformPokedexEntry(entry));
} }
async create(data: Partial<PokedexEntry>): Promise<PokedexEntry> { async create(data: Partial<PokedexEntry>): Promise<PokedexEntry> {
// Transform camelCase to snake_case for database // Transform to database format (minimal transformation needed)
const dbData: any = {}; const dbData: Partial<PokedexEntryDB> = {};
if (data.pokedexNumber !== undefined) dbData.pokedex_number = data.pokedexNumber; if (data.pokedexNumber !== undefined) dbData.pokedexNumber = data.pokedexNumber;
if (data.pokemon !== undefined) dbData.pokemon = data.pokemon; if (data.pokemon !== undefined) dbData.pokemon = data.pokemon;
if (data.form !== undefined) dbData.form = data.form; if (data.form !== undefined) dbData.form = data.form;
if (data.canGigantamax !== undefined) dbData.can_gigantamax = data.canGigantamax; if (data.canGigantamax !== undefined) dbData.canGigantamax = data.canGigantamax;
if (data.regionToCatchIn !== undefined) dbData.region_to_catch_in = data.regionToCatchIn; if (data.regionToCatchIn !== undefined) dbData.regionToCatchIn = data.regionToCatchIn;
if (data.gamesToCatchIn !== undefined) dbData.games_to_catch_in = data.gamesToCatchIn; if (data.gamesToCatchIn !== undefined) dbData.gamesToCatchIn = data.gamesToCatchIn;
if (data.regionToEvolveIn !== undefined) dbData.region_to_evolve_in = data.regionToEvolveIn; if (data.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn;
if (data.evolutionInformation !== undefined) dbData.evolution_information = data.evolutionInformation; if (data.evolutionInformation !== undefined)
if (data.catchInformation !== undefined) dbData.catch_information = data.catchInformation; dbData.evolutionInformation = data.evolutionInformation;
if (data.boxPlacementForms?.box !== undefined) dbData.box_placement_forms_box = data.boxPlacementForms.box; if (data.catchInformation !== undefined) dbData.catchInformation = data.catchInformation;
if (data.boxPlacementForms?.row !== undefined) dbData.box_placement_forms_row = data.boxPlacementForms.row; if (data.boxPlacementForms?.box !== undefined)
if (data.boxPlacementForms?.column !== undefined) dbData.box_placement_forms_column = data.boxPlacementForms.column; dbData.boxPlacementFormsBox = data.boxPlacementForms.box;
if (data.boxPlacement?.box !== undefined) dbData.box_placement_box = data.boxPlacement.box; if (data.boxPlacementForms?.row !== undefined)
if (data.boxPlacement?.row !== undefined) dbData.box_placement_row = data.boxPlacement.row; dbData.boxPlacementFormsRow = data.boxPlacementForms.row;
if (data.boxPlacement?.column !== undefined) dbData.box_placement_column = data.boxPlacement.column; if (data.boxPlacementForms?.column !== undefined)
dbData.boxPlacementFormsColumn = data.boxPlacementForms.column;
if (data.boxPlacement?.box !== undefined) dbData.boxPlacementBox = data.boxPlacement.box;
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
if (data.boxPlacement?.column !== undefined)
dbData.boxPlacementColumn = data.boxPlacement.column;
const { data: result, error } = await this.supabase const { data: result, error } = await this.supabase
.from('pokedex_entries') .from('pokedex_entries')
@@ -81,9 +86,9 @@ class PokedexEntryRepository {
} }
async update(id: string, data: Partial<PokedexEntry>): Promise<PokedexEntry | null> { async update(id: string, data: Partial<PokedexEntry>): Promise<PokedexEntry | null> {
// Transform camelCase to snake_case for database // Transform to database format (minimal transformation needed)
const dbData: any = {}; const dbData: Partial<PokedexEntryDB> = {};
if (data.pokedexNumber !== undefined) dbData.pokedex_number = data.pokedexNumber; if (data.pokedexNumber !== undefined) dbData.pokedexNumber = data.pokedexNumber;
if (data.pokemon !== undefined) dbData.pokemon = data.pokemon; if (data.pokemon !== undefined) dbData.pokemon = data.pokemon;
// ... add other fields as needed // ... add other fields as needed
@@ -99,10 +104,7 @@ class PokedexEntryRepository {
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.supabase await this.supabase.from('pokedex_entries').delete().eq('id', id);
.from('pokedex_entries')
.delete()
.eq('id', id);
} }
} }
+37 -7
View File
@@ -7,15 +7,24 @@ import type { RequestEvent } from '@sveltejs/kit';
export const GET = async (event: RequestEvent) => { export const GET = async (event: RequestEvent) => {
try { try {
const userId = await requireAuth(event); const userId = await requireAuth(event);
// 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 repo = new CatchRecordRepository(event.locals.supabase, userId);
const catchData = await repo.findByUserId(userId); const catchData = await repo.findByUserId(userId);
// order by pokedexEntryId property, ascending // order by pokedexEntryId property, ascending
const sortedData = catchData.sort((a, b) => Number(a.pokedexEntryId) - Number(b.pokedexEntryId)); const sortedData = catchData.sort(
(a, b) => Number(a.pokedexEntryId) - Number(b.pokedexEntryId)
);
return json(sortedData); return json(sortedData);
} catch (error) { } catch (err) {
console.error(error); console.error(err);
if (error.status) { if (err && typeof err === 'object' && 'status' in err) {
throw error; throw err;
} }
return json({ error: 'Internal Server Error' }, { status: 500 }); return json({ error: 'Internal Server Error' }, { status: 500 });
} }
@@ -23,9 +32,23 @@ export const GET = async (event: RequestEvent) => {
export const PUT = async (event: RequestEvent) => { export const PUT = async (event: RequestEvent) => {
try { try {
console.log('PUT request received for catch-records');
// Check if we can get a session first
const { session, user } = await event.locals.safeGetSession();
console.log('Session check:', { hasSession: !!session, hasUser: !!user, userId: user?.id });
const userId = await requireAuth(event); const userId = await requireAuth(event);
console.log('Auth successful, userId:', userId);
const data: Partial<CatchRecord> = await event.request.json(); const data: Partial<CatchRecord> = await event.request.json();
// Get the user's session and set it on the Supabase client
if (session) {
await event.locals.supabase.auth.setSession(session);
console.log('Session set on Supabase client');
}
// Ensure the userId is set to the authenticated user // Ensure the userId is set to the authenticated user
data.userId = userId; data.userId = userId;
@@ -34,7 +57,7 @@ export const PUT = async (event: RequestEvent) => {
return json(upsertedRecord); return json(upsertedRecord);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
if (err.status) { if (err && typeof err === 'object' && 'status' in err) {
throw err; throw err;
} }
return json({ error: 'Internal Server Error' }, { status: 500 }); return json({ error: 'Internal Server Error' }, { status: 500 });
@@ -45,6 +68,13 @@ export const POST = async (event: RequestEvent) => {
try { try {
const userId = await requireAuth(event); const userId = await requireAuth(event);
const records: Partial<CatchRecord>[] = await event.request.json(); const records: Partial<CatchRecord>[] = await event.request.json();
// 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 repo = new CatchRecordRepository(event.locals.supabase, userId);
const insertedRecords = []; const insertedRecords = [];
@@ -58,7 +88,7 @@ export const POST = async (event: RequestEvent) => {
return json(insertedRecords); return json(insertedRecords);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
if (err.status) { if (err && typeof err === 'object' && 'status' in err) {
throw err; throw err;
} }
return json({ error: 'Internal Server Error' }, { status: 500 }); return json({ error: 'Internal Server Error' }, { status: 500 });
+26 -9
View File
@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository'; import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { requireAuth } from '$lib/utils/auth'; import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
export const GET = async (event: RequestEvent) => { export const GET = async (event: RequestEvent) => {
@@ -12,23 +12,40 @@ export const GET = async (event: RequestEvent) => {
const game = url.searchParams.get('game') || ''; const game = url.searchParams.get('game') || '';
try { try {
const userId = await requireAuth(event); // Get userId if authenticated, null if not
const repo = new CombinedDataRepository(event.locals.supabase, userId); const userId = await getOptionalUserId(event);
const combinedData = await repo.findCombinedData(userId, page, limit, enableForms, region, game);
// 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 // Return empty array instead of 404 for better UX
if (combinedData.length === 0) { if (combinedData.length === 0) {
return json({ combinedData: [], totalPages: 0 }); return json({ combinedData: [], totalPages: 0 });
} }
const totalCount = await repo.countCombinedData(userId, enableForms, region, game); const totalCount = await repo.countCombinedData(userId || '', enableForms, region, game);
const totalPages = Math.ceil(totalCount / limit); const totalPages = Math.ceil(totalCount / limit);
return json({ combinedData, totalPages }); return json({ combinedData, totalPages });
} catch (error) { } catch (err) {
console.error(error); console.error(err);
if (error.status) { if (err && typeof err === 'object' && 'status' in err) {
throw error; throw err;
} }
return json({ error: 'Internal Server Error' }, { status: 500 }); return json({ error: 'Internal Server Error' }, { status: 500 });
} }
+18 -8
View File
@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository'; import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import { requireAuth } from '$lib/utils/auth'; import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
export const GET = async (event: RequestEvent) => { export const GET = async (event: RequestEvent) => {
@@ -10,16 +10,26 @@ export const GET = async (event: RequestEvent) => {
const game = url.searchParams.get('game') || ''; const game = url.searchParams.get('game') || '';
try { try {
const userId = await requireAuth(event); // Get userId if authenticated, null if not
const repo = new CombinedDataRepository(event.locals.supabase, userId); const userId = await getOptionalUserId(event);
const combinedData = await repo.findAllCombinedData(userId, enableForms, region, game);
// 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 empty array instead of 404 for better UX
return json(combinedData); return json(combinedData);
} catch (error) { } catch (err) {
console.error(error); console.error(err);
if (error.status) { if (err && typeof err === 'object' && 'status' in err) {
throw error; throw err;
} }
return json({ error: 'Internal Server Error' }, { status: 500 }); return json({ error: 'Internal Server Error' }, { status: 500 });
} }
+13 -2
View File
@@ -8,6 +8,11 @@
import PokedexSidebar from '$lib/components/pokedex/PokedexSidebar.svelte'; import PokedexSidebar from '$lib/components/pokedex/PokedexSidebar.svelte';
import PokedexViewList from '$lib/components/pokedex/PokedexViewList.svelte'; import PokedexViewList from '$lib/components/pokedex/PokedexViewList.svelte';
import PokedexViewBoxes from '$lib/components/pokedex/PokedexViewBoxes.svelte'; import PokedexViewBoxes from '$lib/components/pokedex/PokedexViewBoxes.svelte';
import type { PageData } from './$types';
export let data: PageData;
$: ({ supabase, session } = data);
let combinedData = null as CombinedData[] | null; let combinedData = null as CombinedData[] | null;
let currentPage = 1 as number; let currentPage = 1 as number;
@@ -86,13 +91,19 @@
const requestOptions = { const requestOptions = {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: {
body: JSON.stringify(catchRecord) 'Content-Type': 'application/json'
},
body: JSON.stringify(catchRecord),
credentials: 'include' as RequestCredentials
}; };
try { try {
// Use enhanced fetch that includes authentication context
const response = await fetch('/api/catch-records', requestOptions); const response = await fetch('/api/catch-records', requestOptions);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text();
console.error('Server response:', response.status, errorText);
alert('Failed to update catch record'); alert('Failed to update catch record');
throw new Error('Failed to update catch record'); throw new Error('Failed to update catch record');
} }
+7
View File
@@ -0,0 +1,7 @@
export const load = async ({ parent }) => {
const { supabase, session } = await parent();
return {
supabase,
session
};
};
+1 -1
View File
@@ -168,7 +168,7 @@ enable_signup = true
# addresses. If disabled, only the new email is required to confirm. # addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in. # If enabled, users need to confirm their email address before signing in.
enable_confirmations = false enable_confirmations = true
# If enabled, users will need to reauthenticate or have logged in recently to change their password. # If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
@@ -1,34 +0,0 @@
-- Initial setup migration for LivingDexTracker
-- This sets up any additional schema needed
-- Note: RLS is already enabled on auth.users by default in Supabase
-- and proper policies are already in place
-- Create a profiles table if we need additional user data
-- This is optional for the current setup since we only use Supabase for auth
-- and store everything else in MongoDB
-- CREATE TABLE profiles (
-- id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
-- username TEXT,
-- avatar_url TEXT,
-- created_at TIMESTAMPTZ DEFAULT NOW(),
-- updated_at TIMESTAMPTZ DEFAULT NOW()
-- );
-- Enable RLS on profiles table
-- ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Create RLS policies for profiles
-- CREATE POLICY "Users can view own profile"
-- ON profiles
-- FOR SELECT
-- USING (auth.uid() = id);
-- CREATE POLICY "Users can update own profile"
-- ON profiles
-- FOR UPDATE
-- USING (auth.uid() = id);
-- Note: MongoDB collections (pokedexentries, catchrecords) are handled by the app
-- Supabase is only used for authentication in this hybrid setup
@@ -6,102 +6,128 @@
-- Pokemon entries table (replaces pokedexentries MongoDB collection) -- Pokemon entries table (replaces pokedexentries MongoDB collection)
CREATE TABLE pokedex_entries ( CREATE TABLE pokedex_entries (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
pokedex_number INTEGER NOT NULL, "pokedexNumber" INTEGER NOT NULL,
pokemon TEXT NOT NULL, pokemon TEXT NOT NULL,
form TEXT, form TEXT,
can_gigantamax BOOLEAN DEFAULT FALSE, "canGigantamax" BOOLEAN DEFAULT FALSE,
region_to_catch_in TEXT, "regionToCatchIn" TEXT,
games_to_catch_in TEXT[], -- Array of games "gamesToCatchIn" TEXT[], -- Array of games
region_to_evolve_in TEXT, "regionToEvolveIn" TEXT,
evolution_information TEXT, "evolutionInformation" TEXT,
catch_information TEXT[], -- Array of catch info "catchInformation" TEXT[], -- Array of catch info
-- Box placement for forms view -- Box placement for forms view
box_placement_forms_box INTEGER, "boxPlacementFormsBox" INTEGER,
box_placement_forms_row INTEGER, "boxPlacementFormsRow" INTEGER,
box_placement_forms_column INTEGER, "boxPlacementFormsColumn" INTEGER,
-- Box placement for no-forms view -- Box placement for no-forms view
box_placement_box INTEGER, "boxPlacementBox" INTEGER,
box_placement_row INTEGER, "boxPlacementRow" INTEGER,
box_placement_column INTEGER, "boxPlacementColumn" INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW(), "createdAt" TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() "updatedAt" TIMESTAMPTZ DEFAULT NOW()
); );
-- Add unique constraint to prevent duplicate pokemon forms
ALTER TABLE pokedex_entries ADD CONSTRAINT unique_pokemon_form
UNIQUE("pokedexNumber", form);
-- Catch records table (replaces catchrecords MongoDB collection) -- Catch records table (replaces catchrecords MongoDB collection)
CREATE TABLE catch_records ( CREATE TABLE catch_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, "userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
pokedex_entry_id BIGINT NOT NULL REFERENCES pokedex_entries(id) ON DELETE CASCADE, "pokedexEntryId" BIGINT NOT NULL REFERENCES pokedex_entries(id) ON DELETE CASCADE,
-- Catch status fields -- Catch status fields
have_to_evolve BOOLEAN DEFAULT FALSE, "haveToEvolve" BOOLEAN DEFAULT FALSE,
caught BOOLEAN DEFAULT FALSE, caught BOOLEAN DEFAULT FALSE,
in_home BOOLEAN DEFAULT FALSE, "inHome" BOOLEAN DEFAULT FALSE,
has_gigantamaxed BOOLEAN DEFAULT FALSE, "hasGigantamaxed" BOOLEAN DEFAULT FALSE,
personal_notes TEXT, "personalNotes" TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), "createdAt" TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(), "updatedAt" TIMESTAMPTZ DEFAULT NOW(),
-- Ensure one record per user per pokemon entry -- Ensure one record per user per pokemon entry
UNIQUE(user_id, pokedex_entry_id) UNIQUE("userId", "pokedexEntryId")
); );
-- Region/Game mappings table (replaces regiongamemappings MongoDB collection) -- Region-game mappings table for filtering
CREATE TABLE region_game_mappings ( CREATE TABLE region_game_mappings (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
region TEXT NOT NULL, region TEXT NOT NULL,
games TEXT[] NOT NULL, game TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() UNIQUE(region, game)
); );
-- Pokedex metadata table (replaces pokedexmetadata MongoDB collection) -- Metadata table for migration tracking
CREATE TABLE pokedex_metadata ( CREATE TABLE metadata (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
key TEXT UNIQUE NOT NULL, key TEXT UNIQUE NOT NULL,
value JSONB NOT NULL, value TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), "createdAt" TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() "updatedAt" TIMESTAMPTZ DEFAULT NOW()
); );
-- Enable Row Level Security (RLS) for user data isolation
ALTER TABLE catch_records ENABLE ROW LEVEL SECURITY;
-- RLS Policy: Users can only access their own catch records
CREATE POLICY "Users can only access their own catch records"
ON catch_records FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Create indexes for better performance -- Create indexes for better performance
CREATE INDEX idx_pokedex_entries_pokedex_number ON pokedex_entries(pokedex_number); CREATE INDEX idx_pokedex_entries_pokedex_number ON pokedex_entries("pokedexNumber");
CREATE INDEX idx_pokedex_entries_pokemon ON pokedex_entries(pokemon); CREATE INDEX idx_pokedex_entries_pokemon ON pokedex_entries(pokemon);
CREATE INDEX idx_pokedex_entries_box_placement_forms ON pokedex_entries(box_placement_forms_box, box_placement_forms_row, box_placement_forms_column); CREATE INDEX idx_pokedex_entries_box_placement_forms ON pokedex_entries("boxPlacementFormsBox", "boxPlacementFormsRow", "boxPlacementFormsColumn");
CREATE INDEX idx_pokedex_entries_box_placement ON pokedex_entries(box_placement_box, box_placement_row, box_placement_column); CREATE INDEX idx_pokedex_entries_box_placement ON pokedex_entries("boxPlacementBox", "boxPlacementRow", "boxPlacementColumn");
CREATE INDEX idx_catch_records_user_id ON catch_records(user_id); CREATE INDEX idx_catch_records_user_id ON catch_records("userId");
CREATE INDEX idx_catch_records_pokedex_entry_id ON catch_records(pokedex_entry_id); CREATE INDEX idx_catch_records_pokedex_entry_id ON catch_records("pokedexEntryId");
CREATE INDEX idx_catch_records_caught ON catch_records(caught); CREATE INDEX idx_catch_records_caught ON catch_records(caught);
CREATE INDEX idx_catch_records_in_home ON catch_records(in_home); CREATE INDEX idx_catch_records_in_home ON catch_records("inHome");
CREATE INDEX idx_catch_records_user_caught ON catch_records("userId", caught);
-- Create updated_at trigger function -- Function to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column() CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN
NEW.updated_at = NOW(); NEW."updatedAt" = NOW();
RETURN NEW; RETURN NEW;
END; END;
$$ language 'plpgsql'; $$ LANGUAGE plpgsql;
-- Add updated_at triggers -- Create triggers for updated_at
CREATE TRIGGER update_pokedex_entries_updated_at BEFORE UPDATE ON pokedex_entries CREATE TRIGGER update_pokedex_entries_updated_at
BEFORE UPDATE ON pokedex_entries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_catch_records_updated_at BEFORE UPDATE ON catch_records CREATE TRIGGER update_catch_records_updated_at
BEFORE UPDATE ON catch_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_pokedex_metadata_updated_at BEFORE UPDATE ON pokedex_metadata -- Enable RLS on all tables
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ALTER TABLE pokedex_entries ENABLE ROW LEVEL SECURITY;
ALTER TABLE catch_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE region_game_mappings ENABLE ROW LEVEL SECURITY;
ALTER TABLE metadata ENABLE ROW LEVEL SECURITY;
-- RLS policies for pokedex_entries (public read)
CREATE POLICY "Anyone can view pokemon entries" ON pokedex_entries
FOR SELECT USING (true);
-- RLS policies for catch_records (user-specific)
CREATE POLICY "Users can view own catch records" ON catch_records
FOR SELECT USING (auth.uid() = "userId");
CREATE POLICY "Users can insert own catch records" ON catch_records
FOR INSERT WITH CHECK (auth.uid() = "userId");
CREATE POLICY "Users can update own catch records" ON catch_records
FOR UPDATE USING (auth.uid() = "userId");
CREATE POLICY "Users can delete own catch records" ON catch_records
FOR DELETE USING (auth.uid() = "userId");
-- RLS policies for region_game_mappings (public read)
CREATE POLICY "Anyone can view region game mappings" ON region_game_mappings
FOR SELECT USING (true);
-- RLS policies for metadata (public read)
CREATE POLICY "Anyone can view metadata" ON metadata
FOR SELECT USING (true);
@@ -0,0 +1,130 @@
-- Migration: Create PostgreSQL schema for Pokemon data
-- This replaces the M-- Create indexes for better p$$ LANGUAGE plpgsql;
-- Create triggers for updated_at
CREATE TRIGGER update_pokedex_entries_updated_at
BEFORE UPDATE ON pokedex_entries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_catch_records_updated_at
BEFORE UPDATE ON catch_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();ance
CREATE INDEX idx_pokedex_entries_pokedex_number ON pokedex_entries(pokedex_number);
CREATE INDEX idx_pokedex_entries_pokemon ON pokedex_entries(pokemon);
CREATE INDEX idx_pokedex_entries_box_placement_forms ON pokedex_entries(box_placement_forms_box, box_placement_forms_row, box_placement_forms_column);
CREATE INDEX idx_pokedex_entries_box_placement ON pokedex_entries(box_placement_box, box_placement_row, box_placement_column);
CREATE INDEX idx_catch_records_user_id ON catch_records("userId");
CREATE INDEX idx_catch_records_pokedex_entry_id ON catch_records("pokedexEntryId");
CREATE INDEX idx_catch_records_caught ON catch_records(caught);
CREATE INDEX idx_catch_records_in_home ON catch_records("inHome");
CREATE INDEX idx_catch_records_user_caught ON catch_records("userId", caught);lections with proper PostgreSQL tables
-- Note: Using individual columns instead of composite types for simplicity
-- Pokemon entries table (replaces pokedexentries MongoDB collection)
CREATE TABLE pokedex_entries (
id BIGSERIAL PRIMARY KEY,
pokedexNumber INTEGER NOT NULL,
pokemon TEXT NOT NULL,
form TEXT,
canGigantamax BOOLEAN DEFAULT FALSE,
regionToCatchIn TEXT,
gamesToCatchIn TEXT[], -- Array of games
regionToEvolveIn TEXT,
evolutionInformation TEXT,
catchInformation TEXT[], -- Array of catch info
-- Box placement for forms view
boxPlacementFormsBox INTEGER,
boxPlacementFormsRow INTEGER,
boxPlacementFormsColumn INTEGER,
-- Box placement for no-forms view
boxPlacementBox INTEGER,
boxPlacementRow INTEGER,
boxPlacementColumn INTEGER,
createdAt TIMESTAMPTZ DEFAULT NOW(),
updatedAt TIMESTAMPTZ DEFAULT NOW()
);
-- Catch records table (replaces catchrecords MongoDB collection)
CREATE TABLE catch_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
"pokedexEntryId" BIGINT NOT NULL REFERENCES pokedex_entries(id) ON DELETE CASCADE,
-- Catch status fields
"haveToEvolve" BOOLEAN DEFAULT FALSE,
caught BOOLEAN DEFAULT FALSE,
"inHome" BOOLEAN DEFAULT FALSE,
"hasGigantamaxed" BOOLEAN DEFAULT FALSE,
"personalNotes" TEXT,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
-- Ensure one record per user per pokemon entry
UNIQUE("userId", "pokedexEntryId")
);
-- Region/Game mappings table (replaces regiongamemappings MongoDB collection)
CREATE TABLE region_game_mappings (
id BIGSERIAL PRIMARY KEY,
region TEXT NOT NULL,
games TEXT[] NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Pokedex metadata table (replaces pokedexmetadata MongoDB collection)
CREATE TABLE pokedex_metadata (
id BIGSERIAL PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Add unique constraint to prevent duplicate pokemon forms
ALTER TABLE pokedex_entries ADD CONSTRAINT unique_pokemon_form
UNIQUE(pokedex_number, form);
-- Enable Row Level Security (RLS) for user data isolation
ALTER TABLE catch_records ENABLE ROW LEVEL SECURITY;
-- RLS Policy: Users can only access their own catch records
CREATE POLICY "Users can only access their own catch records"
ON catch_records FOR ALL
USING (auth.uid() = userId)
WITH CHECK (auth.uid() = userId);
-- Create indexes for better performance
CREATE INDEX idx_pokedex_entries_pokedex_number ON pokedex_entries(pokedexNumber);
CREATE INDEX idx_pokedex_entries_pokemon ON pokedex_entries(pokemon);
CREATE INDEX idx_pokedex_entries_box_placement_forms ON pokedex_entries(boxPlacementFormsBox, boxPlacementFormsRow, boxPlacementFormsColumn);
CREATE INDEX idx_pokedex_entries_box_placement ON pokedex_entries(boxPlacementBox, boxPlacementRow, boxPlacementColumn);
CREATE INDEX idx_catch_records_user_id ON catch_records(userId);
CREATE INDEX idx_catch_records_pokedex_entry_id ON catch_records(pokedexEntryId);
CREATE INDEX idx_catch_records_caught ON catch_records(caught);
CREATE INDEX idx_catch_records_in_home ON catch_records(inHome);
-- Create updated_at trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Add updated_at triggers
CREATE TRIGGER update_pokedex_entries_updated_at BEFORE UPDATE ON pokedex_entries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_catch_records_updated_at BEFORE UPDATE ON catch_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_pokedex_metadata_updated_at BEFORE UPDATE ON pokedex_metadata
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
@@ -0,0 +1,133 @@
-- Migration: Create PostgreSQL schema for Pokemon data
-- This replaces the MongoDB collections with proper PostgreSQL tables
-- Note: Using individual columns instead of composite types for simplicity
-- Pokemon entries table (replaces pokedexentries MongoDB collection)
CREATE TABLE pokedex_entries (
id BIGSERIAL PRIMARY KEY,
"pokedexNumber" INTEGER NOT NULL,
pokemon TEXT NOT NULL,
form TEXT,
"canGigantamax" BOOLEAN DEFAULT FALSE,
"regionToCatchIn" TEXT,
"gamesToCatchIn" TEXT[], -- Array of games
"regionToEvolveIn" TEXT,
"evolutionInformation" TEXT,
"catchInformation" TEXT[], -- Array of catch info
-- Box placement for forms view
"boxPlacementFormsBox" INTEGER,
"boxPlacementFormsRow" INTEGER,
"boxPlacementFormsColumn" INTEGER,
-- Box placement for no-forms view
"boxPlacementBox" INTEGER,
"boxPlacementRow" INTEGER,
"boxPlacementColumn" INTEGER,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
);
-- Add unique constraint to prevent duplicate pokemon forms
ALTER TABLE pokedex_entries ADD CONSTRAINT unique_pokemon_form
UNIQUE("pokedexNumber", form);
-- Catch records table (replaces catchrecords MongoDB collection)
CREATE TABLE catch_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
"pokedexEntryId" BIGINT NOT NULL REFERENCES pokedex_entries(id) ON DELETE CASCADE,
-- Catch status fields
"haveToEvolve" BOOLEAN DEFAULT FALSE,
caught BOOLEAN DEFAULT FALSE,
"inHome" BOOLEAN DEFAULT FALSE,
"hasGigantamaxed" BOOLEAN DEFAULT FALSE,
"personalNotes" TEXT,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
-- Ensure one record per user per pokemon entry
UNIQUE("userId", "pokedexEntryId")
);
-- Region-game mappings table for filtering
CREATE TABLE region_game_mappings (
id BIGSERIAL PRIMARY KEY,
region TEXT NOT NULL,
game TEXT NOT NULL,
UNIQUE(region, game)
);
-- Metadata table for migration tracking
CREATE TABLE metadata (
id BIGSERIAL PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
value TEXT,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
);
-- Create indexes for better performance
CREATE INDEX idx_pokedex_entries_pokedex_number ON pokedex_entries("pokedexNumber");
CREATE INDEX idx_pokedex_entries_pokemon ON pokedex_entries(pokemon);
CREATE INDEX idx_pokedex_entries_box_placement_forms ON pokedex_entries("boxPlacementFormsBox", "boxPlacementFormsRow", "boxPlacementFormsColumn");
CREATE INDEX idx_pokedex_entries_box_placement ON pokedex_entries("boxPlacementBox", "boxPlacementRow", "boxPlacementColumn");
CREATE INDEX idx_catch_records_user_id ON catch_records("userId");
CREATE INDEX idx_catch_records_pokedex_entry_id ON catch_records("pokedexEntryId");
CREATE INDEX idx_catch_records_caught ON catch_records(caught);
CREATE INDEX idx_catch_records_in_home ON catch_records("inHome");
CREATE INDEX idx_catch_records_user_caught ON catch_records("userId", caught);
-- Function to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW."updatedAt" = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers for updated_at
CREATE TRIGGER update_pokedex_entries_updated_at
BEFORE UPDATE ON pokedex_entries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_catch_records_updated_at
BEFORE UPDATE ON catch_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Enable RLS on all tables
ALTER TABLE pokedex_entries ENABLE ROW LEVEL SECURITY;
ALTER TABLE catch_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE region_game_mappings ENABLE ROW LEVEL SECURITY;
ALTER TABLE metadata ENABLE ROW LEVEL SECURITY;
-- RLS policies for pokedex_entries (public read)
CREATE POLICY "Anyone can view pokemon entries" ON pokedex_entries
FOR SELECT USING (true);
-- RLS policies for catch_records (user-specific)
CREATE POLICY "Users can view own catch records" ON catch_records
FOR SELECT USING (auth.uid() = "userId");
CREATE POLICY "Users can insert own catch records" ON catch_records
FOR INSERT WITH CHECK (auth.uid() = "userId");
CREATE POLICY "Users can update own catch records" ON catch_records
FOR UPDATE USING (auth.uid() = "userId");
CREATE POLICY "Users can delete own catch records" ON catch_records
FOR DELETE USING (auth.uid() = "userId");
-- RLS policies for region_game_mappings (public read)
CREATE POLICY "Anyone can view region game mappings" ON region_game_mappings
FOR SELECT USING (true);
-- RLS policies for metadata (public read)
CREATE POLICY "Anyone can view metadata" ON metadata
FOR SELECT USING (true);
@@ -1,54 +0,0 @@
-- Migration: Seed Pokemon data from TSV
-- This replaces the separate tsv-parser project and MongoDB seeding
-- Insert Pokemon data based on the TSV structure
-- Note: This is a sample of the data structure - you'll need to convert the full TSV
INSERT INTO pokedex_entries (
pokedex_number,
pokemon,
form,
can_gigantamax,
region_to_catch_in,
games_to_catch_in,
region_to_evolve_in,
evolution_information,
box_placement_forms_box,
box_placement_forms_row,
box_placement_forms_column,
box_placement_box,
box_placement_row,
box_placement_column
) VALUES
-- Sample data from the TSV (first few entries)
(1, 'Bulbasaur', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 1, 1, 1, 1),
(2, 'Ivysaur', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 2, 1, 1, 2),
(3, 'Venusaur', NULL, TRUE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 3, 1, 1, 3),
(3, 'Venusaur', 'Female', FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 4, NULL, NULL, NULL),
(4, 'Charmander', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 5, 1, 1, 4),
(5, 'Charmeleon', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 1, 6, 1, 1, 5),
(6, 'Charizard', NULL, TRUE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 2, 1, 1, 1, 6),
(7, 'Squirtle', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 2, 2, 1, 2, 1),
(8, 'Wartortle', NULL, FALSE, 'Kanto', ARRAY['Red','Blue','Yellow','LG: Pikachu','LG: Eevee'], NULL, NULL, 1, 2, 3, 1, 2, 2);
-- Insert region game mappings
INSERT INTO region_game_mappings (region, games) VALUES
('Kanto', ARRAY['Red', 'Blue', 'Yellow', 'LG: Pikachu', 'LG: Eevee']),
('Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
('Hoenn', ARRAY['Ruby', 'Sapphire', 'Emerald', 'Omega Ruby', 'Alpha Sapphire']),
('Sinnoh', ARRAY['Diamond', 'Pearl', 'Platinum', 'Brilliant Diamond', 'Shining Pearl']),
('Unova', ARRAY['Black', 'White', 'Black 2', 'White 2']),
('Kalos', ARRAY['X', 'Y']),
('Alola', ARRAY['Sun', 'Moon', 'Ultra Sun', 'Ultra Moon']),
('Galar', ARRAY['Sword', 'Shield']),
('Paldea', ARRAY['Scarlet', 'Violet']);
-- Insert some metadata
INSERT INTO pokedex_metadata (key, value) VALUES
('total_pokemon', '{"count": 1025}'),
('last_updated', '{"timestamp": "2025-01-26T00:00:00Z"}'),
('supported_generations', '{"generations": [1,2,3,4,5,6,7,8,9]}');
-- Note: This is a sample seed. For the full migration, we'll need to:
-- 1. Convert the entire TSV file to SQL inserts
-- 2. Or create a function to import from the TSV data directly
@@ -7,20 +7,20 @@ DELETE FROM pokedex_entries;
-- Insert full Pokemon dataset -- Insert full Pokemon dataset
INSERT INTO pokedex_entries ( INSERT INTO pokedex_entries (
pokedex_number, "pokedexNumber",
pokemon, pokemon,
form, form,
can_gigantamax, "canGigantamax",
region_to_catch_in, "regionToCatchIn",
games_to_catch_in, "gamesToCatchIn",
region_to_evolve_in, "regionToEvolveIn",
evolution_information, "evolutionInformation",
box_placement_forms_box, "boxPlacementFormsBox",
box_placement_forms_row, "boxPlacementFormsRow",
box_placement_forms_column, "boxPlacementFormsColumn",
box_placement_box, "boxPlacementBox",
box_placement_row, "boxPlacementRow",
box_placement_column "boxPlacementColumn"
) VALUES ) VALUES
(1, 'Bulbasaur', NULL, false, 'Kanto', ARRAY['Red', 'Blue', 'Yellow', 'LG: Pikachu', 'LG: Eevee'], NULL, NULL, 1, 1, 1, 1, 1, 1), (1, 'Bulbasaur', NULL, false, 'Kanto', ARRAY['Red', 'Blue', 'Yellow', 'LG: Pikachu', 'LG: Eevee'], NULL, NULL, 1, 1, 1, 1, 1, 1),
(2, 'Ivysaur', NULL, false, 'Kanto', ARRAY['Red', 'Blue', 'Yellow', 'LG: Pikachu', 'LG: Eevee'], NULL, NULL, 1, 1, 2, 1, 1, 2), (2, 'Ivysaur', NULL, false, 'Kanto', ARRAY['Red', 'Blue', 'Yellow', 'LG: Pikachu', 'LG: Eevee'], NULL, NULL, 1, 1, 2, 1, 1, 2),
@@ -344,7 +344,8 @@ INSERT INTO pokedex_entries (
(209, 'Snubbull', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 2, 6, 7, 5, 5), (209, 'Snubbull', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 2, 6, 7, 5, 5),
(210, 'Granbull', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 3, 1, 7, 5, 6), (210, 'Granbull', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 3, 1, 7, 5, 6),
(211, 'Qwilfish', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Route 32, Super Rod', 12, 3, 2, 8, 1, 1), (211, 'Qwilfish', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Route 32, Super Rod', 12, 3, 2, 8, 1, 1),
(211, 'Qwilfish', 'Hisuian', false, 'Hisui', ARRAY['PLA'], NULL, 'Obsidian Fieldlands: near Ramanas Island (Hisuian Form) (211, 'Qwilfish', 'Hisuian', false, 'Hisui', ARRAY['PLA'], NULL, 'Obsidian Fieldlands: near Ramanas Island (Hisuian Form)
Cobalt Coastlands: near Bathers'' Lagoon, near Hideaway Bay, near Tombolo Walk, near Sand''s Reach, Tranquility Cove, Islespy Shore and nearby (additional Alpha icon.png), Lunker''s Lair, near Seagrass Haven, near Firespit Island, massive mass outbreaks (Hisuian Form)', 12, 3, 3, null, null, null),
(212, 'Scizor', NULL, false, 'Kanto', ARRAY['Red', 'Yellow', 'LG: Pikachu'], 'Johto', 'It evolves from Scyther when traded while holding a Metal Coat.', 12, 3, 4, 8, 1, 2), (212, 'Scizor', NULL, false, 'Kanto', ARRAY['Red', 'Yellow', 'LG: Pikachu'], 'Johto', 'It evolves from Scyther when traded while holding a Metal Coat.', 12, 3, 4, 8, 1, 2),
(212, 'Scizor', 'Female', false, 'Kanto', ARRAY['Red', 'Yellow', 'LG: Pikachu'], 'Johto', 'It evolves from Scyther when traded while holding a Metal Coat.', 12, 3, 5, null, null, null), (212, 'Scizor', 'Female', false, 'Kanto', ARRAY['Red', 'Yellow', 'LG: Pikachu'], 'Johto', 'It evolves from Scyther when traded while holding a Metal Coat.', 12, 3, 5, null, null, null),
(213, 'Shuckle', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 3, 6, 8, 1, 3), (213, 'Shuckle', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 3, 6, 8, 1, 3),
@@ -352,8 +353,10 @@ INSERT INTO pokedex_entries (
(214, 'Heracross', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 4, 2, null, null, null), (214, 'Heracross', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, NULL, 12, 4, 2, null, null, null),
(215, 'Sneasel', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Ice Path, b3f, 6pm-4am', 12, 4, 3, 8, 1, 5), (215, 'Sneasel', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Ice Path, b3f, 6pm-4am', 12, 4, 3, 8, 1, 5),
(215, 'Sneasel', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Ice Path, b3f, 6pm-4am', 12, 4, 4, null, null, null), (215, 'Sneasel', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Ice Path, b3f, 6pm-4am', 12, 4, 4, null, null, null),
(215, 'Sneasel', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HG', 'SS'], NULL, 'Ice Path, b3f, 6pm-4am', 12, 4, 4, null, null, null), (215, 'Sneasel', 'Hisuian', false, 'Hisui', ARRAY['PLA'], NULL, 'Coronet Highlands: near Celestica Trail, near Primeval Grotto, massive mass outbreaks (Hisuian Form)
(215, 'Sneasel', 'Hisuian', false, 'Hisui', ARRAY['PLA'], NULL, 'Coronet Highlands: near Celestica Trail, near Primeval Grotto, massive mass outbreaks (Hisuian Form) Alabaster Icelands: near Avalugg''s Legacy (additional Alpha icon.png), Glacier Terrace, near Pearl Settlement (Hisuian Form)', 12, 4, 5, null, null, null),
(215, 'Sneasel', 'Female-Hisuian', false, 'Hisui', ARRAY['PLA'], NULL, 'Coronet Highlands: near Celestica Trail, near Primeval Grotto, massive mass outbreaks (Hisuian Form)
Alabaster Icelands: near Avalugg''s Legacy (additional Alpha icon.png), Glacier Terrace, near Pearl Settlement (Hisuian Form)', 12, 4, 6, null, null, null),
(216, 'Teddiursa', NULL, false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, 'Dark Cave, Blackthorn City Entrance, 4am-10am', 12, 5, 1, 8, 1, 6), (216, 'Teddiursa', NULL, false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, 'Dark Cave, Blackthorn City Entrance, 4am-10am', 12, 5, 1, 8, 1, 6),
(217, 'Ursaring', NULL, false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, NULL, 12, 5, 2, 8, 2, 1), (217, 'Ursaring', NULL, false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, NULL, 12, 5, 2, 8, 2, 1),
(217, 'Ursaring', 'Female', false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, NULL, 12, 5, 3, null, null, null), (217, 'Ursaring', 'Female', false, 'Johto', ARRAY['Gold', 'Crystal', 'SS'], NULL, NULL, 12, 5, 3, null, null, null),
@@ -765,7 +768,9 @@ INSERT INTO pokedex_entries (
(549, 'Lilligant', 'Hisuian', false, 'Unova', ARRAY['White', 'White2'], 'Hisui', 'In Hisui, Petilil evolves into Hisuian Lilligant when exposed to a Sun Stone.', 27, 5, 5, null, null, null), (549, 'Lilligant', 'Hisuian', false, 'Unova', ARRAY['White', 'White2'], 'Hisui', 'In Hisui, Petilil evolves into Hisuian Lilligant when exposed to a Sun Stone.', 27, 5, 5, null, null, null),
(550, 'Basculin', 'Red-striped', false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Routes 1, 3, 6, 11, 14, Striaton City, Wellspring Cave, Pinwheel Forest, Dragonspiral Tower, Challenger''s Cave, Victory Road, Village Bridge, Giant Chasm, Abundant Shrine, Lostlorn Forest (Surfing or fishing) (Red-Striped Form) (Rippling water) (Blue-Striped Form)', 27, 5, 6, 19, 2, 4), (550, 'Basculin', 'Red-striped', false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Routes 1, 3, 6, 11, 14, Striaton City, Wellspring Cave, Pinwheel Forest, Dragonspiral Tower, Challenger''s Cave, Victory Road, Village Bridge, Giant Chasm, Abundant Shrine, Lostlorn Forest (Surfing or fishing) (Red-Striped Form) (Rippling water) (Blue-Striped Form)', 27, 5, 6, 19, 2, 4),
(550, 'Basculin', 'Blue-striped', false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Routes 1, 3, 6, 11, 14, Striaton City, Wellspring Cave, Pinwheel Forest, Dragonspiral Tower, Challenger''s Cave, Victory Road, Village Bridge, Giant Chasm, Abundant Shrine, Lostlorn Forest (Surfing or fishing) (Red-Striped Form) (Rippling water) (Blue-Striped Form)', 28, 1, 1, null, null, null), (550, 'Basculin', 'Blue-striped', false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Routes 1, 3, 6, 11, 14, Striaton City, Wellspring Cave, Pinwheel Forest, Dragonspiral Tower, Challenger''s Cave, Victory Road, Village Bridge, Giant Chasm, Abundant Shrine, Lostlorn Forest (Surfing or fishing) (Red-Striped Form) (Rippling water) (Blue-Striped Form)', 28, 1, 1, null, null, null),
(549, 'Lilligant', 'Hisuian', false, 'Unova', ARRAY['White', 'White2'], 'Hisui', 'In Hisui, Petilil evolves into Hisuian Lilligant when exposed to a Sun Stone.', 27, 5, 5, null, null, null), (550, 'Basculin', 'White-striped', false, 'Hisui', ARRAY['PLA'], NULL, 'Cobalt Coastlands: Tranquility Cove, near Islespy Shore, near Firespit Island, massive mass outbreaks (White-Striped Form)
Coronet Highlands: Fabled Spring (White-Striped Form)
Alabaster Icelands: near Avalugg''s Legacy, Heart''s Crag, Lake Acuity, near Pearl Settlement (White-Striped Form)', 28, 1, 2, null, null, null),
(551, 'Sandile', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Route 4, Desert Resort, Relic Castle', 28, 1, 3, 19, 2, 5), (551, 'Sandile', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Route 4, Desert Resort, Relic Castle', 28, 1, 3, 19, 2, 5),
(552, 'Krokorok', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Evolves from Sandile at level 29', 28, 1, 4, 19, 2, 6), (552, 'Krokorok', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Evolves from Sandile at level 29', 28, 1, 4, 19, 2, 6),
(553, 'Krookodile', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Evolves from Krokorok at level 40', 28, 1, 5, 19, 3, 1), (553, 'Krookodile', NULL, false, 'Unova', ARRAY['Black', 'White', 'Black2', 'White2'], NULL, 'Evolves from Krokorok at level 40', 28, 1, 5, 19, 3, 1),
@@ -1414,9 +1419,51 @@ INSERT INTO pokedex_entries (
(1025, 'Pecharunt', NULL, false, 'Paldea', ARRAY['Scarlet', 'Violet'], NULL, NULL, 51, 2, 6, 35, 1, 4); (1025, 'Pecharunt', NULL, false, 'Paldea', ARRAY['Scarlet', 'Violet'], NULL, NULL, 51, 2, 6, 35, 1, 4);
-- Update metadata -- Update metadata
(1023, 'Iron Crown', NULL, false, 'Paldea', ARRAY['Violet'], NULL, NULL, 51, 2, 4, 35, 1, 2), UPDATE metadata
(1024, 'Terapagos', NULL, false, 'Paldea', ARRAY['Scarlet', 'Violet'], NULL, NULL, 51, 2, 5, 35, 1, 3), SET value = '{"count": 1390}', "updatedAt" = NOW()
WHERE key = 'total_pokemon'; WHERE key = 'total_pokemon';
-- Insert region-game mappings if not already present
INSERT INTO region_game_mappings (region, game) VALUES
('Kanto', 'Red'),
('Kanto', 'Blue'),
('Kanto', 'Yellow'),
('Kanto', 'LG: Pikachu'),
('Kanto', 'LG: Eevee'),
('Johto', 'Gold'),
('Johto', 'Silver'),
('Johto', 'Crystal'),
('Hoenn', 'Ruby'),
('Hoenn', 'Sapphire'),
('Hoenn', 'Emerald'),
('Sinnoh', 'Diamond'),
('Sinnoh', 'Pearl'),
('Sinnoh', 'Platinum'),
('Unova', 'Black'),
('Unova', 'White'),
('Unova', 'Black 2'),
('Unova', 'White 2'),
('Kalos', 'X'),
('Kalos', 'Y'),
('Alola', 'Sun'),
('Alola', 'Moon'),
('Alola', 'Ultra Sun'),
('Alola', 'Ultra Moon'),
('Galar', 'Sword'),
('Galar', 'Shield'),
('Paldea', 'Scarlet'),
('Paldea', 'Violet')
ON CONFLICT (region, game) DO NOTHING;
-- Insert metadata
INSERT INTO metadata (key, value) VALUES
('migration_version', '1.0'),
('data_source', 'tsv-parser'),
('last_updated', NOW()::TEXT),
('total_pokemon', '{"count": 1390}')
ON CONFLICT (key) DO UPDATE SET
value = EXCLUDED.value,
"updatedAt" = NOW();
-- Add success message -- Add success message
SELECT 'Successfully loaded 1390 Pokemon entries' as message; SELECT 'Successfully loaded 1390 Pokemon entries' as message;
-14
View File
@@ -1,14 +0,0 @@
-- Initial seed data for local development
-- This file is referenced in config.toml and will be loaded during db reset
-- Note: RLS is already enabled on auth.users by default in Supabase
-- No need to modify system tables here
-- Add any application-specific seed data here
-- For now, this file is minimal since we use MongoDB for app data
-- Example: If we had a profiles table, we could seed it here
-- INSERT INTO profiles (id, username) VALUES
-- ('test-user-id', 'testuser');
SELECT 'Local Supabase setup complete' as message;
+7 -1
View File
@@ -17,7 +17,13 @@
"virtual:pwa-assets" "virtual:pwa-assets"
] ]
}, },
"include": ["node_modules/vite-plugin-pwa/client.d.ts", "node_modules/**/*.d.ts"] "include": [
"src/**/*.ts",
"src/**/*.svelte",
"node_modules/vite-plugin-pwa/client.d.ts",
"node_modules/**/*.d.ts"
],
"exclude": [".svelte-kit/**/*", "node_modules/**/*"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes