Advertisement.species existed in the schema but was never wired up — the admin form never let you set it and the feed's ad query ignored it entirely, showing every active ad to every pet regardless of species. Adds a new Advertisement.breeds field alongside it and wires both through the whole path: a targeting UI in the ad create/edit dialog (species checkboxes, each expandable into its own breed checklist), and a filter in admin.getActiveAds (now scoped to the viewing pet) so cat food ads stop reaching dog owners. Empty species/breeds = shown to everyone, unchanged from today's behavior.
668 lines
23 KiB
Plaintext
668 lines
23 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model Owner {
|
|
id String @id // = Clerk userId (e.g. "user_xxx")
|
|
createdAt DateTime @default(now())
|
|
invitedById String? // Clerk userId of the person who invited this owner
|
|
pets Pet[]
|
|
adminRole AdminRole?
|
|
ban UserBan?
|
|
issuedInvites InviteCode[] @relation("IssuedBy")
|
|
legalAcknowledgedAt DateTime? // last time the owner dismissed the legal-update banner; null = never seen one
|
|
contentRightsConsentedAt DateTime? // first-pet onboarding: confirms the owner holds all rights to uploaded photos/videos
|
|
dataProcessingConsentedAt DateTime? // first-pet onboarding: Willenserklärung consenting to data transfer to Clerk/Cloudflare/Mux/Supabase
|
|
dataExportTokens DataExportToken[]
|
|
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
model InviteCode {
|
|
id String @id @default(cuid())
|
|
code String @unique
|
|
issuedBy Owner? @relation("IssuedBy", fields: [issuedById], references: [id])
|
|
issuedById String? // null = admin-generated root invite
|
|
usedById String? // Clerk userId who redeemed this code (not a FK — owner may not exist yet)
|
|
usedAt DateTime?
|
|
revoked Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([issuedById])
|
|
}
|
|
|
|
model Species {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
breeds Breed[]
|
|
pets Pet[]
|
|
translations SpeciesTranslation[]
|
|
}
|
|
|
|
model SpeciesTranslation {
|
|
id String @id @default(cuid())
|
|
species Species @relation(fields: [speciesId], references: [id])
|
|
speciesId String
|
|
locale String
|
|
name String
|
|
|
|
@@unique([speciesId, locale])
|
|
}
|
|
|
|
model Breed {
|
|
id String @id @default(cuid())
|
|
name String
|
|
species Species @relation(fields: [speciesId], references: [id])
|
|
speciesId String
|
|
pets Pet[]
|
|
translations BreedTranslation[]
|
|
|
|
@@unique([speciesId, name])
|
|
}
|
|
|
|
model BreedTranslation {
|
|
id String @id @default(cuid())
|
|
breed Breed @relation(fields: [breedId], references: [id])
|
|
breedId String
|
|
locale String
|
|
name String
|
|
|
|
@@unique([breedId, locale])
|
|
}
|
|
|
|
model Pet {
|
|
id String @id @default(cuid())
|
|
owner Owner @relation(fields: [ownerId], references: [id])
|
|
ownerId String
|
|
species Species @relation(fields: [speciesId], references: [id])
|
|
speciesId String
|
|
breed Breed? @relation(fields: [breedId], references: [id])
|
|
breedId String?
|
|
name String
|
|
bio String?
|
|
adoptionStory String?
|
|
avatarKey String? // Supabase Storage object key, NOT a full URL
|
|
dmPolicy DmPolicy @default(EVERYONE) // who can send this pet DMs
|
|
birthday DateTime? @db.Date // actual birthday for anniversary notifications
|
|
adoptedAt DateTime? @db.Date // adoption date for anniversary notifications
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Phase 2 back-relations
|
|
posts Post[]
|
|
stories Story[]
|
|
storyViews StoryView[] @relation("StoryViewer")
|
|
following Follow[] @relation("Follower")
|
|
followers Follow[] @relation("Followee")
|
|
blocking Block[] @relation("Blocker")
|
|
blockedBy Block[] @relation("Blocked")
|
|
reports Report[] @relation("Reporter")
|
|
|
|
// Phase 3 back-relations
|
|
reactions Reaction[] @relation("PetReactions")
|
|
comments Comment[] @relation("PetComments")
|
|
reposts Repost[] @relation("PetReposts")
|
|
|
|
// Phase 4 back-relations
|
|
receivedNotifications Notification[] @relation("ReceivedNotifications")
|
|
sentNotifications Notification[] @relation("SentNotifications")
|
|
|
|
// Phase 6 back-relations
|
|
conversationsAsA Conversation[] @relation("ConvPetA")
|
|
conversationsAsB Conversation[] @relation("ConvPetB")
|
|
sentMessages Message[] @relation("MessageSender")
|
|
|
|
// Phase 7 back-relations
|
|
weightLogs WeightLog[]
|
|
vetVisits VetVisit[]
|
|
vaccines Vaccine[]
|
|
healthCard HealthCard?
|
|
emergencyVet EmergencyVet?
|
|
feedingInfo PetFeedingInfo?
|
|
|
|
// Ad interaction back-relations
|
|
adReactions AdReaction[] @relation("AdReactions")
|
|
adComments AdComment[] @relation("AdComments")
|
|
adReposts AdRepost[] @relation("AdReposts")
|
|
|
|
@@index([ownerId])
|
|
@@index([speciesId])
|
|
@@index([breedId])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
// ─── Phase 6 Enums ───────────────────────────────────────────────────────────
|
|
|
|
enum DmPolicy {
|
|
EVERYONE // anyone can send a DM
|
|
FOLLOWERS_ONLY // only pets that this pet follows can send a DM
|
|
}
|
|
|
|
// ─── Phase 2 Enums ───────────────────────────────────────────────────────────
|
|
|
|
enum PostType {
|
|
PHOTO
|
|
MILESTONE
|
|
REPOST
|
|
VIDEO
|
|
}
|
|
|
|
enum VideoStatus {
|
|
PROCESSING
|
|
READY
|
|
ERROR
|
|
}
|
|
|
|
enum MilestoneType {
|
|
BIRTHDAY
|
|
ADOPTION_DAY
|
|
FIRST_OUTING
|
|
VET_VISIT
|
|
LITTER
|
|
FOLLOWERS_500
|
|
FOLLOWERS_1K
|
|
FOLLOWERS_2_5K
|
|
FOLLOWERS_5K
|
|
FOLLOWERS_10K
|
|
FOLLOWERS_25K
|
|
FOLLOWERS_50K
|
|
FOLLOWERS_100K
|
|
FOLLOWERS_250K
|
|
FOLLOWERS_500K
|
|
FOLLOWERS_1M
|
|
}
|
|
|
|
enum ReportReason {
|
|
SPAM
|
|
HARASSMENT
|
|
INAPPROPRIATE
|
|
MISINFORMATION
|
|
OTHER
|
|
}
|
|
|
|
// ─── Phase 2 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Post {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
type PostType @default(PHOTO)
|
|
caption String? @db.VarChar(500)
|
|
images PostImage[]
|
|
milestone Milestone?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Denormalized engagement counters — kept in sync transactionally at each
|
|
// reaction/comment/repost create+delete site (reactions.ts, comments.ts,
|
|
// reposts.ts, admin.ts deleteComment). Avoids live _count aggregation on
|
|
// feed/explore reads. Backfilled from existing rows in the same migration.
|
|
reactionCount Int @default(0)
|
|
commentCount Int @default(0)
|
|
repostCount Int @default(0)
|
|
|
|
// Phase 3 back-relations
|
|
reactions Reaction[] @relation("PostReactions")
|
|
comments Comment[] @relation("PostComments")
|
|
repost Repost? @relation("RepostEntry")
|
|
originalReposts Repost[] @relation("OriginalReposts")
|
|
hashtags PostHashtag[] @relation("PostHashtags")
|
|
|
|
// Phase 4 back-relations
|
|
notifications Notification[]
|
|
|
|
// Phase 5 back-relation
|
|
videoPost VideoPost?
|
|
|
|
@@index([petId, createdAt(sort: Desc)])
|
|
}
|
|
|
|
model PostImage {
|
|
id String @id @default(cuid())
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
storageKey String // Supabase Storage object key
|
|
position Int // 0-based order in carousel
|
|
width Int?
|
|
height Int?
|
|
blurhash String? // computed client-side at upload (src/lib/blurhash.ts), decoded as a loading placeholder
|
|
|
|
@@unique([postId, position])
|
|
@@index([postId])
|
|
}
|
|
|
|
model Milestone {
|
|
id String @id @default(cuid())
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String @unique
|
|
milestoneType MilestoneType
|
|
notes String? @db.VarChar(500) // free text for 'litter' type (D-09)
|
|
}
|
|
|
|
model Story {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
storageKey String // Supabase Storage object key
|
|
blurhash String? // computed client-side at upload (src/lib/blurhash.ts), decoded as a loading placeholder
|
|
expiresAt DateTime // set to NOW() + 24 hours on create (D-07)
|
|
views StoryView[]
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([petId, expiresAt]) // supports lazy-delete filter query
|
|
}
|
|
|
|
model StoryView {
|
|
story Story @relation(fields: [storyId], references: [id]) // NO onDelete:Cascade (D-08)
|
|
storyId String
|
|
viewerPet Pet @relation("StoryViewer", fields: [viewerPetId], references: [id])
|
|
viewerPetId String
|
|
viewedAt DateTime @default(now())
|
|
|
|
@@id([storyId, viewerPetId]) // deduplication: one view per viewer per story
|
|
@@index([viewedAt]) // supports 30-day cleanup job (D-08)
|
|
}
|
|
|
|
model Follow {
|
|
follower Pet @relation("Follower", fields: [followerPetId], references: [id], onDelete: Cascade)
|
|
followerPetId String
|
|
followee Pet @relation("Followee", fields: [followeePetId], references: [id], onDelete: Cascade)
|
|
followeePetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([followerPetId, followeePetId])
|
|
@@index([followerPetId])
|
|
@@index([followeePetId])
|
|
}
|
|
|
|
model Block {
|
|
blocker Pet @relation("Blocker", fields: [blockerPetId], references: [id], onDelete: Cascade)
|
|
blockerPetId String
|
|
blocked Pet @relation("Blocked", fields: [blockedPetId], references: [id], onDelete: Cascade)
|
|
blockedPetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([blockerPetId, blockedPetId])
|
|
@@index([blockerPetId])
|
|
}
|
|
|
|
model Report {
|
|
id String @id @default(cuid())
|
|
reporter Pet @relation("Reporter", fields: [reporterPetId], references: [id])
|
|
reporterPetId String
|
|
targetPostId String? // null if reporting a profile
|
|
targetPetId String? // null if reporting a post
|
|
reason ReportReason
|
|
notes String? @db.VarChar(500)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([reporterPetId])
|
|
@@index([targetPostId])
|
|
}
|
|
|
|
// ─── Phase 3 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Reaction {
|
|
id String @id @default(cuid())
|
|
post Post @relation("PostReactions", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
pet Pet @relation("PetReactions", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([postId, petId])
|
|
@@index([postId])
|
|
}
|
|
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
post Post @relation("PostComments", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
pet Pet @relation("PetComments", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
body String @db.VarChar(500)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Phase 4 back-relation
|
|
notifications Notification[]
|
|
|
|
@@index([postId, createdAt(sort: Asc)])
|
|
@@index([petId])
|
|
}
|
|
|
|
model Repost {
|
|
post Post @relation("RepostEntry", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String @unique
|
|
originalPost Post @relation("OriginalReposts", fields: [originalPostId], references: [id])
|
|
originalPostId String
|
|
reposterPet Pet @relation("PetReposts", fields: [reposterPetId], references: [id], onDelete: Cascade)
|
|
reposterPetId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([reposterPetId, originalPostId])
|
|
@@index([originalPostId])
|
|
}
|
|
|
|
model Hashtag {
|
|
id String @id @default(cuid())
|
|
tag String @unique
|
|
posts PostHashtag[]
|
|
}
|
|
|
|
model PostHashtag {
|
|
post Post @relation("PostHashtags", fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String
|
|
hashtag Hashtag @relation(fields: [hashtagId], references: [id], onDelete: Cascade)
|
|
hashtagId String
|
|
|
|
@@id([postId, hashtagId])
|
|
@@index([hashtagId])
|
|
}
|
|
|
|
// ─── Phase 5 Models ──────────────────────────────────────────────────────────
|
|
|
|
model VideoPost {
|
|
id String @id @default(cuid())
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String @unique
|
|
muxUploadId String? // Mux direct upload ID — set on creation
|
|
muxAssetId String? // Mux asset ID — set by video.upload.asset_created webhook
|
|
muxPlaybackId String? // Mux playback ID — set by video.asset.ready webhook
|
|
status VideoStatus @default(PROCESSING)
|
|
durationSecs Float? // populated by webhook
|
|
aspectRatio String? // e.g. "16:9" from Mux
|
|
|
|
@@index([muxUploadId])
|
|
@@index([muxAssetId])
|
|
}
|
|
|
|
// ─── Phase 6 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Conversation {
|
|
id String @id @default(cuid())
|
|
petA Pet @relation("ConvPetA", fields: [petAId], references: [id], onDelete: Cascade)
|
|
petAId String
|
|
petB Pet @relation("ConvPetB", fields: [petBId], references: [id], onDelete: Cascade)
|
|
petBId String
|
|
messages Message[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt // updated on every new message for inbox ordering
|
|
|
|
@@unique([petAId, petBId]) // petAId is always < petBId (sorted on create) — deduplication
|
|
@@index([petAId])
|
|
@@index([petBId])
|
|
}
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
conversationId String
|
|
senderPet Pet @relation("MessageSender", fields: [senderPetId], references: [id])
|
|
senderPetId String
|
|
body String @db.VarChar(1000)
|
|
readAt DateTime? // null = unread by recipient
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([conversationId, createdAt(sort: Asc)])
|
|
@@index([senderPetId])
|
|
}
|
|
|
|
// ─── Phase 7 Models ──────────────────────────────────────────────────────────
|
|
|
|
model WeightLog {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
date DateTime @db.Date
|
|
weightGrams Int // stored in grams; display as kg
|
|
notes String? @db.VarChar(200)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([petId, date(sort: Desc)])
|
|
}
|
|
|
|
model VetVisit {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
date DateTime @db.Date
|
|
reason String @db.VarChar(100)
|
|
notes String? @db.VarChar(500)
|
|
vetName String? @db.VarChar(100)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([petId, date(sort: Desc)])
|
|
}
|
|
|
|
model Vaccine {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
name String @db.VarChar(100)
|
|
dateGiven DateTime @db.Date
|
|
nextDueDate DateTime? @db.Date
|
|
notes String? @db.VarChar(200)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([petId, dateGiven(sort: Desc)])
|
|
}
|
|
|
|
model EmergencyVet {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String @unique
|
|
name String @db.VarChar(150)
|
|
address String? @db.VarChar(300)
|
|
phone String? @db.VarChar(30)
|
|
website String? @db.VarChar(200)
|
|
lat Float?
|
|
lng Float?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model HealthCard {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String @unique
|
|
token String @unique @default(cuid())
|
|
expiresAt DateTime? // null = never expires; set on create/regenerate
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
// GDPR Art. 15 data-export download token (Gitea #6 follow-up). Deliberately
|
|
// a bespoke DB-backed token instead of emailing Supabase's native signed URL
|
|
// directly — the download link handed to the user must read pawfeed.org, not
|
|
// the storage backend. /api/gdpr-export/[token] validates against this table
|
|
// and streams the file server-side; storageKey/bucket stay an internal detail.
|
|
model DataExportToken {
|
|
id String @id @default(cuid())
|
|
owner Owner @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
ownerId String
|
|
token String @unique @default(cuid())
|
|
storageKey String
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([ownerId])
|
|
}
|
|
|
|
model PetFeedingInfo {
|
|
id String @id @default(cuid())
|
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String @unique
|
|
foodType String? @db.VarChar(100)
|
|
feedingTimes Int?
|
|
dailyAmountG Int?
|
|
foodBrand String? @db.VarChar(100)
|
|
specialDiet String? @db.VarChar(500)
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
// ─── Phase 4 Enums ───────────────────────────────────────────────────────────
|
|
|
|
enum NotificationType {
|
|
FOLLOW
|
|
REACTION
|
|
COMMENT
|
|
MESSAGE
|
|
BIRTHDAY
|
|
ADOPTION_DAY
|
|
MENTION
|
|
PAW_BACK
|
|
}
|
|
|
|
// ─── Phase 4 Models ──────────────────────────────────────────────────────────
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
recipientPet Pet @relation("ReceivedNotifications", fields: [recipientPetId], references: [id], onDelete: Cascade)
|
|
recipientPetId String
|
|
type NotificationType
|
|
actorPet Pet? @relation("SentNotifications", fields: [actorPetId], references: [id], onDelete: SetNull)
|
|
actorPetId String?
|
|
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
postId String?
|
|
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
commentId String?
|
|
read Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([recipientPetId, read, createdAt(sort: Desc)])
|
|
}
|
|
|
|
// ─── Admin / Moderation Models ────────────────────────────────────────────────
|
|
|
|
enum AdminRoleType {
|
|
SUPER_ADMIN
|
|
MODERATOR
|
|
}
|
|
|
|
model AdminRole {
|
|
id String @id @default(cuid())
|
|
owner Owner @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
ownerId String @unique
|
|
role AdminRoleType @default(MODERATOR)
|
|
grantedAt DateTime @default(now())
|
|
grantedBy String? // ownerId of the granting super admin
|
|
}
|
|
|
|
model UserBan {
|
|
id String @id @default(cuid())
|
|
owner Owner @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
ownerId String @unique
|
|
reason String? @db.VarChar(500)
|
|
bannedAt DateTime @default(now())
|
|
bannedBy String // ownerId of moderator who issued the ban
|
|
expiresAt DateTime? // null = permanent ban
|
|
}
|
|
|
|
model Advertisement {
|
|
id String @id @default(cuid())
|
|
title String @db.VarChar(100)
|
|
caption String? @db.VarChar(500)
|
|
imageKey String?
|
|
targetUrl String? @db.VarChar(500)
|
|
isActive Boolean @default(true)
|
|
species String[] // targeted Species ids; empty = all species (no restriction)
|
|
breeds String[] // targeted Breed ids, narrows WITHIN the species selection above; empty = no breed restriction
|
|
startDate DateTime?
|
|
endDate DateTime?
|
|
createdBy String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Denormalized engagement counters — see Post model comment for rationale.
|
|
// adViewCount is incremented once per ad-card impression (client fires
|
|
// ads.recordView the first time the card is ≥50% scrolled into view —
|
|
// see AdCard.tsx), not deduplicated per pet, matching standard ad-impression
|
|
// semantics rather than the per-pet-once pattern reactions/reposts use.
|
|
adViewCount Int @default(0)
|
|
adReactionCount Int @default(0)
|
|
adCommentCount Int @default(0)
|
|
adRepostCount Int @default(0)
|
|
|
|
reactions AdReaction[]
|
|
comments AdComment[]
|
|
reposts AdRepost[]
|
|
|
|
@@index([isActive, startDate, endDate])
|
|
}
|
|
|
|
model AdReaction {
|
|
id String @id @default(cuid())
|
|
ad Advertisement @relation(fields: [adId], references: [id], onDelete: Cascade)
|
|
adId String
|
|
pet Pet @relation("AdReactions", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([adId, petId])
|
|
@@index([adId])
|
|
}
|
|
|
|
model AdComment {
|
|
id String @id @default(cuid())
|
|
ad Advertisement @relation(fields: [adId], references: [id], onDelete: Cascade)
|
|
adId String
|
|
pet Pet @relation("AdComments", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
body String @db.VarChar(500)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([adId])
|
|
}
|
|
|
|
model AdRepost {
|
|
id String @id @default(cuid())
|
|
ad Advertisement @relation(fields: [adId], references: [id], onDelete: Cascade)
|
|
adId String
|
|
pet Pet @relation("AdReposts", fields: [petId], references: [id], onDelete: Cascade)
|
|
petId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([adId, petId])
|
|
@@index([adId])
|
|
}
|
|
|
|
model ModerationLog {
|
|
id String @id @default(cuid())
|
|
action String @db.VarChar(50) // DELETE_POST | DELETE_COMMENT | BAN_USER | UNBAN_USER | DELETE_IMAGE
|
|
targetType String @db.VarChar(20) // POST | COMMENT | USER | IMAGE
|
|
targetId String
|
|
reason String? @db.VarChar(500)
|
|
moderatorId String // ownerId
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([moderatorId, createdAt(sort: Desc)])
|
|
@@index([targetType, targetId])
|
|
}
|
|
|
|
// ─── Legal Document Versioning ─────────────────────────────────────────────
|
|
// Pull-based, not push-based: publishing a version doesn't fan out a
|
|
// Notification row per pet (Notification is petId-scoped; ToS/Datenschutz
|
|
// acceptance is an Owner-level concern). Instead, each owner's session
|
|
// compares Owner.legalAcknowledgedAt against the latest publishedAt.
|
|
|
|
enum LegalDocType {
|
|
NUTZUNGSBEDINGUNGEN
|
|
DATENSCHUTZ
|
|
}
|
|
|
|
model LegalDocumentVersion {
|
|
id String @id @default(cuid())
|
|
docType LegalDocType
|
|
changeSummary String @db.VarChar(1000)
|
|
publishedAt DateTime @default(now())
|
|
publishedBy String // ownerId of the publishing admin
|
|
|
|
@@index([publishedAt(sort: Desc)])
|
|
}
|