Files
livedash-node/prisma/schema.prisma
Kaj Kowalski e027dc9565 refactor: enhance Prisma schema with PostgreSQL optimizations and data integrity
- 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
2025-06-28 03:22:53 +02:00

320 lines
14 KiB
Plaintext
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"
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
}