222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
import Database from 'better-sqlite3';
|
||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||
import * as schema from './schema';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
|
||
// Lazy initialization to avoid database access during build
|
||
let _db: ReturnType<typeof drizzle> | null = null;
|
||
let _sqlite: Database.Database | null = null;
|
||
|
||
function getDb() {
|
||
if (!_db) {
|
||
// Ensure data directories exist
|
||
const dataDir = path.join(process.cwd(), 'data');
|
||
const dbDir = path.join(dataDir, 'db');
|
||
const uploadsDir = path.join(dataDir, 'uploads');
|
||
|
||
if (!fs.existsSync(dbDir)) {
|
||
fs.mkdirSync(dbDir, { recursive: true });
|
||
}
|
||
|
||
if (!fs.existsSync(uploadsDir)) {
|
||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||
}
|
||
|
||
// Database file path
|
||
const dbPath = path.join(dbDir, 'wishlist.db');
|
||
|
||
// Create SQLite database connection
|
||
_sqlite = new Database(dbPath);
|
||
_sqlite.pragma('journal_mode = WAL'); // Better concurrency
|
||
|
||
// Create Drizzle instance
|
||
_db = drizzle(_sqlite, { schema });
|
||
}
|
||
return _db;
|
||
}
|
||
|
||
// Export db as a getter
|
||
export const db = new Proxy({} as ReturnType<typeof drizzle>, {
|
||
get(target, prop) {
|
||
return getDb()[prop as keyof ReturnType<typeof drizzle>];
|
||
}
|
||
});
|
||
|
||
// Initialize database (create tables and seed if needed)
|
||
export async function initializeDatabase() {
|
||
try {
|
||
// Ensure database is initialized
|
||
const sqlite = _sqlite || getDb() && _sqlite;
|
||
if (!sqlite) throw new Error('Failed to initialize database');
|
||
|
||
// Create wishlists table
|
||
sqlite.exec(`
|
||
CREATE TABLE IF NOT EXISTS wishlists (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
name TEXT NOT NULL,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
description TEXT,
|
||
preferences TEXT,
|
||
image_url TEXT,
|
||
notes TEXT,
|
||
is_public INTEGER DEFAULT 0 NOT NULL,
|
||
sort_order INTEGER DEFAULT 0 NOT NULL,
|
||
created_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
updated_at INTEGER DEFAULT (unixepoch()) NOT NULL
|
||
)
|
||
`);
|
||
|
||
// Create wishlist_items table
|
||
sqlite.exec(`
|
||
CREATE TABLE IF NOT EXISTS wishlist_items (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
wishlist_id TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
price REAL,
|
||
currency TEXT DEFAULT 'USD' NOT NULL,
|
||
quantity INTEGER DEFAULT 1 NOT NULL,
|
||
images TEXT,
|
||
purchase_urls TEXT,
|
||
notes TEXT,
|
||
is_archived INTEGER DEFAULT 0 NOT NULL,
|
||
claimed_by_name TEXT,
|
||
claimed_by_note TEXT,
|
||
claimed_by_token TEXT UNIQUE,
|
||
claimed_at INTEGER,
|
||
is_purchased INTEGER DEFAULT 0 NOT NULL,
|
||
sort_order INTEGER DEFAULT 0 NOT NULL,
|
||
created_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
updated_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
FOREIGN KEY (wishlist_id) REFERENCES wishlists(id) ON DELETE CASCADE
|
||
)
|
||
`);
|
||
|
||
// Create settings table
|
||
sqlite.exec(`
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
key TEXT NOT NULL UNIQUE,
|
||
value TEXT NOT NULL,
|
||
updated_at INTEGER DEFAULT (unixepoch()) NOT NULL
|
||
)
|
||
`);
|
||
|
||
// Create guests table
|
||
sqlite.exec(`
|
||
CREATE TABLE IF NOT EXISTS guests (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
name TEXT NOT NULL,
|
||
created_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
updated_at INTEGER DEFAULT (unixepoch()) NOT NULL
|
||
)
|
||
`);
|
||
|
||
// Create item_claims table
|
||
sqlite.exec(`
|
||
CREATE TABLE IF NOT EXISTS item_claims (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
item_id TEXT NOT NULL,
|
||
guest_id TEXT NOT NULL,
|
||
quantity INTEGER DEFAULT 1 NOT NULL,
|
||
note TEXT,
|
||
is_purchased INTEGER DEFAULT 0 NOT NULL,
|
||
claimed_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
updated_at INTEGER DEFAULT (unixepoch()) NOT NULL,
|
||
FOREIGN KEY (item_id) REFERENCES wishlist_items(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (guest_id) REFERENCES guests(id) ON DELETE CASCADE,
|
||
UNIQUE(item_id, guest_id)
|
||
)
|
||
`);
|
||
sqlite.exec('CREATE INDEX IF NOT EXISTS idx_item_claims_item ON item_claims(item_id)');
|
||
sqlite.exec('CREATE INDEX IF NOT EXISTS idx_item_claims_guest ON item_claims(guest_id)');
|
||
|
||
// Run migrations for existing databases
|
||
try {
|
||
const columns = sqlite.pragma('table_info(wishlists)') as Array<{ name: string }>;
|
||
|
||
// Add image_url column if it doesn't exist
|
||
const hasImageUrl = columns.some((col) => col.name === 'image_url');
|
||
if (!hasImageUrl) {
|
||
sqlite.exec('ALTER TABLE wishlists ADD COLUMN image_url TEXT');
|
||
console.log('✅ Added image_url column to wishlists table');
|
||
}
|
||
|
||
// Add sort_order column if it doesn't exist
|
||
const hasSortOrder = columns.some((col) => col.name === 'sort_order');
|
||
if (!hasSortOrder) {
|
||
sqlite.exec('ALTER TABLE wishlists ADD COLUMN sort_order INTEGER DEFAULT 0 NOT NULL');
|
||
console.log('✅ Added sort_order column to wishlists table');
|
||
}
|
||
|
||
// Add preferences column if it doesn't exist
|
||
const hasPreferences = columns.some((col) => col.name === 'preferences');
|
||
if (!hasPreferences) {
|
||
sqlite.exec('ALTER TABLE wishlists ADD COLUMN preferences TEXT');
|
||
console.log('✅ Added preferences column to wishlists table');
|
||
}
|
||
|
||
const itemColumns = sqlite.pragma('table_info(wishlist_items)') as Array<{ name: string }>;
|
||
|
||
const hasClaimedByGuestId = itemColumns.some((col) => col.name === 'claimed_by_guest_id');
|
||
if (!hasClaimedByGuestId) {
|
||
sqlite.exec('ALTER TABLE wishlist_items ADD COLUMN claimed_by_guest_id TEXT REFERENCES guests(id) ON DELETE SET NULL');
|
||
console.log('✅ Added claimed_by_guest_id column to wishlist_items');
|
||
}
|
||
|
||
const hasClaimedByName = itemColumns.some((col) => col.name === 'claimed_by_name');
|
||
const hasClaimedByToken = itemColumns.some((col) => col.name === 'claimed_by_token');
|
||
if (hasClaimedByName || hasClaimedByToken) {
|
||
if (hasClaimedByName) {
|
||
sqlite.exec('UPDATE wishlist_items SET claimed_by_name = NULL');
|
||
}
|
||
if (hasClaimedByToken) {
|
||
sqlite.exec('UPDATE wishlist_items SET claimed_by_token = NULL');
|
||
}
|
||
console.log('ℹ️ Legacy claim columns nullified (claimed_by_name / claimed_by_token)');
|
||
}
|
||
|
||
// Move any existing inline claims into item_claims (forward-migration), then nullify the inline columns
|
||
const itemColsAfter = sqlite.pragma('table_info(wishlist_items)') as Array<{ name: string }>;
|
||
const stillHasClaimedByGuestId = itemColsAfter.some((c) => c.name === 'claimed_by_guest_id');
|
||
if (stillHasClaimedByGuestId) {
|
||
sqlite.exec(`
|
||
INSERT INTO item_claims (id, item_id, guest_id, quantity, note, is_purchased, claimed_at, updated_at)
|
||
SELECT
|
||
lower(hex(randomblob(16))),
|
||
wi.id,
|
||
wi.claimed_by_guest_id,
|
||
1,
|
||
wi.claimed_by_note,
|
||
wi.is_purchased,
|
||
COALESCE(wi.claimed_at, unixepoch()),
|
||
unixepoch()
|
||
FROM wishlist_items wi
|
||
WHERE wi.claimed_by_guest_id IS NOT NULL
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM item_claims ic
|
||
WHERE ic.item_id = wi.id AND ic.guest_id = wi.claimed_by_guest_id
|
||
)
|
||
`);
|
||
sqlite.exec('UPDATE wishlist_items SET claimed_by_guest_id = NULL, claimed_by_note = NULL, claimed_at = NULL, is_purchased = 0');
|
||
console.log('ℹ️ Migrated inline claims into item_claims and cleared inline claim columns');
|
||
}
|
||
} catch (migrationError) {
|
||
console.log('Migration already applied or not needed');
|
||
}
|
||
|
||
// Auto-seed database if empty
|
||
const { seedDatabase } = await import('./seed');
|
||
await seedDatabase();
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('❌ Database initialization failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Export schema for use in other files
|
||
export * from './schema';
|