Files
petfeed/prisma/schema.prisma
T
adminandClaude Sonnet 4.6 60366691e8 feat(05): Phase 5 — short-form video via Mux
Schema:
- Add VideoPost model (muxUploadId, muxAssetId, muxPlaybackId,
  status, durationSecs, aspectRatio) linked 1-to-1 with Post
- Add VideoStatus enum (PROCESSING/READY/ERROR)
- Add PostType.VIDEO

Backend:
- src/lib/mux.ts — singleton Mux client + webhook secret
- videos router: createUpload (Mux direct upload URL + PROCESSING post),
  getByPostId (status polling), updateCaption
- POST /api/webhooks/mux: verifies signature; handles
  video.upload.asset_created (store muxAssetId),
  video.asset.ready (set READY + playbackId + fan-out to feed),
  video.asset.errored (set ERROR)
- feed.ts + posts.byPetId: include videoPost in all post queries

UI:
- VideoUploadForm: drop zone → XHR PUT to Mux upload URL with
  progress bar; caption field; transitions to "Processing…" on success
- VideoCard: PROCESSING shows spinner; READY renders MuxPlayer
  (HLS, orange accent); polls every 5s until status changes
- PostTypeSheet: "Video Post" option added (between Photo and Milestone)
- PostCard: VIDEO type renders VideoCard instead of image carousel

Env vars: MUX_TOKEN_ID, MUX_TOKEN_SECRET, MUX_WEBHOOK_SECRET,
NEXT_PUBLIC_APP_URL — placeholders added to .env.local

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:27:43 +02:00

326 lines
10 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())
pets Pet[]
}
model Species {
id String @id @default(cuid())
name String @unique // "Dog" | "Cat" | "Bird"
breeds Breed[]
pets Pet[]
}
model Breed {
id String @id @default(cuid())
name String
species Species @relation(fields: [speciesId], references: [id])
speciesId String
pets Pet[]
@@unique([speciesId, name])
}
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
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")
@@index([ownerId])
}
// ─── 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
// 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?
@@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
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])
}
// ─── 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[]
@@index([tag])
}
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 4 Enums ───────────────────────────────────────────────────────────
enum NotificationType {
FOLLOW
REACTION
COMMENT
MESSAGE
}
// ─── 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)])
}