286 lines
8.2 KiB
Plaintext
286 lines
8.2 KiB
Plaintext
// prisma/schema.prisma
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
model Guest {
|
||
id String @id @default(cuid())
|
||
name String
|
||
inviteCode String @unique @db.VarChar(6)
|
||
email String?
|
||
phone String?
|
||
maxPax Int @default(1) // singles default
|
||
dietaryRestrictions String?
|
||
isAttending Boolean?
|
||
rsvpTimestamp DateTime?
|
||
confirmationCode String?
|
||
|
||
// Seating
|
||
tableId String?
|
||
table Table? @relation(fields: [tableId], references: [id])
|
||
|
||
// Profile
|
||
relationship String? @default("Guest")
|
||
relationshipTo String? // "groom" or "bride"
|
||
title String?
|
||
surname String?
|
||
notes String?
|
||
avatar String?
|
||
bio String?
|
||
socialOptIn Boolean? @default(false) // Opt-in to appear in Who's Who directory
|
||
|
||
// Social Media (optional, with privacy controls)
|
||
facebook String?
|
||
instagram String?
|
||
linkedin String?
|
||
facebookPublic Boolean @default(false)
|
||
instagramPublic Boolean @default(false)
|
||
linkedinPublic Boolean @default(false)
|
||
|
||
// Contact Info Privacy Controls
|
||
emailPublic Boolean @default(false)
|
||
phonePublic Boolean @default(false)
|
||
|
||
// Analytics / geo
|
||
lastActive DateTime?
|
||
ipCountryCode String? @db.VarChar(2)
|
||
ipCity String?
|
||
ipData Json?
|
||
|
||
// Relations
|
||
songRequests Song[]
|
||
guestbookEntries GuestbookEntry[]
|
||
galleryPhotos GalleryPhoto[]
|
||
tributes Tribute[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([inviteCode])
|
||
@@index([isAttending])
|
||
@@index([tableId])
|
||
@@index([rsvpTimestamp])
|
||
}
|
||
|
||
model Song {
|
||
id String @id @default(cuid())
|
||
title String
|
||
artist String?
|
||
votes Int @default(0)
|
||
image String?
|
||
album String?
|
||
duration String?
|
||
appleMusicId String?
|
||
previewUrl String? // 30–60s preview
|
||
trackUrl String? // Full track URL (Spotify, Apple Music, etc.)
|
||
|
||
guestId String
|
||
guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade)
|
||
votesList SongVote[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([guestId])
|
||
@@index([votes])
|
||
}
|
||
|
||
model SongVote {
|
||
id String @id @default(cuid())
|
||
songId String
|
||
song Song @relation(fields: [songId], references: [id], onDelete: Cascade)
|
||
guestId String? // Nullable for device-based votes
|
||
deviceId String? // Device UUID for unauthenticated votes
|
||
ipAddress String? // IP address for additional tracking
|
||
createdAt DateTime @default(now())
|
||
|
||
// Note: Unique constraints on nullable fields are handled in application logic
|
||
// Prisma doesn't support unique constraints with nullable fields directly
|
||
@@index([songId])
|
||
@@index([guestId])
|
||
@@index([deviceId])
|
||
@@index([songId, guestId])
|
||
@@index([songId, deviceId])
|
||
}
|
||
|
||
model GuestbookEntry {
|
||
id String @id @default(cuid())
|
||
guestId String
|
||
guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade)
|
||
guestName String
|
||
message String
|
||
photo String?
|
||
audioUrl String?
|
||
videoUrl String?
|
||
durationSeconds Int?
|
||
status String @default("active")
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([guestId])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model GalleryPhoto {
|
||
id String @id @default(cuid())
|
||
guestId String
|
||
guest Guest @relation(fields: [guestId], references: [id], onDelete: Cascade)
|
||
url String
|
||
thumbnailUrl String?
|
||
caption String?
|
||
likes Int @default(0)
|
||
comments PhotoComment[]
|
||
status String @default("active")
|
||
width Int?
|
||
height Int?
|
||
bytes Int?
|
||
exif Json?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([guestId])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model PhotoComment {
|
||
id String @id @default(cuid())
|
||
photoId String
|
||
photo GalleryPhoto @relation(fields: [photoId], references: [id], onDelete: Cascade)
|
||
guestName String
|
||
message String
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([photoId])
|
||
}
|
||
|
||
model Table {
|
||
id String @id @default(cuid())
|
||
name String @unique
|
||
capacity Int
|
||
guests Guest[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
model Admin {
|
||
id String @id @default(cuid())
|
||
username String @unique
|
||
password String
|
||
email String?
|
||
totpSecret String? // Base32 encoded TOTP secret for Google Authenticator
|
||
totpEnabled Boolean @default(false) // Whether TOTP is enabled for this admin
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([username])
|
||
}
|
||
|
||
model ContentViolation {
|
||
id String @id @default(cuid())
|
||
deviceId String?
|
||
ipAddress String?
|
||
violationType String // copy, paste, screenshot, rightclick, selection, devtools
|
||
count Int @default(1)
|
||
guestId String? // If violation by authenticated guest
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([deviceId])
|
||
@@index([ipAddress])
|
||
@@index([guestId])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model BlockedDevice {
|
||
id String @id @default(cuid())
|
||
deviceId String?
|
||
ipAddress String?
|
||
guestId String? // If blocking authenticated guest
|
||
reason String?
|
||
durationHours Int? @default(24) // Block duration in hours
|
||
blockedAt DateTime @default(now())
|
||
expiresAt DateTime?
|
||
isActive Boolean @default(true)
|
||
|
||
@@index([deviceId])
|
||
@@index([ipAddress])
|
||
@@index([guestId])
|
||
@@index([isActive])
|
||
@@index([blockedAt])
|
||
}
|
||
|
||
model Tribute {
|
||
id String @id @default(cuid())
|
||
guestId String? // Optional - can be null for anonymous tributes
|
||
guest Guest? @relation(fields: [guestId], references: [id], onDelete: SetNull)
|
||
guestName String // Store guest name even if guest is deleted
|
||
dedication String // The tribute/dedication message
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([guestId])
|
||
@@index([createdAt])
|
||
@@index([guestName])
|
||
}
|
||
|
||
model Reminder {
|
||
id String @id @default(cuid())
|
||
name String
|
||
type String // "email" | "sms" | "both"
|
||
trigger String // "days_before" | "days_after" | "specific_date" | "rsvp_deadline"
|
||
daysBefore Int?
|
||
daysAfter Int?
|
||
specificDate DateTime?
|
||
targetAudience String // "all_guests" | "confirmed" | "pending" | "declined" | "no_rsvp"
|
||
templateId String?
|
||
customMessage String?
|
||
enabled Boolean @default(true)
|
||
lastSent DateTime?
|
||
nextSend DateTime?
|
||
sentCount Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([enabled])
|
||
@@index([nextSend])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model ABTest {
|
||
id String @id @default(cuid())
|
||
name String
|
||
type String // "email" | "sms"
|
||
status String @default("draft") // "draft" | "running" | "paused" | "completed"
|
||
variantA Json // { subject?, message, recipients, sent, opened?, clicked?, responded? }
|
||
variantB Json // { subject?, message, recipients, sent, opened?, clicked?, responded? }
|
||
splitRatio Float @default(50.0) // Percentage for variant A (0-100)
|
||
startDate DateTime?
|
||
endDate DateTime?
|
||
winner String? // "A" | "B" | null
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([status])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model CouplesPlaylist {
|
||
id String @id @default(cuid())
|
||
title String
|
||
artist String?
|
||
trackUrl String? // Full track URL (Spotify, Apple Music, etc.)
|
||
previewUrl String? // 30–60s preview
|
||
image String? // Album art
|
||
album String?
|
||
duration String?
|
||
order Int @default(0) // Order in playlist
|
||
revealAt Int? // Itinerary item index (0-based) when song should be revealed. null = reveal after final item
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([order])
|
||
@@index([revealAt])
|
||
}
|