feat(db): introduce item_claims for quantity-aware claims; supersede inline claim columns
This commit is contained in:
@@ -113,6 +113,25 @@ export async function initializeDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// 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
|
// Run migrations for existing databases
|
||||||
try {
|
try {
|
||||||
const columns = sqlite.pragma('table_info(wishlists)') as Array<{ name: string }>;
|
const columns = sqlite.pragma('table_info(wishlists)') as Array<{ name: string }>;
|
||||||
@@ -157,6 +176,32 @@ export async function initializeDatabase() {
|
|||||||
}
|
}
|
||||||
console.log('ℹ️ Legacy claim columns nullified (claimed_by_name / claimed_by_token)');
|
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) {
|
} catch (migrationError) {
|
||||||
console.log('Migration already applied or not needed');
|
console.log('Migration already applied or not needed');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
|
import { sqliteTable, text, integer, real, unique } from 'drizzle-orm/sqlite-core';
|
||||||
import { createId } from '@paralleldrive/cuid2';
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
@@ -36,17 +36,29 @@ export const wishlistItems = sqliteTable('wishlist_items', {
|
|||||||
url: string;
|
url: string;
|
||||||
}>>(),
|
}>>(),
|
||||||
isArchived: integer('is_archived', { mode: 'boolean' }).notNull().default(false),
|
isArchived: integer('is_archived', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
// claim ownership lives in item_claims (1 item -> many claims; unique per (item, guest))
|
||||||
claimedByGuestId: text('claimed_by_guest_id').references(() => guests.id, { onDelete: 'set null' }),
|
|
||||||
claimedByNote: text('claimed_by_note'),
|
|
||||||
claimedAt: integer('claimed_at', { mode: 'timestamp' }),
|
|
||||||
isPurchased: integer('is_purchased', { mode: 'boolean' }).notNull().default(false),
|
|
||||||
|
|
||||||
sortOrder: integer('sort_order').notNull().default(0),
|
sortOrder: integer('sort_order').notNull().default(0),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const itemClaims = sqliteTable(
|
||||||
|
'item_claims',
|
||||||
|
{
|
||||||
|
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||||
|
itemId: text('item_id').notNull().references(() => wishlistItems.id, { onDelete: 'cascade' }),
|
||||||
|
guestId: text('guest_id').notNull().references(() => guests.id, { onDelete: 'cascade' }),
|
||||||
|
quantity: integer('quantity').notNull().default(1),
|
||||||
|
note: text('note'),
|
||||||
|
isPurchased: integer('is_purchased', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
claimedAt: integer('claimed_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
||||||
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
uniqueItemGuest: unique('item_claims_item_guest_unique').on(t.itemId, t.guestId),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const settings = sqliteTable('settings', {
|
export const settings = sqliteTable('settings', {
|
||||||
id: text('id').primaryKey().$defaultFn(() => createId()),
|
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||||
key: text('key').notNull().unique(),
|
key: text('key').notNull().unique(),
|
||||||
@@ -57,4 +69,5 @@ export const settings = sqliteTable('settings', {
|
|||||||
export type Wishlist = typeof wishlists.$inferSelect;
|
export type Wishlist = typeof wishlists.$inferSelect;
|
||||||
export type WishlistItem = typeof wishlistItems.$inferSelect;
|
export type WishlistItem = typeof wishlistItems.$inferSelect;
|
||||||
export type Guest = typeof guests.$inferSelect;
|
export type Guest = typeof guests.$inferSelect;
|
||||||
|
export type ItemClaim = typeof itemClaims.$inferSelect;
|
||||||
export type Setting = typeof settings.$inferSelect;
|
export type Setting = typeof settings.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user