Akshar2325 commited on
Commit
d189be0
·
1 Parent(s): dffd7d2

feat(db): add core models for video streaming platform

Browse files

- 【Models】Introduce schemas for Movies, Series, Seasons, Episodes, and
related video, genre, and tag tables.
- 【User Interaction】Add models for Comments, Likes, Ratings, Playlists,
and Watch History to support user engagement.
- 【Enums】Define new enums for content, upload, session, and
notification types to standardize states across the application.

refactor(schema): standardize primary keys and relations
- 【Primary Keys】Migrate all primary key default values from cuid() to
uuid() for industry-standard identifiers.
- 【Data Integrity】Implement onDelete: Cascade on relations to ensure
automatic cleanup of orphaned records.
- 【Enums】Consolidate session status into a single unified enum for
consistency across user and admin models.

chore(db): improve timestamp precision
- 【Timestamps】Specify millisecond precision (@db .Timestamp(3)) for all
DateTime fields to enhance data accuracy.

Files changed (1) hide show
  1. prisma/schema.prisma +551 -45
prisma/schema.prisma CHANGED
@@ -6,83 +6,123 @@ datasource db {
6
  provider = "postgresql"
7
  }
8
 
 
 
9
  enum USER_STATUS {
10
  ENABLED
11
  DISABLED
12
  INVITED
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  model User {
16
- id String @id @default(cuid())
17
  uniqueId String @unique
18
  email String? @unique
19
  phone String?
20
- code String? // country code
21
  firstName String?
22
  lastName String?
23
  status USER_STATUS @default(INVITED)
24
  emailVerified Boolean @default(false)
25
  phoneVerified Boolean @default(false)
26
- lastLogin DateTime?
27
- createdAt DateTime @default(now())
28
- updatedAt DateTime @updatedAt
29
  isDeleted Boolean @default(false)
30
 
31
  // Relations
32
- sessions UserSession[]
33
- credentials UserCredential[]
 
 
 
 
 
 
 
34
 
35
  @@map("users")
36
  }
37
 
38
  model UserSession {
39
- id String @id @default(cuid())
40
  userId String
41
- accessToken String @db.Text
42
- refreshToken String @db.Text
43
  ipAddress String?
44
- userAgent String? @db.Text
45
  geoIpCountry String?
46
  clientId String?
47
- loginAt DateTime?
48
- logoutAt DateTime?
49
- createdAt DateTime @default(now())
50
- updatedAt DateTime @updatedAt
51
- isDeleted Boolean @default(false)
 
 
52
 
53
  // Relations
54
- user User @relation(fields: [userId], references: [id])
55
 
56
  @@map("user_sessions")
57
  }
58
 
59
  model UserCredential {
60
- id String @id @default(cuid())
61
  userId String
62
  password String
63
- createdAt DateTime @default(now())
64
- updatedAt DateTime @updatedAt
65
  isDeleted Boolean @default(false)
66
 
67
  // Relations
68
- user User @relation(fields: [userId], references: [id])
69
 
70
  @@map("user_credentials")
71
  }
72
 
73
- enum SUPER_ADMIN_SESSION_STATUS {
74
- CURRENT
75
- EXPIRED
76
- REVOKED
77
- }
78
 
79
  model SuperAdmin {
80
- id String @id @default(cuid())
81
  email String @unique
82
  name String
83
  profileImage String?
84
- createdAt DateTime @default(now())
85
- updatedAt DateTime @updatedAt
86
  isDeleted Boolean @default(false)
87
 
88
  // Relations
@@ -93,41 +133,507 @@ model SuperAdmin {
93
  }
94
 
95
  model SuperAdminSession {
96
- id String @id @default(cuid())
97
  superAdminId String
98
- accessToken String @db.Text
99
- refreshToken String @db.Text
100
  ipAddress String?
101
- userAgent String? @db.Text
102
  geoIpCountry String?
103
  city String?
104
  state String?
105
  latitude Float?
106
  longitude Float?
107
- isFromAdmin Boolean @default(false)
108
- status SUPER_ADMIN_SESSION_STATUS @default(CURRENT)
109
- loginAt DateTime?
110
- expiredAt DateTime?
111
- createdAt DateTime @default(now())
112
- updatedAt DateTime @updatedAt
113
- isDeleted Boolean @default(false)
114
 
115
  // Relations
116
- superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id])
117
 
118
  @@map("super_admin_sessions")
119
  }
120
 
121
  model SuperAdminCredential {
122
- id String @id @default(cuid())
123
  superAdminId String
124
  password String
125
- createdAt DateTime @default(now())
126
- updatedAt DateTime @updatedAt
127
  isDeleted Boolean @default(false)
128
 
129
  // Relations
130
- superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id])
131
 
132
  @@map("super_admin_credentials")
133
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  provider = "postgresql"
7
  }
8
 
9
+ // ================ ENUMS =================
10
+
11
  enum USER_STATUS {
12
  ENABLED
13
  DISABLED
14
  INVITED
15
  }
16
 
17
+ enum SESSION_STATUS {
18
+ CURRENT
19
+ EXPIRED
20
+ REVOKED
21
+ }
22
+
23
+ enum UPLOAD_STATUS {
24
+ UPLOADING
25
+ PROCESSING
26
+ COMPLETED
27
+ FAILED
28
+ }
29
+
30
+ enum NOTIFICATION_TYPE {
31
+ NEW_MOVIE
32
+ NEW_SERIES
33
+ NEW_EPISODE
34
+ COMMENT_REPLY
35
+ COMMENT_LIKE
36
+ CONTENT_LIKE
37
+ ACCOUNT_UPDATE
38
+ SUBSCRIPTION_REMINDER
39
+ RECOMMENDATION
40
+ }
41
+
42
+ enum CONTENT_TYPE {
43
+ MOVIE
44
+ SERIES
45
+ EPISODE
46
+ }
47
+
48
+ // ============ USER MANAGEMENT ============
49
+
50
  model User {
51
+ id String @id @default(uuid())
52
  uniqueId String @unique
53
  email String? @unique
54
  phone String?
55
+ code String?
56
  firstName String?
57
  lastName String?
58
  status USER_STATUS @default(INVITED)
59
  emailVerified Boolean @default(false)
60
  phoneVerified Boolean @default(false)
61
+ lastLogin DateTime? @db.Timestamp(3)
62
+ createdAt DateTime @default(now()) @db.Timestamp(3)
63
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
64
  isDeleted Boolean @default(false)
65
 
66
  // Relations
67
+ sessions UserSession[]
68
+ credentials UserCredential[]
69
+ watchHistories WatchHistory[]
70
+ notifications Notification[]
71
+ playlists Playlist[]
72
+ likes Like[]
73
+ ratings Rating[]
74
+ comments Comment[]
75
+ commentLikes CommentLike[]
76
 
77
  @@map("users")
78
  }
79
 
80
  model UserSession {
81
+ id String @id @default(uuid())
82
  userId String
83
+ accessToken String @db.Text
84
+ refreshToken String @db.Text
85
  ipAddress String?
86
+ userAgent String? @db.Text
87
  geoIpCountry String?
88
  clientId String?
89
+ status SESSION_STATUS @default(CURRENT)
90
+ loginAt DateTime? @db.Timestamp(3)
91
+ logoutAt DateTime? @db.Timestamp(3)
92
+ expiredAt DateTime? @db.Timestamp(3)
93
+ createdAt DateTime @default(now()) @db.Timestamp(3)
94
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
95
+ isDeleted Boolean @default(false)
96
 
97
  // Relations
98
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
99
 
100
  @@map("user_sessions")
101
  }
102
 
103
  model UserCredential {
104
+ id String @id @default(uuid())
105
  userId String
106
  password String
107
+ createdAt DateTime @default(now()) @db.Timestamp(3)
108
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
109
  isDeleted Boolean @default(false)
110
 
111
  // Relations
112
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
113
 
114
  @@map("user_credentials")
115
  }
116
 
117
+ // ============ SUPER ADMIN ============
 
 
 
 
118
 
119
  model SuperAdmin {
120
+ id String @id @default(uuid())
121
  email String @unique
122
  name String
123
  profileImage String?
124
+ createdAt DateTime @default(now()) @db.Timestamp(3)
125
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
126
  isDeleted Boolean @default(false)
127
 
128
  // Relations
 
133
  }
134
 
135
  model SuperAdminSession {
136
+ id String @id @default(uuid())
137
  superAdminId String
138
+ accessToken String @db.Text
139
+ refreshToken String @db.Text
140
  ipAddress String?
141
+ userAgent String? @db.Text
142
  geoIpCountry String?
143
  city String?
144
  state String?
145
  latitude Float?
146
  longitude Float?
147
+ isFromAdmin Boolean @default(false)
148
+ status SESSION_STATUS @default(CURRENT)
149
+ loginAt DateTime? @db.Timestamp(3)
150
+ expiredAt DateTime? @db.Timestamp(3)
151
+ createdAt DateTime @default(now()) @db.Timestamp(3)
152
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
153
+ isDeleted Boolean @default(false)
154
 
155
  // Relations
156
+ superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade)
157
 
158
  @@map("super_admin_sessions")
159
  }
160
 
161
  model SuperAdminCredential {
162
+ id String @id @default(uuid())
163
  superAdminId String
164
  password String
165
+ createdAt DateTime @default(now()) @db.Timestamp(3)
166
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
167
  isDeleted Boolean @default(false)
168
 
169
  // Relations
170
+ superAdmin SuperAdmin @relation(fields: [superAdminId], references: [id], onDelete: Cascade)
171
 
172
  @@map("super_admin_credentials")
173
  }
174
+
175
+ // ============ MOVIES ============
176
+
177
+ model Movie {
178
+ id String @id @default(uuid())
179
+ title String
180
+ slug String @unique
181
+ description String? @db.Text
182
+ thumbnailUrl String
183
+ bannerUrl String?
184
+ trailerUrl String?
185
+ rating String?
186
+ releaseYear Int?
187
+ language String?
188
+ duration Int?
189
+ imdbId String?
190
+ tmdbId String?
191
+ isFeatured Boolean @default(false)
192
+ uploadStatus UPLOAD_STATUS @default(COMPLETED)
193
+ views Int @default(0)
194
+ createdAt DateTime @default(now()) @db.Timestamp(3)
195
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
196
+ isDeleted Boolean @default(false)
197
+
198
+ // Relations
199
+ genres MovieGenre[]
200
+ tags MovieTag[]
201
+ video MovieVideo?
202
+ comments Comment[]
203
+ likes Like[]
204
+ ratings Rating[]
205
+ playlistItems PlaylistItem[]
206
+ watchHistory WatchHistory[]
207
+
208
+ @@index([isDeleted])
209
+ @@index([isFeatured])
210
+ @@index([slug])
211
+ @@map("movies")
212
+ }
213
+
214
+ model MovieVideo {
215
+ id String @id @default(uuid())
216
+ movieId String @unique
217
+ videoUrl String
218
+ videoKey String?
219
+ fileSize BigInt?
220
+ quality String?
221
+ uploadStatus UPLOAD_STATUS @default(COMPLETED)
222
+ createdAt DateTime @default(now()) @db.Timestamp(3)
223
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
224
+ isDeleted Boolean @default(false)
225
+
226
+ // Relations
227
+ movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade)
228
+
229
+ @@index([isDeleted])
230
+ @@map("movie_videos")
231
+ }
232
+
233
+ model MovieGenre {
234
+ id String @id @default(uuid())
235
+ movieId String
236
+ genreId String
237
+ createdAt DateTime @default(now()) @db.Timestamp(3)
238
+
239
+ // Relations
240
+ movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade)
241
+ genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade)
242
+
243
+ @@unique([movieId, genreId])
244
+ @@index([movieId])
245
+ @@index([genreId])
246
+ @@map("movie_genres")
247
+ }
248
+
249
+ // ============ SERIES ============
250
+
251
+ model Series {
252
+ id String @id @default(uuid())
253
+ title String
254
+ slug String @unique
255
+ description String? @db.Text
256
+ thumbnailUrl String
257
+ bannerUrl String?
258
+ trailerUrl String?
259
+ rating String?
260
+ releaseYear Int?
261
+ language String?
262
+ imdbId String?
263
+ tmdbId String?
264
+ isFeatured Boolean @default(false)
265
+ uploadStatus UPLOAD_STATUS @default(COMPLETED)
266
+ views Int @default(0)
267
+ createdAt DateTime @default(now()) @db.Timestamp(3)
268
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
269
+ isDeleted Boolean @default(false)
270
+
271
+ // Relations
272
+ genres SeriesGenre[]
273
+ tags SeriesTag[]
274
+ seasons Season[]
275
+ comments Comment[]
276
+ likes Like[]
277
+ ratings Rating[]
278
+ playlistItems PlaylistItem[]
279
+ watchHistory WatchHistory[]
280
+
281
+ @@index([isDeleted])
282
+ @@index([isFeatured])
283
+ @@index([slug])
284
+ @@map("series")
285
+ }
286
+
287
+ model Season {
288
+ id String @id @default(uuid())
289
+ seriesId String
290
+ seasonNumber Int
291
+ title String
292
+ description String? @db.Text
293
+ thumbnailUrl String?
294
+ releaseYear Int?
295
+ createdAt DateTime @default(now()) @db.Timestamp(3)
296
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
297
+ isDeleted Boolean @default(false)
298
+
299
+ // Relations
300
+ series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
301
+ episodes Episode[]
302
+
303
+ @@unique([seriesId, seasonNumber])
304
+ @@index([seriesId])
305
+ @@map("seasons")
306
+ }
307
+
308
+ model Episode {
309
+ id String @id @default(uuid())
310
+ seasonId String
311
+ episodeNumber Int
312
+ title String
313
+ description String? @db.Text
314
+ thumbnailUrl String?
315
+ duration Int
316
+ createdAt DateTime @default(now()) @db.Timestamp(3)
317
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
318
+ isDeleted Boolean @default(false)
319
+
320
+ // Relations
321
+ season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade)
322
+ video EpisodeVideo?
323
+ comments Comment[]
324
+ likes Like[]
325
+ ratings Rating[]
326
+ playlistItems PlaylistItem[]
327
+ watchHistory WatchHistory[]
328
+
329
+ @@unique([seasonId, episodeNumber])
330
+ @@index([seasonId])
331
+ @@map("episodes")
332
+ }
333
+
334
+ model EpisodeVideo {
335
+ id String @id @default(uuid())
336
+ episodeId String @unique
337
+ videoUrl String
338
+ videoKey String?
339
+ fileSize BigInt?
340
+ quality String?
341
+ uploadStatus UPLOAD_STATUS @default(COMPLETED)
342
+ createdAt DateTime @default(now()) @db.Timestamp(3)
343
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
344
+ isDeleted Boolean @default(false)
345
+
346
+ // Relations
347
+ episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
348
+
349
+ @@index([isDeleted])
350
+ @@map("episode_videos")
351
+ }
352
+
353
+ model SeriesGenre {
354
+ id String @id @default(uuid())
355
+ seriesId String
356
+ genreId String
357
+ createdAt DateTime @default(now()) @db.Timestamp(3)
358
+
359
+ // Relations
360
+ series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
361
+ genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade)
362
+
363
+ @@unique([seriesId, genreId])
364
+ @@index([seriesId])
365
+ @@index([genreId])
366
+ @@map("series_genres")
367
+ }
368
+
369
+ // ============ GENRES ============
370
+
371
+ model Genre {
372
+ id String @id @default(uuid())
373
+ name String @unique
374
+ slug String @unique
375
+ description String? @db.Text
376
+ icon String?
377
+ sortOrder Int @default(0)
378
+ createdAt DateTime @default(now()) @db.Timestamp(3)
379
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
380
+ isDeleted Boolean @default(false)
381
+
382
+ // Relations
383
+ movieGenres MovieGenre[]
384
+ seriesGenres SeriesGenre[]
385
+
386
+ @@index([slug])
387
+ @@map("genres")
388
+ }
389
+
390
+ // ============ UNIFIED COMMENT ============
391
+
392
+ model Comment {
393
+ id String @id @default(uuid())
394
+ content String @db.Text
395
+ userId String
396
+ contentType CONTENT_TYPE
397
+ movieId String?
398
+ seriesId String?
399
+ episodeId String?
400
+ parentId String?
401
+ createdAt DateTime @default(now()) @db.Timestamp(3)
402
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
403
+ isDeleted Boolean @default(false)
404
+
405
+ // Relations
406
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
407
+ movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade)
408
+ series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade)
409
+ episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
410
+ parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
411
+ replies Comment[] @relation("CommentReplies")
412
+ likes CommentLike[]
413
+
414
+ @@index([movieId, isDeleted])
415
+ @@index([seriesId, isDeleted])
416
+ @@index([episodeId, isDeleted])
417
+ @@index([userId, isDeleted])
418
+ @@index([contentType, isDeleted])
419
+ @@map("comments")
420
+ }
421
+
422
+ // ============ UNIFIED LIKE ============
423
+
424
+ model Like {
425
+ id String @id @default(uuid())
426
+ userId String
427
+ contentType CONTENT_TYPE
428
+ movieId String?
429
+ seriesId String?
430
+ episodeId String?
431
+ isLike Boolean
432
+ createdAt DateTime @default(now()) @db.Timestamp(3)
433
+
434
+ // Relations
435
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
436
+ movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade)
437
+ series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade)
438
+ episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
439
+
440
+ // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code)
441
+ @@unique([userId, movieId])
442
+ @@unique([userId, seriesId])
443
+ @@unique([userId, episodeId])
444
+ @@index([movieId, contentType])
445
+ @@index([seriesId, contentType])
446
+ @@index([episodeId, contentType])
447
+ @@map("likes")
448
+ }
449
+
450
+ // ============ COMMENT LIKE ============
451
+
452
+ model CommentLike {
453
+ id String @id @default(uuid())
454
+ userId String
455
+ commentId String
456
+ isLike Boolean
457
+ createdAt DateTime @default(now()) @db.Timestamp(3)
458
+
459
+ // Relations
460
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
461
+ comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
462
+
463
+ @@unique([userId, commentId])
464
+ @@index([commentId])
465
+ @@map("comment_likes")
466
+ }
467
+
468
+ // ============ PLAYLISTS ============
469
+
470
+ model Playlist {
471
+ id String @id @default(uuid())
472
+ name String
473
+ description String? @db.Text
474
+ userId String
475
+ createdAt DateTime @default(now()) @db.Timestamp(3)
476
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
477
+ isDeleted Boolean @default(false)
478
+
479
+ // Relations
480
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
481
+ items PlaylistItem[]
482
+
483
+ @@index([userId, isDeleted])
484
+ @@map("playlists")
485
+ }
486
+
487
+ model PlaylistItem {
488
+ id String @id @default(uuid())
489
+ playlistId String
490
+ contentType CONTENT_TYPE
491
+ movieId String?
492
+ seriesId String?
493
+ episodeId String?
494
+ position Int
495
+ createdAt DateTime @default(now()) @db.Timestamp(3)
496
+
497
+ // Relations
498
+ playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
499
+ movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade)
500
+ series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade)
501
+ episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
502
+
503
+ // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code)
504
+ @@unique([playlistId, movieId])
505
+ @@unique([playlistId, seriesId])
506
+ @@unique([playlistId, episodeId])
507
+ @@index([playlistId])
508
+ @@index([movieId, contentType])
509
+ @@index([seriesId, contentType])
510
+ @@index([episodeId, contentType])
511
+ @@map("playlist_items")
512
+ }
513
+
514
+ // ============ WATCH HISTORY ============
515
+
516
+ model WatchHistory {
517
+ id String @id @default(uuid())
518
+ userId String
519
+ contentType CONTENT_TYPE
520
+ movieId String?
521
+ seriesId String?
522
+ episodeId String?
523
+ watchedAt DateTime @default(now()) @db.Timestamp(3)
524
+ watchTime Int
525
+ duration Int
526
+ completed Boolean @default(false)
527
+
528
+ // Relations
529
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
530
+ movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade)
531
+ series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade)
532
+ episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
533
+
534
+ @@index([userId, contentType])
535
+ @@index([movieId])
536
+ @@index([seriesId])
537
+ @@index([episodeId])
538
+ @@map("watch_history")
539
+ }
540
+
541
+ // ============ NOTIFICATIONS ============
542
+
543
+ model Notification {
544
+ id String @id @default(uuid())
545
+ userId String
546
+ type NOTIFICATION_TYPE
547
+ title String
548
+ message String @db.Text
549
+ read Boolean @default(false)
550
+ data Json?
551
+ createdAt DateTime @default(now()) @db.Timestamp(3)
552
+ isDeleted Boolean @default(false)
553
+
554
+ // Relations
555
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
556
+
557
+ @@index([userId, isDeleted])
558
+ @@index([userId, read, isDeleted])
559
+ @@map("notifications")
560
+ }
561
+
562
+ // ============ RATINGS (Star Ratings 1-5) ============
563
+
564
+ model Rating {
565
+ id String @id @default(uuid())
566
+ userId String
567
+ contentType CONTENT_TYPE
568
+ movieId String?
569
+ seriesId String?
570
+ episodeId String?
571
+ value Int
572
+ createdAt DateTime @default(now()) @db.Timestamp(3)
573
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
574
+
575
+ // Relations
576
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
577
+ movie Movie? @relation(fields: [movieId], references: [id], onDelete: Cascade)
578
+ series Series? @relation(fields: [seriesId], references: [id], onDelete: Cascade)
579
+ episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
580
+
581
+ // Only one of movieId/seriesId/episodeId should be non-null per row (enforce in code)
582
+ @@unique([userId, movieId])
583
+ @@unique([userId, seriesId])
584
+ @@unique([userId, episodeId])
585
+ @@index([movieId, contentType])
586
+ @@index([seriesId, contentType])
587
+ @@index([episodeId, contentType])
588
+ @@map("ratings")
589
+ }
590
+
591
+ // ============ TAGS (Flexible Content Tags) ============
592
+
593
+ model Tag {
594
+ id String @id @default(uuid())
595
+ name String @unique
596
+ slug String @unique
597
+ createdAt DateTime @default(now()) @db.Timestamp(3)
598
+ updatedAt DateTime @updatedAt @db.Timestamp(3)
599
+ isDeleted Boolean @default(false)
600
+
601
+ // Relations
602
+ movieTags MovieTag[]
603
+ seriesTags SeriesTag[]
604
+
605
+ @@index([slug])
606
+ @@map("tags")
607
+ }
608
+
609
+ model MovieTag {
610
+ id String @id @default(uuid())
611
+ movieId String
612
+ tagId String
613
+ createdAt DateTime @default(now()) @db.Timestamp(3)
614
+
615
+ // Relations
616
+ movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade)
617
+ tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
618
+
619
+ @@unique([movieId, tagId])
620
+ @@index([movieId])
621
+ @@index([tagId])
622
+ @@map("movie_tags")
623
+ }
624
+
625
+ model SeriesTag {
626
+ id String @id @default(uuid())
627
+ seriesId String
628
+ tagId String
629
+ createdAt DateTime @default(now()) @db.Timestamp(3)
630
+
631
+ // Relations
632
+ series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
633
+ tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
634
+
635
+ @@unique([seriesId, tagId])
636
+ @@index([seriesId])
637
+ @@index([tagId])
638
+ @@map("series_tags")
639
+ }