diff --git a/api/migrate.ts b/api/migrate.ts index da38ad7..6daaea4 100644 --- a/api/migrate.ts +++ b/api/migrate.ts @@ -1,9 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { neon } from '@neondatabase/serverless'; +import { Pool } from 'pg'; import cardsData from '../src/data/cards.json'; -// Initialize Neon client -const sql = neon(process.env.DATABASE_URL!); +// Create PostgreSQL connection pool +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}); interface JsonCard { id: number; @@ -49,12 +52,14 @@ export default async function handler(req: NextRequest) { }); } + const client = await pool.connect(); + try { console.log('Starting card migration to Neon database...'); // Check if cards already exist to prevent duplicate migration - const existingCards = await sql`SELECT COUNT(*) as count FROM cards`; - const cardCount = existingCards[0]?.count || 0; + const existingCards = await client.query('SELECT COUNT(*) as count FROM cards'); + const cardCount = existingCards.rows[0]?.count || 0; if (cardCount > 0) { return new NextResponse(JSON.stringify({ @@ -75,7 +80,7 @@ export default async function handler(req: NextRequest) { for (const card of cards) { try { - await sql` + await client.query(` INSERT INTO cards ( name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, flavor_text, power, toughness, loyalty, @@ -83,39 +88,42 @@ export default async function handler(req: NextRequest) { market_price, low_price, high_price, price_last_updated, ocr_confidence, ocr_raw_text, scryfall_id, tcg_player_id, verified, created_at, updated_at ) VALUES ( - ${card.name}, - ${card.set_name || null}, - ${card.set_code || null}, - ${card.card_number || null}, - ${card.rarity || null}, - ${card.game}, - ${card.mana_cost || null}, - ${card.cmc || null}, - ${card.card_type || null}, - ${card.colors ? JSON.stringify(card.colors) : null}, - ${card.oracle_text || null}, - ${card.flavor_text || null}, - ${card.power || null}, - ${card.toughness || null}, - ${card.loyalty || null}, - ${card.artist || null}, - ${card.image_url || null}, - ${card.stock_image_url || null}, - ${card.artwork_crop_coords ? JSON.stringify(card.artwork_crop_coords) : null}, - ${card.current_price || null}, - ${card.market_price || null}, - ${card.low_price || null}, - ${card.high_price || null}, - ${card.price_last_updated ? new Date(card.price_last_updated) : null}, - ${card.ocr_confidence || null}, - ${card.ocr_raw_text || null}, - ${card.scryfall_id || null}, - ${card.tcg_player_id || null}, - ${Boolean(card.verified)}, - ${card.created_at ? new Date(card.created_at) : new Date()}, - ${card.updated_at ? new Date(card.updated_at) : new Date()} + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, + $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31 ) - `; + `, [ + card.name, + card.set_name || null, + card.set_code || null, + card.card_number || null, + card.rarity || null, + card.game, + card.mana_cost || null, + card.cmc || null, + card.card_type || null, + card.colors ? JSON.stringify(card.colors) : null, + card.oracle_text || null, + card.flavor_text || null, + card.power || null, + card.toughness || null, + card.loyalty || null, + card.artist || null, + card.image_url || null, + card.stock_image_url || null, + card.artwork_crop_coords ? JSON.stringify(card.artwork_crop_coords) : null, + card.current_price || null, + card.market_price || null, + card.low_price || null, + card.high_price || null, + card.price_last_updated ? new Date(card.price_last_updated) : null, + card.ocr_confidence || null, + card.ocr_raw_text || null, + card.scryfall_id || null, + card.tcg_player_id || null, + Boolean(card.verified), + card.created_at ? new Date(card.created_at) : new Date(), + card.updated_at ? new Date(card.updated_at) : new Date() + ]); migratedCount++; } catch (error) { const errorMsg = `Error migrating card ${card.name}: ${(error as Error).message}`; @@ -125,8 +133,8 @@ export default async function handler(req: NextRequest) { } // Get final count to verify - const finalCount = await sql`SELECT COUNT(*) as count FROM cards`; - const finalCardCount = finalCount[0]?.count || 0; + const finalCount = await client.query('SELECT COUNT(*) as count FROM cards'); + const finalCardCount = finalCount.rows[0]?.count || 0; console.log(`✅ Successfully migrated ${migratedCount} cards to Neon database!`); @@ -151,5 +159,7 @@ export default async function handler(req: NextRequest) { status: 500, headers: { 'Content-Type': 'application/json' }, }); + } finally { + client.release(); } } \ No newline at end of file diff --git a/api/v1/cards/[id].ts b/api/v1/cards/[id].ts index 7327d3e..598d1b9 100644 --- a/api/v1/cards/[id].ts +++ b/api/v1/cards/[id].ts @@ -1,8 +1,11 @@ import { NextRequest, NextResponse } from 'next/server' -import { neon } from '@neondatabase/serverless' +import { Pool } from 'pg' -// Initialize Neon client -const sql = neon(process.env.DATABASE_URL!) +// Create PostgreSQL connection pool +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}) interface Card { id: number @@ -70,34 +73,39 @@ export default async function handler(req: NextRequest) { } // Query the database for the specific card - const result = await sql`SELECT * FROM cards WHERE id = ${parseInt(cardId)}` + const client = await pool.connect() + try { + const result = await client.query('SELECT * FROM cards WHERE id = $1', [parseInt(cardId)]) - if (result.length === 0) { - return new NextResponse(JSON.stringify({ error: 'Card not found' }), { - status: 404, + if (result.rows.length === 0) { + return new NextResponse(JSON.stringify({ error: 'Card not found' }), { + status: 404, + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }) + } + + // Transform the result to match expected format + const card = { + ...result.rows[0], + colors: result.rows[0].colors ? (typeof result.rows[0].colors === 'string' ? JSON.parse(result.rows[0].colors) : result.rows[0].colors) : null, + artwork_crop_coords: result.rows[0].artwork_crop_coords ? + (typeof result.rows[0].artwork_crop_coords === 'string' ? JSON.parse(result.rows[0].artwork_crop_coords) : result.rows[0].artwork_crop_coords) : null + } + + return new NextResponse(JSON.stringify(card), { + status: 200, headers: { ...headers, 'Content-Type': 'application/json', }, }) + } finally { + client.release() } - // Transform the result to match expected format - const card = { - ...result[0], - colors: result[0].colors ? (typeof result[0].colors === 'string' ? JSON.parse(result[0].colors) : result[0].colors) : null, - artwork_crop_coords: result[0].artwork_crop_coords ? - (typeof result[0].artwork_crop_coords === 'string' ? JSON.parse(result[0].artwork_crop_coords) : result[0].artwork_crop_coords) : null - } - - return new NextResponse(JSON.stringify(card), { - status: 200, - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - }) - } catch (error) { console.error('API Error:', error) return new NextResponse(JSON.stringify({ diff --git a/api/v1/cards/index.ts b/api/v1/cards/index.ts index e87e256..14b431b 100644 --- a/api/v1/cards/index.ts +++ b/api/v1/cards/index.ts @@ -1,8 +1,11 @@ import { NextRequest, NextResponse } from 'next/server' -import { neon } from '@neondatabase/serverless' +import { Pool } from 'pg' -// Initialize Neon client -const sql = neon(process.env.DATABASE_URL!) +// Create PostgreSQL connection pool +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +}) interface Card { id: number @@ -54,57 +57,65 @@ export default async function handler(req: NextRequest) { const game = searchParams.get('game') const set_name = searchParams.get('set_name') - // Build the query dynamically using template literals + // Build the query dynamically let baseQuery = 'SELECT * FROM cards WHERE 1=1' - let queryConditions: string[] = [] - let queryValues: any[] = [] + let queryParams: any[] = [] + let paramIndex = 1 if (game) { - queryConditions.push(`game = '${game.replace(/'/g, "''")}'`) + baseQuery += ` AND game = $${paramIndex}` + queryParams.push(game) + paramIndex++ } if (set_name) { - queryConditions.push(`set_name = '${set_name.replace(/'/g, "''")}'`) + baseQuery += ` AND set_name = $${paramIndex}` + queryParams.push(set_name) + paramIndex++ } if (search) { - const escapedSearch = search.replace(/'/g, "''") - queryConditions.push(`(name ILIKE '%${escapedSearch}%' OR oracle_text ILIKE '%${escapedSearch}%' OR card_type ILIKE '%${escapedSearch}%')`) - } - - // Combine conditions - if (queryConditions.length > 0) { - baseQuery += ' AND ' + queryConditions.join(' AND ') + baseQuery += ` AND (name ILIKE $${paramIndex} OR oracle_text ILIKE $${paramIndex} OR card_type ILIKE $${paramIndex})` + queryParams.push(`%${search}%`) + paramIndex++ } baseQuery += ' ORDER BY name' if (limit > 0) { - baseQuery += ` LIMIT ${limit}` + baseQuery += ` LIMIT $${paramIndex}` + queryParams.push(limit) + paramIndex++ } if (skip > 0) { - baseQuery += ` OFFSET ${skip}` + baseQuery += ` OFFSET $${paramIndex}` + queryParams.push(skip) } - // Execute the query using unsafe (for dynamic queries) - const result = await sql.unsafe(baseQuery) as unknown as any[] + // Execute the query + const client = await pool.connect() + try { + const result = await client.query(baseQuery, queryParams) + + // Transform the result to match expected format + const cards = result.rows.map((row: any) => ({ + ...row, + colors: row.colors ? (typeof row.colors === 'string' ? JSON.parse(row.colors) : row.colors) : null, + artwork_crop_coords: row.artwork_crop_coords ? + (typeof row.artwork_crop_coords === 'string' ? JSON.parse(row.artwork_crop_coords) : row.artwork_crop_coords) : null + })) - // Transform the result to match expected format - const cards = result.map((row: any) => ({ - ...row, - colors: row.colors ? (typeof row.colors === 'string' ? JSON.parse(row.colors) : row.colors) : null, - artwork_crop_coords: row.artwork_crop_coords ? - (typeof row.artwork_crop_coords === 'string' ? JSON.parse(row.artwork_crop_coords) : row.artwork_crop_coords) : null - })) - - return new NextResponse(JSON.stringify(cards), { - status: 200, - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - }) + return new NextResponse(JSON.stringify(cards), { + status: 200, + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }) + } finally { + client.release() + } } catch (error) { console.error('API Error:', error) diff --git a/package-lock.json b/package-lock.json index d52d259..5d485ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "@neondatabase/serverless": "^0.10.4", "@stackframe/stack": "^2.8.22", "@tanstack/react-query": "^5.83.0", "@testing-library/dom": "^10.4.0", @@ -17,6 +16,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.126", + "@types/pg": "^8.15.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-router-dom": "^5.3.3", @@ -24,6 +24,7 @@ "@vercel/blob": "^1.1.1", "@vercel/speed-insights": "^1.2.0", "axios": "^1.10.0", + "pg": "^8.16.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0", @@ -3903,15 +3904,6 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, - "node_modules/@neondatabase/serverless": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz", - "integrity": "sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==", - "license": "MIT", - "dependencies": { - "@types/pg": "8.11.6" - } - }, "node_modules/@next/env": { "version": "15.4.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.2.tgz", @@ -6660,14 +6652,14 @@ "license": "MIT" }, "node_modules/@types/pg": { - "version": "8.11.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", - "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", "license": "MIT", "dependencies": { "@types/node": "*", "pg-protocol": "*", - "pg-types": "^4.0.1" + "pg-types": "^2.2.0" } }, "node_modules/@types/prettier": { @@ -15685,6 +15677,46 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -15694,13 +15726,13 @@ "node": ">=4.0.0" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "license": "ISC", - "engines": { - "node": ">=4" + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" } }, "node_modules/pg-protocol": { @@ -15710,21 +15742,28 @@ "license": "MIT" }, "node_modules/pg-types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", - "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", "dependencies": { "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">=10" + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" } }, "node_modules/picocolors": { @@ -17161,50 +17200,44 @@ "license": "MIT" }, "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=4" } }, "node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, "node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, "node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -19193,6 +19226,15 @@ "wbuf": "^1.7.3" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -21737,6 +21779,15 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 40cde32..7dc3a93 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,9 @@ "version": "0.1.0", "private": true, "engines": { - "node": "20.x" + "node": "18.x" }, "dependencies": { - "@neondatabase/serverless": "^0.10.4", "@stackframe/stack": "^2.8.22", "@tanstack/react-query": "^5.83.0", "@testing-library/dom": "^10.4.0", @@ -15,6 +14,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.126", + "@types/pg": "^8.15.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-router-dom": "^5.3.3", @@ -22,6 +22,7 @@ "@vercel/blob": "^1.1.1", "@vercel/speed-insights": "^1.2.0", "axios": "^1.10.0", + "pg": "^8.16.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.7.0",