Initial commit: DrinkTracker full-stack app
Next.js 14 drink collection tracker with AI-powered search, menu scanning, ratings, wishlist, sharing, and CSV backup/restore. Features: - Auth (credentials + OAuth ready) - Drink collection with ratings and reviews - AI search via Claude/OpenAI with search history - Menu photo scanning with AI extraction - Wishlist / Try Later system - Public sharing via slug URLs - CSV backup and restore (merge/replace modes) - Docker Compose for Postgres + MinIO + dev server Security: docker-compose files use env var interpolation instead of hardcoded secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
251
prisma/schema.prisma
Normal file
251
prisma/schema.prisma
Normal file
@@ -0,0 +1,251 @@
|
||||
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[]
|
||||
}
|
||||
|
||||
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[]
|
||||
|
||||
@@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])
|
||||
}
|
||||
Reference in New Issue
Block a user