- Drink Images: upload/display photos of bottles/cans on drink cards and detail pages - My Bar: inventory tracker for spirits, liqueurs, mixers, bitters, garnishes, tools - Bartender: AI-powered cocktail recipe generation, "what can I make" suggestions, saved recipes. Cross-references bar inventory for ingredient availability. - Recommend: AI flavor profile analysis, personalized drink recommendations, "find similar" drinks based on highly-rated favorites - Navigation: desktop sidebar with all 8 routes, mobile bottom nav with 4 primary items + "More" popup menu - New Prisma models: BarItem, Recipe, FlavorProfile - Backup/restore updated to include bar items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
9.4 KiB
Plaintext
325 lines
9.4 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ─── Auth.js Models ──────────────────────────────────────────────
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
password String? // hashed password for credentials auth
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
accounts Account[]
|
|
sessions Session[]
|
|
drinks Drink[]
|
|
ratings Rating[]
|
|
menuScans MenuScan[]
|
|
apiKeys UserApiKey[]
|
|
preferences UserPreference?
|
|
wishlistItems WishlistItem[]
|
|
sharedLists SharedList[]
|
|
barItems BarItem[]
|
|
recipes Recipe[]
|
|
flavorProfile FlavorProfile?
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
}
|
|
|
|
// ─── App Models ──────────────────────────────────────────────────
|
|
|
|
model UserApiKey {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
provider String // "claude" | "openai"
|
|
encryptedKey String @db.Text
|
|
iv String // initialization vector for decryption
|
|
label String? // optional user-friendly label
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, provider])
|
|
}
|
|
|
|
model UserPreference {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
preferredStyles String[] // e.g., ["IPA", "Stout", "Pinot Noir"]
|
|
avoidedStyles String[] // e.g., ["Sour", "Light Lager"]
|
|
minAbv Float?
|
|
maxAbv Float?
|
|
defaultProvider String? // preferred AI provider
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
enum DrinkType {
|
|
BEER
|
|
WINE
|
|
COCKTAIL
|
|
SPIRIT
|
|
OTHER
|
|
}
|
|
|
|
model Drink {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
name String
|
|
type DrinkType
|
|
subType String? // e.g., "IPA", "Stout", "Cabernet Sauvignon"
|
|
brewery String? // brewery or winery
|
|
region String?
|
|
abv Float?
|
|
description String? @db.Text
|
|
imageUrl String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
ratings Rating[]
|
|
menuItems MenuItem[]
|
|
recipes Recipe[]
|
|
|
|
@@index([userId])
|
|
@@index([userId, type])
|
|
@@index([userId, name])
|
|
}
|
|
|
|
model Rating {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
drinkId String
|
|
score Int // 1-5
|
|
notes String? @db.Text
|
|
wouldReorder Boolean @default(false)
|
|
location String? // where they tried it
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
drink Drink @relation(fields: [drinkId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([drinkId])
|
|
}
|
|
|
|
enum ScanStatus {
|
|
UPLOADING
|
|
PROCESSING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
model MenuScan {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
imageUrl String
|
|
status ScanStatus @default(UPLOADING)
|
|
aiProvider String? // which provider was used
|
|
aiRawResponse Json? // raw AI response for debugging
|
|
errorMessage String? @db.Text
|
|
processedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
items MenuItem[]
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model MenuItem {
|
|
id String @id @default(cuid())
|
|
scanId String
|
|
name String
|
|
type DrinkType
|
|
subType String?
|
|
brewery String?
|
|
abv Float?
|
|
price String?
|
|
description String? @db.Text
|
|
matchedDrinkId String?
|
|
userRating Int? // cached rating from matched drink
|
|
aiRecommended Boolean @default(false)
|
|
aiReason String? @db.Text // why AI recommends it
|
|
createdAt DateTime @default(now())
|
|
|
|
scan MenuScan @relation(fields: [scanId], references: [id], onDelete: Cascade)
|
|
matchedDrink Drink? @relation(fields: [matchedDrinkId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([scanId])
|
|
}
|
|
|
|
// ─── Wishlist / Try Later ────────────────────────────────────────
|
|
|
|
model WishlistItem {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
name String
|
|
type DrinkType
|
|
subType String?
|
|
brewery String?
|
|
abv Float?
|
|
description String? @db.Text
|
|
notes String? @db.Text // user's personal note ("saw at Joe's bar")
|
|
source String? // "scan", "ai_search", "manual"
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
// ─── Search Cache ───────────────────────────────────────────────
|
|
|
|
model SearchCache {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
queryHash String // normalized (lowercase, trimmed)
|
|
query String // original text
|
|
results Json // { drinks: [...] }
|
|
provider String // "claude" | "openai"
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([userId, queryHash, provider])
|
|
@@index([userId])
|
|
}
|
|
|
|
// ─── Shared Lists ────────────────────────────────────────────────
|
|
|
|
model SharedList {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
slug String @unique // public URL slug
|
|
title String
|
|
description String? @db.Text
|
|
listType String @default("collection") // "collection", "wishlist", "custom"
|
|
isPublic Boolean @default(true)
|
|
drinkIds String[] // ids of drinks to include (empty = all)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([slug])
|
|
}
|
|
|
|
// ─── Bar Inventory ──────────────────────────────────────────────
|
|
|
|
enum BarItemCategory {
|
|
SPIRITS
|
|
LIQUEURS
|
|
MIXERS
|
|
BITTERS
|
|
GARNISHES
|
|
TOOLS
|
|
}
|
|
|
|
enum BarItemQuantity {
|
|
FULL
|
|
HALF
|
|
LOW
|
|
EMPTY
|
|
}
|
|
|
|
model BarItem {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
name String
|
|
category BarItemCategory
|
|
quantity BarItemQuantity @default(FULL)
|
|
notes String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([userId, category])
|
|
}
|
|
|
|
// ─── Recipes ────────────────────────────────────────────────────
|
|
|
|
model Recipe {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
title String
|
|
ingredients Json // [{ name: string, amount: string, available: boolean }]
|
|
steps Json // string[]
|
|
garnish String?
|
|
glassware String?
|
|
sourceDrinkId String?
|
|
notes String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
sourceDrink Drink? @relation(fields: [sourceDrinkId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
// ─── Flavor Profile ─────────────────────────────────────────────
|
|
|
|
model FlavorProfile {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
profileText String @db.Text
|
|
profileData Json?
|
|
generatedAt DateTime @default(now())
|
|
ratingCount Int @default(0)
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|