From 44d4dcbf7e1bcbaadb7649c601b56b95fff44b05 Mon Sep 17 00:00:00 2001 From: belisards Date: Sun, 3 May 2026 16:12:24 -0300 Subject: [PATCH] feat(db): add guests table and claimed_by_guest_id column --- lib/db/index.ts | 30 ++++++++++++++++++++++++++++++ lib/db/schema.ts | 30 +++++++++++------------------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/lib/db/index.ts b/lib/db/index.ts index 7bc519e..ae1c3b6 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -103,6 +103,16 @@ export async function initializeDatabase() { ) `); + // 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 + ) + `); + // Run migrations for existing databases try { const columns = sqlite.pragma('table_info(wishlists)') as Array<{ name: string }>; @@ -127,6 +137,26 @@ export async function initializeDatabase() { 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)'); + } } catch (migrationError) { console.log('Migration already applied or not needed'); } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 4a852ea..1a40983 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -2,22 +2,12 @@ import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'; import { createId } from '@paralleldrive/cuid2'; import { sql } from 'drizzle-orm'; -/** - * Database schema for family wishlist app - * - * Simplified for self-hosted family use: - * - No Settings table (use env vars instead) - * - Image URLs only (no upload handling in MVP) - * - Simplified purchase URLs (no usage tracking) - */ - -// Wishlists table export const wishlists = sqliteTable('wishlists', { id: text('id').primaryKey().$defaultFn(() => createId()), name: text('name').notNull(), slug: text('slug').notNull().unique(), description: text('description'), - preferences: text('preferences'), // General interests/likes section + preferences: text('preferences'), imageUrl: text('image_url'), isPublic: integer('is_public', { mode: 'boolean' }).notNull().default(false), sortOrder: integer('sort_order').notNull().default(0), @@ -25,7 +15,13 @@ export const wishlists = sqliteTable('wishlists', { updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), }); -// Wishlist items table +export const guests = sqliteTable('guests', { + id: text('id').primaryKey().$defaultFn(() => createId()), + name: text('name').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), +}); + export const wishlistItems = sqliteTable('wishlist_items', { id: text('id').primaryKey().$defaultFn(() => createId()), wishlistId: text('wishlist_id').notNull().references(() => wishlists.id, { onDelete: 'cascade' }), @@ -34,18 +30,15 @@ export const wishlistItems = sqliteTable('wishlist_items', { price: real('price'), currency: text('currency').notNull().default('USD'), quantity: integer('quantity').notNull().default(1), - imageUrl: text('images'), // Stored as 'images' in DB but exposed as imageUrl + imageUrl: text('images'), purchaseUrls: text('purchase_urls', { mode: 'json' }).$type>(), - isArchived: integer('is_archived', { mode: 'boolean' }).notNull().default(false), - // Claim information - claimedByName: text('claimed_by_name'), + claimedByGuestId: text('claimed_by_guest_id').references(() => guests.id, { onDelete: 'set null' }), claimedByNote: text('claimed_by_note'), - claimedByToken: text('claimed_by_token').unique(), claimedAt: integer('claimed_at', { mode: 'timestamp' }), isPurchased: integer('is_purchased', { mode: 'boolean' }).notNull().default(false), @@ -54,7 +47,6 @@ export const wishlistItems = sqliteTable('wishlist_items', { updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), }); -// Settings table export const settings = sqliteTable('settings', { id: text('id').primaryKey().$defaultFn(() => createId()), key: text('key').notNull().unique(), @@ -62,7 +54,7 @@ export const settings = sqliteTable('settings', { updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`), }); -// Type exports export type Wishlist = typeof wishlists.$inferSelect; export type WishlistItem = typeof wishlistItems.$inferSelect; +export type Guest = typeof guests.$inferSelect; export type Setting = typeof settings.$inferSelect;