Files
dragonsstash/prisma/schema.prisma
admin 36a7e3d5f4 feat: add channel categories and improved creator detection
- Add category field to TelegramChannel (filterable tag like STL, PDF, D&D)
- Category column in channels table with edit via dropdown menu
- Improved creator extraction: filename patterns + channel title fallback
- extractCreatorFromChannelTitle strips [Completed], (Paid), emoji, etc.
- Fix ArchiveType in PackageListItem and PackageRow for new types
- Add Prisma migration for category column
2026-03-21 20:37:44 +01:00

649 lines
18 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
// ───────────────────────────────────────
// Auth.js required models
// ───────────────────────────────────────
enum Role {
ADMIN
USER
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
hashedPassword String?
role Role @default(ADMIN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
filaments Filament[]
resins Resin[]
paints Paint[]
supplies Supply[]
vendors Vendor[]
locations Location[]
usageLogs UsageLog[]
tags Tag[]
settings UserSettings?
telegramLink TelegramLink?
inviteCodes InviteCode[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String
expires DateTime
@@unique([identifier, token])
}
// ───────────────────────────────────────
// Domain models
// ───────────────────────────────────────
model Vendor {
id String @id @default(cuid())
name String @db.VarChar(64)
website String? @db.VarChar(256)
notes String? @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
filaments Filament[]
resins Resin[]
paints Paint[]
supplies Supply[]
@@index([userId])
@@index([archived])
}
model Location {
id String @id @default(cuid())
name String @db.VarChar(64)
description String? @db.VarChar(256)
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
filaments Filament[]
resins Resin[]
paints Paint[]
supplies Supply[]
@@index([userId])
@@index([archived])
}
model Filament {
id String @id @default(cuid())
name String @db.VarChar(128)
brand String @db.VarChar(64)
material String @db.VarChar(32)
color String @db.VarChar(64)
colorHex String @db.VarChar(7)
diameter Float @default(1.75)
spoolWeight Float
usedWeight Float @default(0)
emptySpoolWeight Float @default(0)
purchaseDate DateTime?
cost Float?
notes String? @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
vendorId String?
locationId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
tags TagOnFilament[]
usageLogs UsageLog[]
@@index([userId])
@@index([vendorId])
@@index([locationId])
@@index([material])
@@index([archived])
@@index([brand])
}
model Resin {
id String @id @default(cuid())
name String @db.VarChar(128)
brand String @db.VarChar(64)
resinType String @db.VarChar(32)
color String @db.VarChar(64)
colorHex String @db.VarChar(7)
bottleSize Float
usedML Float @default(0)
purchaseDate DateTime?
cost Float?
notes String? @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
vendorId String?
locationId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
tags TagOnResin[]
usageLogs UsageLog[]
@@index([userId])
@@index([vendorId])
@@index([locationId])
@@index([resinType])
@@index([archived])
@@index([brand])
}
model Paint {
id String @id @default(cuid())
name String @db.VarChar(128)
brand String @db.VarChar(64)
line String? @db.VarChar(64)
color String @db.VarChar(64)
colorHex String @db.VarChar(7)
finish String @db.VarChar(32)
volumeML Float
usedML Float @default(0)
purchaseDate DateTime?
cost Float?
notes String? @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
vendorId String?
locationId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
tags TagOnPaint[]
usageLogs UsageLog[]
@@index([userId])
@@index([vendorId])
@@index([locationId])
@@index([finish])
@@index([archived])
@@index([brand])
}
model Tag {
id String @id @default(cuid())
name String @db.VarChar(64)
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
filaments TagOnFilament[]
resins TagOnResin[]
paints TagOnPaint[]
supplies TagOnSupply[]
@@unique([name, userId])
@@index([userId])
}
model TagOnFilament {
filamentId String
tagId String
filament Filament @relation(fields: [filamentId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([filamentId, tagId])
}
model TagOnResin {
resinId String
tagId String
resin Resin @relation(fields: [resinId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([resinId, tagId])
}
model TagOnPaint {
paintId String
tagId String
paint Paint @relation(fields: [paintId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([paintId, tagId])
}
model Supply {
id String @id @default(cuid())
name String @db.VarChar(128)
brand String @db.VarChar(64)
category String @db.VarChar(32)
color String? @db.VarChar(64)
colorHex String? @db.VarChar(7)
totalAmount Float
usedAmount Float @default(0)
unit String @db.VarChar(16)
purchaseDate DateTime?
cost Float?
notes String? @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
vendorId String?
locationId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull)
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
tags TagOnSupply[]
usageLogs UsageLog[]
@@index([userId])
@@index([vendorId])
@@index([locationId])
@@index([category])
@@index([archived])
@@index([brand])
}
model TagOnSupply {
supplyId String
tagId String
supply Supply @relation(fields: [supplyId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([supplyId, tagId])
}
model UsageLog {
id String @id @default(cuid())
itemType String @db.VarChar(16)
itemId String
amount Float
unit String @db.VarChar(16)
notes String? @db.Text
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
filament Filament? @relation(fields: [filamentId], references: [id], onDelete: Cascade)
filamentId String?
resin Resin? @relation(fields: [resinId], references: [id], onDelete: Cascade)
resinId String?
paint Paint? @relation(fields: [paintId], references: [id], onDelete: Cascade)
paintId String?
supply Supply? @relation(fields: [supplyId], references: [id], onDelete: Cascade)
supplyId String?
@@index([userId])
@@index([itemType, itemId])
@@index([createdAt])
}
model UserSettings {
id String @id @default(cuid())
userId String @unique
lowStockThreshold Float @default(10)
currency String @default("USD") @db.VarChar(3)
theme String @default("dark") @db.VarChar(8)
units String @default("metric") @db.VarChar(8)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
// ───────────────────────────────────────
// Telegram ingestion models
// ───────────────────────────────────────
enum AuthState {
PENDING
AWAITING_CODE
AWAITING_PASSWORD
AUTHENTICATED
EXPIRED
}
enum ChannelType {
SOURCE
DESTINATION
}
enum ChannelRole {
READER
WRITER
}
enum ArchiveType {
ZIP
RAR
SEVEN_Z
DOCUMENT
}
enum IngestionStatus {
RUNNING
COMPLETED
FAILED
CANCELLED
}
enum FetchStatus {
PENDING
IN_PROGRESS
COMPLETED
FAILED
}
model TelegramAccount {
id String @id @default(cuid())
phone String @unique
displayName String?
isActive Boolean @default(true)
authState AuthState @default(PENDING)
authCode String?
lastSeenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
channelMaps AccountChannelMap[]
ingestionRuns IngestionRun[]
fetchRequests ChannelFetchRequest[]
@@index([isActive])
@@map("telegram_accounts")
}
model TelegramChannel {
id String @id @default(cuid())
telegramId BigInt @unique
title String
type ChannelType
isForum Boolean @default(false)
isActive Boolean @default(false)
category String? @db.VarChar(64)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accountMaps AccountChannelMap[]
packages Package[]
@@index([type, isActive])
@@index([category])
@@map("telegram_channels")
}
model AccountChannelMap {
id String @id @default(cuid())
accountId String
channelId String
role ChannelRole @default(READER)
lastProcessedMessageId BigInt?
createdAt DateTime @default(now())
account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
channel TelegramChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
topicProgress TopicProgress[]
@@unique([accountId, channelId])
@@index([accountId])
@@index([channelId])
@@map("account_channel_map")
}
model Package {
id String @id @default(cuid())
contentHash String @unique
fileName String
fileSize BigInt
archiveType ArchiveType
creator String?
sourceChannelId String
sourceMessageId BigInt
sourceTopicId BigInt?
destChannelId String?
destMessageId BigInt?
isMultipart Boolean @default(false)
partCount Int @default(1)
fileCount Int @default(0)
previewData Bytes? // JPEG thumbnail from nearby Telegram photo (stored as raw bytes)
previewMsgId BigInt? // Telegram message ID of the matched photo
indexedAt DateTime @default(now())
createdAt DateTime @default(now())
sourceChannel TelegramChannel @relation(fields: [sourceChannelId], references: [id])
files PackageFile[]
ingestionRun IngestionRun? @relation(fields: [ingestionRunId], references: [id])
ingestionRunId String?
sendRequests BotSendRequest[]
@@index([sourceChannelId])
@@index([destChannelId])
@@index([fileName])
@@index([indexedAt])
@@index([archiveType])
@@index([creator])
@@map("packages")
}
model PackageFile {
id String @id @default(cuid())
packageId String
path String
fileName String
extension String?
compressedSize BigInt @default(0)
uncompressedSize BigInt @default(0)
crc32 String?
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
@@index([packageId])
@@index([extension])
@@index([fileName])
@@map("package_files")
}
model IngestionRun {
id String @id @default(cuid())
accountId String
status IngestionStatus @default(RUNNING)
startedAt DateTime @default(now())
finishedAt DateTime?
messagesScanned Int @default(0)
zipsFound Int @default(0)
zipsDuplicate Int @default(0)
zipsIngested Int @default(0)
errorMessage String?
// Live activity tracking — written by worker in real-time
currentActivity String? // Human-readable: "Downloading pack.zip (part 2/5)"
currentStep String? // Machine-readable step key
currentChannel String? // Channel title being processed
currentFile String? // File currently being processed
currentFileNum Int? // Which archive set (1-indexed)
totalFiles Int? // Total archive sets found
downloadedBytes BigInt? // Current download progress in bytes
totalBytes BigInt? // Total size of current download
downloadPercent Int? // 0-100
lastActivityAt DateTime? // When activity was last updated
account TelegramAccount @relation(fields: [accountId], references: [id])
packages Package[]
@@index([accountId])
@@index([status])
@@index([startedAt])
@@map("ingestion_runs")
}
model TopicProgress {
id String @id @default(cuid())
accountChannelMapId String
topicId BigInt
topicName String?
lastProcessedMessageId BigInt?
accountChannelMap AccountChannelMap @relation(fields: [accountChannelMapId], references: [id], onDelete: Cascade)
@@unique([accountChannelMapId, topicId])
@@index([accountChannelMapId])
@@map("topic_progress")
}
model GlobalSetting {
key String @id @db.VarChar(64)
value String @db.Text
updatedAt DateTime @updatedAt
@@map("global_settings")
}
model InviteCode {
id String @id @default(cuid())
code String @unique @db.VarChar(32)
maxUses Int @default(1)
uses Int @default(0)
expiresAt DateTime?
createdBy String
createdAt DateTime @default(now())
creator User @relation(fields: [createdBy], references: [id], onDelete: Cascade)
@@index([code])
@@map("invite_codes")
}
model ChannelFetchRequest {
id String @id @default(cuid())
accountId String
status FetchStatus @default(PENDING)
resultJson String? @db.Text
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@index([accountId, status])
@@map("channel_fetch_requests")
}
// ───────────────────────────────────────
// Telegram Bot models
// ───────────────────────────────────────
enum BotSendStatus {
PENDING
SENDING
SENT
FAILED
}
/// Links a NextAuth user to a Telegram user ID.
/// Created when a user sends /link <code> to the bot.
model TelegramLink {
id String @id @default(cuid())
userId String @unique
telegramUserId BigInt @unique
telegramName String? @db.VarChar(128)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
sendRequests BotSendRequest[]
@@map("telegram_links")
}
/// A queued request to send a package to a Telegram user via the bot.
model BotSendRequest {
id String @id @default(cuid())
packageId String
telegramLinkId String
requestedByUserId String
status BotSendStatus @default(PENDING)
error String?
createdAt DateTime @default(now())
completedAt DateTime?
package Package @relation(fields: [packageId], references: [id])
telegramLink TelegramLink @relation(fields: [telegramLinkId], references: [id], onDelete: Cascade)
@@index([status])
@@index([telegramLinkId])
@@index([createdAt])
@@map("bot_send_requests")
}
/// Subscriptions for new-package notifications via the bot.
model BotSubscription {
id String @id @default(cuid())
telegramUserId BigInt
pattern String @db.VarChar(256)
createdAt DateTime @default(now())
@@unique([telegramUserId, pattern])
@@index([telegramUserId])
@@map("bot_subscriptions")
}