Files
chadebebe/lib/db/index.ts

222 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';