Vollstaendiges Datenmodell: Katalog (manufacturer, theme, set, tag, instruction), Besitz (collection_item, location, missing_part, photo), Wunschlisten (wishlist, wishlist_item) und die Auth-Tabellen von better-auth inklusive passkey und invite. Kernregel: ein set ist ein Katalogeintrag, ein collection_item die Verknuepfung mit dem tatsaechlichen Besitz. Preise liegen als Cent in INTEGER-Spalten, damit Summen exakt bleiben. SQLite laeuft mit WAL, foreign_keys=ON und busy_timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
61 lines
2.5 KiB
TypeScript
61 lines
2.5 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
|
import { check, index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
import { user } from './auth';
|
|
import { set } from './catalog';
|
|
|
|
export const wishlist = sqliteTable(
|
|
'wishlist',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
ownerUserId: text('owner_user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
/** e.g. "Weihnachten 2026". */
|
|
title: text('title').notNull(),
|
|
occasion: text('occasion', { enum: ['christmas', 'birthday', 'easter', 'other'] })
|
|
.notNull()
|
|
.default('other'),
|
|
eventDate: integer('event_date', { mode: 'timestamp' }),
|
|
/** 32 random bytes, base64url encoded - the only key to the public view. */
|
|
shareToken: text('share_token').notNull().unique(),
|
|
isPublic: integer('is_public', { mode: 'boolean' }).notNull().default(false),
|
|
/** When on, the owner sees "reserviert" but not by whom. */
|
|
surpriseMode: integer('surprise_mode', { mode: 'boolean' }).notNull().default(false),
|
|
/** Message shown to the gift givers, e.g. "Bitte keine Doppelungen". */
|
|
note: text('note'),
|
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
|
},
|
|
(t) => [index('wishlist_owner_idx').on(t.ownerUserId)],
|
|
);
|
|
|
|
export const wishlistItem = sqliteTable(
|
|
'wishlist_item',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
wishlistId: text('wishlist_id')
|
|
.notNull()
|
|
.references(() => wishlist.id, { onDelete: 'cascade' }),
|
|
setId: text('set_id')
|
|
.notNull()
|
|
.references(() => set.id, { onDelete: 'cascade' }),
|
|
/** 1 = highest, 3 = lowest. */
|
|
priority: integer('priority').notNull().default(2),
|
|
note: text('note'),
|
|
sortOrder: integer('sort_order').notNull().default(0),
|
|
reservedByName: text('reserved_by_name'),
|
|
reservedAt: integer('reserved_at', { mode: 'timestamp' }),
|
|
/** SHA-256 of the cancellation token; plaintext only lives in the link. */
|
|
reservationTokenHash: text('reservation_token_hash'),
|
|
/** Set once the set showed up in the collection. */
|
|
fulfilledAt: integer('fulfilled_at', { mode: 'timestamp' }),
|
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
},
|
|
(t) => [
|
|
// Makes "skip duplicates" a database guarantee, not just a UI rule.
|
|
uniqueIndex('wishlist_item_unique').on(t.wishlistId, t.setId),
|
|
index('wishlist_item_list_idx').on(t.wishlistId, t.sortOrder),
|
|
check('wishlist_item_priority', sql`${t.priority} BETWEEN 1 AND 3`),
|
|
],
|
|
);
|