stremio/apps/master-api/prisma/schema.prisma
Jos Vooges | STH e4aba1950b Add ends-at times, compact red topnav, and custom profile avatars.
Show end times on detail/player, tighten TV chrome, and let profiles toggle or upload a cropped photo via API and mobile/iOS.
2026-09-13 03:04:47 +02:00

734 lines
26 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum NodeStatus {
ONLINE
OFFLINE
DEGRADED
REVOKED
UPDATING
SCANNING
ERROR
}
enum PlaybackSessionStatus {
ACTIVE
IDLE
EXPIRED
REVOKED
COMPLETED
}
enum LocationType {
LOCAL
REMOTE
}
enum LibraryShelfKind {
MOVIE
SERIES
}
enum DownloadJobStatus {
DOWNLOADING
FINISHED
IMPORTING
IMPORTED
FAILED
}
enum GooglePlaySyncStatus {
NOT_ENABLED
PENDING_ADD
ACTIVE
PENDING_REMOVE
REMOVED
ERROR
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String @map("password_hash")
name String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
addonTokens AddonToken[]
sessions AdminSession[]
@@map("users")
}
/** App-gebruiker (Android/iOS/Tizen) — huishouden met optioneel IPTV + Stremio-addons. */
model ViewerUser {
id String @id @default(uuid())
email String @unique
passwordHash String @map("password_hash")
name String?
enabled Boolean @default(true)
appAccess Boolean @default(true) @map("app_access")
/** Live TV in de apps (Xtream-lijn op deze gebruiker). */
iptvAccess Boolean @default(false) @map("iptv_access")
/** @deprecated Legacy: IPTV via addon-token; voorkeur = iptvLine op deze user. */
iptvAddonTokenId String? @map("iptv_addon_token_id")
/** Google-account voor Play Closed Testing; fallback = email */
googlePlayEmail String? @map("google_play_email")
/** Gewenste Play-distributietoegang (sync met Google Group) */
googlePlayAccess Boolean @default(false) @map("google_play_access")
googlePlayMembershipId String? @map("google_play_membership_id")
googlePlaySyncStatus GooglePlaySyncStatus @default(NOT_ENABLED) @map("google_play_sync_status")
googlePlayLastSyncedAt DateTime? @map("google_play_last_synced_at")
googlePlaySyncError String? @map("google_play_sync_error") @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
devices ViewerDevice[]
profiles ViewerProfile[]
linkCodes DeviceLinkCode[]
googlePlayAuditLogs GooglePlayAccessAuditLog[]
/** Legacy koppeling — behouden voor migratie. */
iptvAddonToken AddonToken? @relation("ViewerLegacyIptvToken", fields: [iptvAddonTokenId], references: [id], onDelete: SetNull)
iptvLine IptvLine?
addonTokens AddonToken[] @relation("ViewerAddonTokens")
@@map("viewer_users")
}
/** Bruteforce-bescherming op e-mail/wachtwoord (claim + direct login). */
model ViewerLoginLock {
email String @id
failedCount Int @default(0) @map("failed_count")
lockedUntil DateTime? @map("locked_until")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("viewer_login_locks")
}
/** Netflix-stijl kijkprofiel onder één ViewerUser (voortgang/prefs per profiel). */
model ViewerProfile {
id String @id @default(uuid())
viewerUserId String @map("viewer_user_id")
name String
avatarId String @default("fox") @map("avatar_id")
/** Eigen geüploade foto (relatief pad onder data/profile-avatars). */
avatarImage String? @map("avatar_image")
/** Toon avatar/foto in UI (uit = alleen naam). */
showAvatar Boolean @default(true) @map("show_avatar")
isOwner Boolean @default(false) @map("is_owner")
isKids Boolean @default(false) @map("is_kids")
pinHash String? @map("pin_hash")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
devices ViewerDevice[]
progress WatchProgress[]
shelfPrefs ViewerShelfPreference[]
favorites ViewerFavorite[]
@@unique([viewerUserId, name])
@@index([viewerUserId])
@@map("viewer_profiles")
}
model ViewerDevice {
id String @id @default(uuid())
viewerUserId String @map("viewer_user_id")
name String
platform String @default("android_tv")
refreshTokenHash String @unique @map("refresh_token_hash")
activeProfileId String? @map("active_profile_id")
lastSeenAt DateTime? @map("last_seen_at")
revoked Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
activeProfile ViewerProfile? @relation(fields: [activeProfileId], references: [id], onDelete: SetNull)
@@index([viewerUserId])
@@index([activeProfileId])
@@map("viewer_devices")
}
/** Kortstondige koppelcode (TV toont code → gebruiker claimt op /link). */
model DeviceLinkCode {
id String @id @default(uuid())
codeHash String @unique @map("code_hash")
/** Display-code (alleen tot claim; daarna null) */
codeDisplay String? @map("code_display")
deviceName String @map("device_name")
platform String @default("android_tv")
expiresAt DateTime @map("expires_at")
consumedAt DateTime? @map("consumed_at")
viewerUserId String? @map("viewer_user_id")
/** Eenmalig token voor device poll na claim */
claimTokenHash String? @unique @map("claim_token_hash")
createdAt DateTime @default(now()) @map("created_at")
viewer ViewerUser? @relation(fields: [viewerUserId], references: [id], onDelete: SetNull)
@@index([expiresAt])
@@map("device_link_codes")
}
enum WatchMediaType {
MOVIE
EPISODE
}
model WatchProgress {
id String @id @default(uuid())
profileId String @map("profile_id")
mediaType WatchMediaType @map("media_type")
movieId String? @map("movie_id")
episodeId String? @map("episode_id")
seriesId String? @map("series_id")
positionMs Int @default(0) @map("position_ms")
durationMs Int @default(0) @map("duration_ms")
completed Boolean @default(false)
hiddenFromContinue Boolean @default(false) @map("hidden_from_continue")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
@@unique([profileId, movieId])
@@unique([profileId, episodeId])
@@index([profileId, updatedAt])
@@index([profileId, hiddenFromContinue, completed])
@@map("watch_progress")
}
model ViewerShelfPreference {
id String @id @default(uuid())
profileId String @map("profile_id")
shelfId String @map("shelf_id")
visible Boolean @default(true)
sortOrder Int @default(0) @map("sort_order")
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
@@unique([profileId, shelfId])
@@index([profileId, sortOrder])
@@map("viewer_shelf_preferences")
}
enum FavoriteMediaType {
MOVIE
SERIES
}
/** Favorieten/bookmarks per kijkprofiel — gesynchroniseerd over devices. */
model ViewerFavorite {
id String @id @default(uuid())
profileId String @map("profile_id")
mediaType FavoriteMediaType @map("media_type")
mediaId String @map("media_id")
createdAt DateTime @default(now()) @map("created_at")
profile ViewerProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
@@unique([profileId, mediaType, mediaId])
@@index([profileId, createdAt])
@@map("viewer_favorites")
}
model AdminSession {
id String @id @default(uuid())
userId String @map("user_id")
tokenHash String @unique @map("token_hash")
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("admin_sessions")
}
model AddonToken {
id String @id @default(uuid())
userId String @map("user_id")
/** Gebruiker (huishouden) waaraan deze Stremio-addon hangt. */
viewerUserId String? @map("viewer_user_id")
tokenHash String @unique @map("token_hash")
label String?
revoked Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
viewerUser ViewerUser? @relation("ViewerAddonTokens", fields: [viewerUserId], references: [id], onDelete: SetNull)
iptvLine IptvLine?
playbackSessions PlaybackSession[]
legacyIptvViewers ViewerUser[] @relation("ViewerLegacyIptvToken")
@@index([viewerUserId])
@@map("addon_tokens")
}
/** Xtream/XUI credentials — bij voorkeur per ViewerUser; legacy optioneel per AddonToken. */
model IptvLine {
id String @id @default(uuid())
/** Primair: IPTV-lijn van één app-gebruiker. */
viewerUserId String? @unique @map("viewer_user_id")
/** Legacy: IPTV direct op Stremio-addon (pre-merge). */
addonTokenId String? @unique @map("addon_token_id")
label String?
baseUrl String @map("base_url")
username String
/** AES-GCM ciphertext (base64) */
passwordEnc String @map("password_enc")
enableLive Boolean @default(true) @map("enable_live")
enableVod Boolean @default(true) @map("enable_vod")
enableSeries Boolean @default(true) @map("enable_series")
lastOkAt DateTime? @map("last_ok_at")
lastError String? @map("last_error")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
viewerUser ViewerUser? @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
addonToken AddonToken? @relation(fields: [addonTokenId], references: [id], onDelete: Cascade)
@@map("iptv_lines")
}
model Location {
id String @id @default(uuid())
name String @unique
description String?
priority Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
nodes Node[]
@@map("locations")
}
model Node {
id String @id @default(uuid())
name String
hostname String?
locationId String? @map("location_id")
locationType LocationType @default(LOCAL) @map("location_type")
status NodeStatus @default(OFFLINE)
version String?
architecture String?
publicStreamUrl String @map("public_stream_url")
lastSeen DateTime? @map("last_seen")
totalStorage BigInt @default(0) @map("total_storage")
freeStorage BigInt @default(0) @map("free_storage")
activeStreams Int @default(0) @map("active_streams")
currentBandwidth BigInt @default(0) @map("current_bandwidth")
libraryFileCount Int @default(0) @map("library_file_count")
lastRevision Int @default(0) @map("last_revision")
/** Node scanner: "off" or duration like "24h" / "6h" */
fullScanInterval String @default("off") @map("full_scan_interval")
/** Nightly catch-up time HH:MM in node local TZ */
fullScanAt String @default("03:30") @map("full_scan_at")
/** Host/container scan roots for movies (e.g. /mnt/user/NL Films) — legacy; prefer scanRoots */
moviesPaths String[] @default([]) @map("movies_paths")
/** Host/container scan roots for series — legacy; prefer scanRoots */
seriesPaths String[] @default([]) @map("series_paths")
revoked Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
location Location? @relation(fields: [locationId], references: [id])
credentials NodeCredential?
mediaFiles MediaFile[]
playbackSessions PlaybackSession[]
scanRoots NodeScanRoot[]
synologyDsConfig SynologyDsConfig?
downloadJobs DownloadJob[]
@@map("nodes")
}
/** Named Stremio catalog / shelf shared across all nodes (e.g. Kids Films). */
model LibraryShelf {
id String @id @default(uuid())
name String
homepageName String? @map("homepage_name")
slug String @unique
kind LibraryShelfKind
sortOrder Int @default(0) @map("sort_order")
enabled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
scanRoots NodeScanRoot[]
mediaFiles MediaFile[]
@@map("library_shelves")
}
/** One filesystem root on a node bound to a library shelf. */
model NodeScanRoot {
id String @id @default(uuid())
nodeId String @map("node_id")
path String
shelfId String @map("shelf_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
shelf LibraryShelf @relation(fields: [shelfId], references: [id], onDelete: Restrict)
@@unique([nodeId, path])
@@index([shelfId])
@@map("node_scan_roots")
}
model NodeCredential {
id String @id @default(uuid())
nodeId String @unique @map("node_id")
apiKeyHash String @map("api_key_hash")
createdAt DateTime @default(now()) @map("created_at")
rotatedAt DateTime? @map("rotated_at")
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@map("node_credentials")
}
model NodeEnrollmentToken {
id String @id @default(uuid())
tokenHash String @unique @map("token_hash")
name String?
locationId String? @map("location_id")
expiresAt DateTime @map("expires_at")
usedAt DateTime? @map("used_at")
createdAt DateTime @default(now()) @map("created_at")
@@map("node_enrollment_tokens")
}
model Movie {
id String @id @default(uuid())
title String
originalTitle String? @map("original_title")
year Int?
overview String?
posterUrl String? @map("poster_url")
backdropUrl String? @map("backdrop_url")
logoUrl String? @map("logo_url")
genres String[] @default([])
/** TMDB adult-flag; kids-profielen filteren hierop. */
adult Boolean @default(false)
runtime Int?
imdbId String? @unique @map("imdb_id")
tmdbId Int? @unique @map("tmdb_id")
/** TMDB release_date — for “Net uitgebracht”. */
releaseDate DateTime? @map("release_date")
/**
* Eerste keer dat deze titel in het cluster beschikbaar kwam.
* Bump’t niet als een tweede node dezelfde film aanlevert.
*/
libraryAddedAt DateTime? @map("library_added_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
mediaFiles MediaFile[]
matches LibraryMatch[]
subtitles LibrarySubtitle[]
@@index([adult])
@@index([libraryAddedAt])
@@index([releaseDate])
@@map("movies")
}
model Series {
id String @id @default(uuid())
title String
year Int?
overview String?
posterUrl String? @map("poster_url")
backdropUrl String? @map("backdrop_url")
logoUrl String? @map("logo_url")
genres String[] @default([])
/** TMDB adult-flag; kids-profielen filteren hierop. */
adult Boolean @default(false)
imdbId String? @unique @map("imdb_id")
tmdbId Int? @unique @map("tmdb_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
seasons Season[]
episodes Episode[]
matches LibraryMatch[]
@@index([adult])
@@map("series")
}
model Season {
id String @id @default(uuid())
seriesId String @map("series_id")
seasonNumber Int @map("season_number")
series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
episodes Episode[]
@@unique([seriesId, seasonNumber])
@@map("seasons")
}
model Episode {
id String @id @default(uuid())
seriesId String @map("series_id")
seasonId String @map("season_id")
seasonNumber Int @map("season_number")
episodeNumber Int @map("episode_number")
title String?
overview String?
stillUrl String? @map("still_url")
runtime Int?
airDate DateTime? @map("air_date")
imdbId String? @map("imdb_id")
tmdbId Int? @map("tmdb_id")
/** Eerste keer dat deze aflevering in het cluster kwam (geen bump door 2e node). */
libraryAddedAt DateTime? @map("library_added_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
series Series @relation(fields: [seriesId], references: [id], onDelete: Cascade)
season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade)
mediaFiles MediaFile[]
matches LibraryMatch[]
@@unique([seriesId, seasonNumber, episodeNumber])
@@index([libraryAddedAt])
@@map("episodes")
}
model MediaFile {
id String @id @default(uuid())
nodeId String @map("node_id")
movieId String? @map("movie_id")
episodeId String? @map("episode_id")
shelfId String? @map("shelf_id")
localFileId String @map("local_file_id")
sizeBytes BigInt @map("size_bytes")
container String?
resolution String?
videoCodec String? @map("video_codec")
videoBitrate Int? @map("video_bitrate")
hdrType String? @map("hdr_type")
audioCodec String? @map("audio_codec")
audioChannels String? @map("audio_channels")
audioLanguage String? @map("audio_language")
releaseName String @map("release_name")
/** Absolute path on the media-node host (filled from node ≥1.3.7). */
path String? @map("path")
available Boolean @default(true)
lastSeen DateTime @default(now()) @map("last_seen")
/// Eerste indexering van dit bestand: bij create ≈ file-mtime (niet scan-now).
discoveredAt DateTime @default(now()) @map("discovered_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
movie Movie? @relation(fields: [movieId], references: [id])
episode Episode? @relation(fields: [episodeId], references: [id])
shelf LibraryShelf? @relation(fields: [shelfId], references: [id], onDelete: SetNull)
playbackSessions PlaybackSession[]
@@unique([nodeId, localFileId])
@@index([shelfId])
@@index([discoveredAt])
@@map("media_files")
}
model LibraryMatch {
id String @id @default(uuid())
movieId String? @map("movie_id")
seriesId String? @map("series_id")
episodeId String? @map("episode_id")
releaseName String @map("release_name")
ignored Boolean @default(false)
manual Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
movie Movie? @relation(fields: [movieId], references: [id])
series Series? @relation(fields: [seriesId], references: [id])
episode Episode? @relation(fields: [episodeId], references: [id])
@@map("library_matches")
}
model PlaybackSession {
id String @id @default(uuid())
tokenHash String @unique @map("token_hash")
userId String? @map("user_id")
addonTokenId String? @map("addon_token_id")
nodeId String @map("node_id")
mediaFileId String @map("media_file_id")
status PlaybackSessionStatus @default(ACTIVE)
idleTimeoutSeconds Int @map("idle_timeout_seconds")
absoluteExpiresAt DateTime @map("absolute_expires_at")
lastActivity DateTime @default(now()) @map("last_activity")
bytesSent BigInt @default(0) @map("bytes_sent")
lastByteOffset BigInt @default(0) @map("last_byte_offset")
fileSizeBytes BigInt @default(0) @map("file_size_bytes")
revoked Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
node Node @relation(fields: [nodeId], references: [id])
mediaFile MediaFile @relation(fields: [mediaFileId], references: [id])
addonToken AddonToken? @relation(fields: [addonTokenId], references: [id], onDelete: SetNull)
@@index([status, lastActivity])
@@index([addonTokenId])
@@map("playback_sessions")
}
/** Synology Download Station credentials + library import paths (per node). */
model SynologyDsConfig {
id String @id @default(uuid())
nodeId String @unique @map("node_id")
/** DSM base URL, e.g. https://nas.local:5001 */
baseUrl String @map("base_url")
username String
passwordEnc String @map("password_enc")
/** DS task destination (share-relative), e.g. Downloads/complete */
downloadDestination String @map("download_destination")
/** Absolute host path of that destination on the media-node */
downloadHostPath String @map("download_host_path")
/** Absolute host path for 1080p movies (e.g. …/Media/Films/Films) */
filmsHostPath String @default("") @map("films_host_path")
/** Absolute host path for 4K movies (e.g. …/Media/Films/4K) */
fourKHostPath String @map("four_k_host_path")
/** Absolute host path for series (e.g. …/Media/Series/Series) */
seriesHostPath String @default("") @map("series_host_path")
enabled Boolean @default(true)
lastOkAt DateTime? @map("last_ok_at")
lastError String? @map("last_error")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@map("synology_ds_configs")
}
enum DownloadLibraryTarget {
FILMS
FOUR_K
SERIES
}
enum DownloadSeriesMode {
EPISODE
SEASON
}
model DownloadJob {
id String @id @default(uuid())
nodeId String @map("node_id")
status DownloadJobStatus @default(DOWNLOADING)
/** Where to import after DS finishes */
libraryTarget DownloadLibraryTarget @default(FOUR_K) @map("library_target")
/** Series only: single episode vs whole season pack */
seriesMode DownloadSeriesMode? @map("series_mode")
sourceUri String @map("source_uri")
sourceTitle String @map("source_title")
dsTaskId String? @map("ds_task_id")
tmdbId Int? @map("tmdb_id")
imdbId String? @map("imdb_id")
movieTitle String @map("movie_title")
movieYear Int? @map("movie_year")
posterUrl String? @map("poster_url")
seasonNumber Int? @map("season_number")
episodeNumber Int? @map("episode_number")
episodeTitle String? @map("episode_title")
/** Season pack tracking JSON */
episodesJson String? @map("episodes_json") @db.Text
destFolderName String? @map("dest_folder_name")
sourcePath String? @map("source_path")
importedPath String? @map("imported_path")
releaseTags String? @map("release_tags")
subtitleFileId Int? @map("subtitle_file_id")
subtitleLang String? @map("subtitle_lang")
subtitleSource String? @map("subtitle_source")
/** Cached subtitle body (downloaded at job create) */
subtitleContent String? @map("subtitle_content") @db.Text
subtitleError String? @map("subtitle_error")
subtitlePath String? @map("subtitle_path")
error String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@index([status, updatedAt])
@@index([nodeId, status])
@@map("download_jobs")
}
/** Singleton-style integration credentials (OpenSubtitles, etc.). */
model IntegrationSetting {
id String @id
apiKeyEnc String? @map("api_key_enc")
username String?
passwordEnc String? @map("password_enc")
/** Must match OpenSubtitles.com API Consumer / User-Agent exactly */
userAgent String? @map("user_agent")
/** Non-secret JSON config (e.g. Google Play group/project fields) */
configJson String? @map("config_json") @db.Text
enabled Boolean @default(true)
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
@@map("integration_settings")
}
/** Sidecar / OpenSubtitles text stored for Stremio stream.subtitles. */
model LibrarySubtitle {
id String @id @default(uuid())
movieId String? @map("movie_id")
tmdbId Int? @map("tmdb_id")
imdbId String? @map("imdb_id")
language String @default("nl")
format String @default("srt")
content String
fileName String? @map("file_name")
downloadJobId String? @map("download_job_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
movie Movie? @relation(fields: [movieId], references: [id], onDelete: SetNull)
@@index([movieId])
@@index([tmdbId])
@@index([imdbId])
@@map("library_subtitles")
}
/** Audit trail voor Google Play group-lidmaatschap (geen secrets). */
model GooglePlayAccessAuditLog {
id String @id @default(uuid())
adminUserId String? @map("admin_user_id")
adminEmail String? @map("admin_email")
viewerUserId String @map("viewer_user_id")
targetEmail String @map("target_email")
action String
result String
membershipId String? @map("membership_id")
errorMessage String? @map("error_message") @db.Text
createdAt DateTime @default(now()) @map("created_at")
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
@@index([viewerUserId, createdAt])
@@map("google_play_access_audit_logs")
}