mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 06:32:10 +01:00
- Add PostgreSQL-specific data types (@db.VarChar, @db.Text, @db.Timestamptz, @db.JsonB, @db.Inet) - Implement comprehensive database constraints via custom migration - Add detailed field-level documentation and enum descriptions - Optimize indexes for common query patterns and company-scoped data - Ensure data integrity with check constraints for positive values and logical time validation - Add partial indexes for performance optimization on failed/pending processing sessions
320 lines
14 KiB
Plaintext
320 lines
14 KiB
Plaintext
generator client {
|
||
provider = "prisma-client-js"
|
||
previewFeatures = ["driverAdapters"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
directUrl = env("DATABASE_URL_DIRECT")
|
||
}
|
||
|
||
/// *
|
||
/// * COMPANY (multi-tenant root)
|
||
/// * Root entity for multi-tenant architecture
|
||
/// * Each company has isolated data with own users, sessions, and AI model configurations
|
||
model Company {
|
||
id String @id @default(uuid())
|
||
name String @db.VarChar(255) /// Company name for display and filtering
|
||
csvUrl String @db.Text /// URL endpoint for CSV data import
|
||
csvUsername String? @db.VarChar(255) /// Optional HTTP auth username for CSV endpoint
|
||
csvPassword String? @db.VarChar(255) /// Optional HTTP auth password for CSV endpoint
|
||
dashboardOpts Json? @db.JsonB /// Company-specific dashboard configuration (theme, layout, etc.)
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||
companyAiModels CompanyAIModel[] /// AI models assigned to this company
|
||
sessions Session[] /// All processed sessions for this company
|
||
imports SessionImport[] /// Raw CSV import data for this company
|
||
users User[] @relation("CompanyUsers") /// Users belonging to this company
|
||
|
||
@@index([name])
|
||
}
|
||
|
||
/// *
|
||
/// * USER (authentication accounts)
|
||
/// * Application users with role-based access control
|
||
/// * Each user belongs to exactly one company for data isolation
|
||
model User {
|
||
id String @id @default(uuid())
|
||
email String @unique @db.VarChar(255) /// User email address, must be unique across all companies
|
||
password String @db.VarChar(255) /// Hashed password for authentication
|
||
role UserRole @default(USER) /// User permission level within their company
|
||
companyId String /// Foreign key to Company - enforces data isolation
|
||
resetToken String? @db.VarChar(255) /// Temporary token for password reset functionality
|
||
resetTokenExpiry DateTime? @db.Timestamptz(6) /// Expiration time for reset token
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||
company Company @relation("CompanyUsers", fields: [companyId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([companyId])
|
||
@@index([email])
|
||
}
|
||
|
||
/// *
|
||
/// * SESSION (processed conversation data)
|
||
/// * Normalized session data derived from raw CSV imports
|
||
/// * Contains AI-enhanced data like sentiment analysis and categorization
|
||
/// * 1:1 relationship with SessionImport via importId
|
||
model Session {
|
||
id String @id @default(uuid())
|
||
companyId String /// Foreign key to Company for data isolation
|
||
importId String? @unique /// Optional 1:1 link to source SessionImport record
|
||
/// Session timing and basic data
|
||
startTime DateTime @db.Timestamptz(6) /// When the conversation started
|
||
endTime DateTime @db.Timestamptz(6) /// When the conversation ended
|
||
ipAddress String? @db.Inet /// Client IP address (IPv4/IPv6)
|
||
country String? @db.VarChar(3) /// ISO 3166-1 alpha-3 country code
|
||
fullTranscriptUrl String? @db.Text /// URL to external transcript source
|
||
avgResponseTime Float? @db.Real /// Average response time in seconds
|
||
initialMsg String? @db.Text /// First message in the conversation
|
||
language String? @db.VarChar(10) /// ISO 639 language code
|
||
messagesSent Int? /// Total number of messages in session
|
||
/// AI-enhanced analysis fields
|
||
sentiment SentimentCategory? /// AI-determined overall sentiment
|
||
escalated Boolean? /// Whether session was escalated to human
|
||
forwardedHr Boolean? /// Whether session was forwarded to HR
|
||
category SessionCategory? /// AI-determined conversation category
|
||
summary String? @db.Text /// AI-generated session summary
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||
/// Related data
|
||
aiProcessingRequests AIProcessingRequest[] /// All AI API calls made for this session
|
||
messages Message[] /// Individual messages in conversation order
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
import SessionImport? @relation("ImportToSession", fields: [importId], references: [id])
|
||
processingStatus SessionProcessingStatus[] /// Pipeline stage tracking
|
||
sessionQuestions SessionQuestion[] /// Questions extracted from conversation
|
||
|
||
@@index([companyId, startTime]) /// Primary query pattern: company sessions by time
|
||
@@index([companyId, sentiment]) /// Filter sessions by sentiment within company
|
||
@@index([companyId, category]) /// Filter sessions by category within company
|
||
}
|
||
|
||
/// *
|
||
/// * 2. Raw CSV row (pure data storage) ----------
|
||
model SessionImport {
|
||
id String @id @default(uuid())
|
||
companyId String
|
||
externalSessionId String
|
||
startTimeRaw String @db.VarChar(255)
|
||
endTimeRaw String @db.VarChar(255)
|
||
ipAddress String? @db.VarChar(45)
|
||
countryCode String? @db.VarChar(3)
|
||
language String? @db.VarChar(10)
|
||
messagesSent Int?
|
||
sentimentRaw String? @db.VarChar(50)
|
||
escalatedRaw String? @db.VarChar(50)
|
||
forwardedHrRaw String? @db.VarChar(50)
|
||
fullTranscriptUrl String? @db.Text
|
||
avgResponseTimeSeconds Float? @db.Real
|
||
tokens Int?
|
||
tokensEur Float? @db.Real
|
||
category String? @db.VarChar(255)
|
||
initialMessage String? @db.Text
|
||
rawTranscriptContent String? @db.Text
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
session Session? @relation("ImportToSession")
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([companyId, externalSessionId])
|
||
@@index([companyId])
|
||
@@index([companyId, createdAt])
|
||
}
|
||
|
||
/// *
|
||
/// * MESSAGE (individual lines)
|
||
model Message {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
timestamp DateTime? @db.Timestamptz(6)
|
||
role String @db.VarChar(50)
|
||
content String @db.Text
|
||
order Int
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, order])
|
||
@@index([sessionId, order])
|
||
@@index([sessionId, timestamp])
|
||
}
|
||
|
||
/// *
|
||
/// * UNIFIED PROCESSING STATUS TRACKING
|
||
model SessionProcessingStatus {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
stage ProcessingStage
|
||
status ProcessingStatus @default(PENDING)
|
||
startedAt DateTime? @db.Timestamptz(6)
|
||
completedAt DateTime? @db.Timestamptz(6)
|
||
errorMessage String? @db.Text
|
||
retryCount Int @default(0)
|
||
metadata Json? @db.JsonB
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, stage])
|
||
@@index([stage, status])
|
||
@@index([sessionId])
|
||
@@index([status, startedAt])
|
||
}
|
||
|
||
/// *
|
||
/// * QUESTION MANAGEMENT (separate from Session for better analytics)
|
||
model Question {
|
||
id String @id @default(uuid())
|
||
content String @unique @db.Text
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
sessionQuestions SessionQuestion[]
|
||
}
|
||
|
||
model SessionQuestion {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
questionId String
|
||
order Int
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
question Question @relation(fields: [questionId], references: [id])
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, questionId])
|
||
@@unique([sessionId, order])
|
||
@@index([sessionId])
|
||
@@index([questionId])
|
||
}
|
||
|
||
/// *
|
||
/// * AI PROCESSING COST TRACKING
|
||
model AIProcessingRequest {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
openaiRequestId String? @db.VarChar(255)
|
||
model String @db.VarChar(100)
|
||
serviceTier String? @db.VarChar(50)
|
||
systemFingerprint String? @db.VarChar(255)
|
||
promptTokens Int
|
||
completionTokens Int
|
||
totalTokens Int
|
||
cachedTokens Int?
|
||
audioTokensPrompt Int?
|
||
reasoningTokens Int?
|
||
audioTokensCompletion Int?
|
||
acceptedPredictionTokens Int?
|
||
rejectedPredictionTokens Int?
|
||
promptTokenCost Float @db.Real
|
||
completionTokenCost Float @db.Real
|
||
totalCostEur Float @db.Real
|
||
processingType String @db.VarChar(100)
|
||
success Boolean
|
||
errorMessage String? @db.Text
|
||
requestedAt DateTime @default(now()) @db.Timestamptz(6)
|
||
completedAt DateTime? @db.Timestamptz(6)
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([sessionId])
|
||
@@index([sessionId, requestedAt])
|
||
@@index([requestedAt])
|
||
@@index([model])
|
||
@@index([success, requestedAt])
|
||
}
|
||
|
||
/// *
|
||
/// * AI Model definitions (without pricing)
|
||
model AIModel {
|
||
id String @id @default(uuid())
|
||
name String @unique @db.VarChar(100)
|
||
provider String @db.VarChar(50)
|
||
maxTokens Int?
|
||
isActive Boolean @default(true)
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||
pricing AIModelPricing[]
|
||
companyModels CompanyAIModel[]
|
||
|
||
@@index([provider, isActive])
|
||
@@index([name])
|
||
}
|
||
|
||
/// *
|
||
/// * Time-based pricing for AI models
|
||
model AIModelPricing {
|
||
id String @id @default(uuid())
|
||
aiModelId String
|
||
promptTokenCost Float @db.Real
|
||
completionTokenCost Float @db.Real
|
||
effectiveFrom DateTime @db.Timestamptz(6)
|
||
effectiveUntil DateTime? @db.Timestamptz(6)
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([aiModelId, effectiveFrom])
|
||
@@index([effectiveFrom, effectiveUntil])
|
||
}
|
||
|
||
/// *
|
||
/// * Company-specific AI model assignments
|
||
model CompanyAIModel {
|
||
id String @id @default(uuid())
|
||
companyId String
|
||
aiModelId String
|
||
isDefault Boolean @default(false)
|
||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([companyId, aiModelId])
|
||
@@index([companyId, isDefault])
|
||
}
|
||
|
||
/// *
|
||
/// * ENUMS – typed constants for better data integrity
|
||
///
|
||
|
||
/// User permission levels within a company
|
||
enum UserRole {
|
||
ADMIN /// Full access to company data and settings
|
||
USER /// Standard access to view and interact with data
|
||
AUDITOR /// Read-only access for compliance and auditing
|
||
}
|
||
|
||
/// AI-determined sentiment categories for sessions
|
||
enum SentimentCategory {
|
||
POSITIVE /// Customer expressed satisfaction or positive emotions
|
||
NEUTRAL /// Neutral tone or mixed emotions
|
||
NEGATIVE /// Customer expressed frustration or negative emotions
|
||
}
|
||
|
||
/// AI-determined conversation categories based on content analysis
|
||
enum SessionCategory {
|
||
SCHEDULE_HOURS /// Questions about work schedules and hours
|
||
LEAVE_VACATION /// Vacation requests and leave policies
|
||
SICK_LEAVE_RECOVERY /// Sick leave and recovery-related discussions
|
||
SALARY_COMPENSATION /// Salary, benefits, and compensation questions
|
||
CONTRACT_HOURS /// Contract terms and working hours
|
||
ONBOARDING /// New employee onboarding processes
|
||
OFFBOARDING /// Employee departure and offboarding
|
||
WORKWEAR_STAFF_PASS /// Equipment, uniforms, and access cards
|
||
TEAM_CONTACTS /// Team directory and contact information
|
||
PERSONAL_QUESTIONS /// Personal HR matters and private concerns
|
||
ACCESS_LOGIN /// System access and login issues
|
||
SOCIAL_QUESTIONS /// Social events and company culture
|
||
UNRECOGNIZED_OTHER /// Conversations that don't fit other categories
|
||
}
|
||
|
||
/// Processing pipeline stages for session data transformation
|
||
enum ProcessingStage {
|
||
CSV_IMPORT /// Initial import of raw CSV data into SessionImport
|
||
TRANSCRIPT_FETCH /// Fetching transcript content from external URLs
|
||
SESSION_CREATION /// Converting SessionImport to normalized Session
|
||
AI_ANALYSIS /// AI processing for sentiment, categorization, summaries
|
||
QUESTION_EXTRACTION /// Extracting questions from conversation content
|
||
}
|
||
|
||
/// Status of each processing stage
|
||
enum ProcessingStatus {
|
||
PENDING /// Stage is queued for processing
|
||
IN_PROGRESS /// Stage is currently being processed
|
||
COMPLETED /// Stage completed successfully
|
||
FAILED /// Stage failed with errors
|
||
SKIPPED /// Stage was intentionally skipped
|
||
}
|