mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 15:32:10 +01:00
DB refactor
This commit is contained in:
@ -0,0 +1,63 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AIModel" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"maxTokens" INTEGER,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AIModel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AIModelPricing" (
|
||||
"id" TEXT NOT NULL,
|
||||
"aiModelId" TEXT NOT NULL,
|
||||
"promptTokenCost" DOUBLE PRECISION NOT NULL,
|
||||
"completionTokenCost" DOUBLE PRECISION NOT NULL,
|
||||
"effectiveFrom" TIMESTAMP(3) NOT NULL,
|
||||
"effectiveUntil" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AIModelPricing_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CompanyAIModel" (
|
||||
"id" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"aiModelId" TEXT NOT NULL,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "CompanyAIModel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AIModel_name_key" ON "AIModel"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AIModel_provider_isActive_idx" ON "AIModel"("provider", "isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AIModelPricing_aiModelId_effectiveFrom_idx" ON "AIModelPricing"("aiModelId", "effectiveFrom");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AIModelPricing_effectiveFrom_effectiveUntil_idx" ON "AIModelPricing"("effectiveFrom", "effectiveUntil");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CompanyAIModel_companyId_isDefault_idx" ON "CompanyAIModel"("companyId", "isDefault");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CompanyAIModel_companyId_aiModelId_key" ON "CompanyAIModel"("companyId", "aiModelId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AIModelPricing" ADD CONSTRAINT "AIModelPricing_aiModelId_fkey" FOREIGN KEY ("aiModelId") REFERENCES "AIModel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyAIModel" ADD CONSTRAINT "CompanyAIModel_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyAIModel" ADD CONSTRAINT "CompanyAIModel_aiModelId_fkey" FOREIGN KEY ("aiModelId") REFERENCES "AIModel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -1,10 +1,12 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["driverAdapters"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DATABASE_URL_DIRECT")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,6 +40,22 @@ enum SessionCategory {
|
||||
UNRECOGNIZED_OTHER
|
||||
}
|
||||
|
||||
enum ProcessingStage {
|
||||
CSV_IMPORT // SessionImport created
|
||||
TRANSCRIPT_FETCH // Transcript content fetched
|
||||
SESSION_CREATION // Session + Messages created
|
||||
AI_ANALYSIS // AI processing completed
|
||||
QUESTION_EXTRACTION // Questions extracted
|
||||
}
|
||||
|
||||
enum ProcessingStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
FAILED
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
/**
|
||||
* COMPANY (multi-tenant root)
|
||||
*/
|
||||
@ -50,9 +68,10 @@ model Company {
|
||||
sentimentAlert Float?
|
||||
dashboardOpts Json? // JSON column instead of opaque string
|
||||
|
||||
users User[] @relation("CompanyUsers")
|
||||
sessions Session[]
|
||||
imports SessionImport[]
|
||||
users User[] @relation("CompanyUsers")
|
||||
sessions Session[]
|
||||
imports SessionImport[]
|
||||
companyAiModels CompanyAIModel[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -118,9 +137,6 @@ model Session {
|
||||
|
||||
// AI-generated fields
|
||||
summary String? // AI-generated summary
|
||||
|
||||
// Processing metadata
|
||||
processed Boolean @default(false)
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
@ -128,6 +144,7 @@ model Session {
|
||||
messages Message[] // Individual conversation messages
|
||||
sessionQuestions SessionQuestion[] // Questions asked in this session
|
||||
aiProcessingRequests AIProcessingRequest[] // AI processing cost tracking
|
||||
processingStatus SessionProcessingStatus[] // Processing pipeline status
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@ -136,15 +153,8 @@ model Session {
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. Raw CSV row waiting to be processed ----------
|
||||
* 2. Raw CSV row (pure data storage) ----------
|
||||
*/
|
||||
enum ImportStatus {
|
||||
QUEUED
|
||||
PROCESSING
|
||||
DONE
|
||||
ERROR
|
||||
}
|
||||
|
||||
model SessionImport {
|
||||
id String @id @default(uuid())
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
@ -177,13 +187,9 @@ model SessionImport {
|
||||
rawTranscriptContent String? // Fetched content from fullTranscriptUrl
|
||||
|
||||
// ─── bookkeeping ─────────────────────────────────
|
||||
status ImportStatus @default(QUEUED)
|
||||
errorMsg String?
|
||||
processedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([companyId, externalSessionId]) // idempotent re-imports
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,6 +212,30 @@ model Message {
|
||||
@@index([sessionId, order])
|
||||
}
|
||||
|
||||
/**
|
||||
* UNIFIED PROCESSING STATUS TRACKING
|
||||
*/
|
||||
model SessionProcessingStatus {
|
||||
id String @id @default(uuid())
|
||||
sessionId String
|
||||
stage ProcessingStage
|
||||
status ProcessingStatus @default(PENDING)
|
||||
|
||||
startedAt DateTime?
|
||||
completedAt DateTime?
|
||||
errorMessage String?
|
||||
retryCount Int @default(0)
|
||||
|
||||
// Stage-specific metadata (e.g., AI costs, token usage, fetch details)
|
||||
metadata Json?
|
||||
|
||||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([sessionId, stage])
|
||||
@@index([stage, status])
|
||||
@@index([sessionId])
|
||||
}
|
||||
|
||||
/**
|
||||
* QUESTION MANAGEMENT (separate from Session for better analytics)
|
||||
*/
|
||||
@ -281,3 +311,66 @@ model AIProcessingRequest {
|
||||
@@index([requestedAt])
|
||||
@@index([model])
|
||||
}
|
||||
|
||||
/**
|
||||
* AI MODEL MANAGEMENT SYSTEM
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI Model definitions (without pricing)
|
||||
*/
|
||||
model AIModel {
|
||||
id String @id @default(uuid())
|
||||
name String @unique // "gpt-4o", "gpt-4-turbo", etc.
|
||||
provider String // "openai", "anthropic", etc.
|
||||
maxTokens Int? // Maximum tokens for this model
|
||||
isActive Boolean @default(true)
|
||||
|
||||
// Relationships
|
||||
pricing AIModelPricing[]
|
||||
companyModels CompanyAIModel[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([provider, isActive])
|
||||
}
|
||||
|
||||
/**
|
||||
* Time-based pricing for AI models
|
||||
*/
|
||||
model AIModelPricing {
|
||||
id String @id @default(uuid())
|
||||
aiModelId String
|
||||
promptTokenCost Float // Cost per prompt token in USD
|
||||
completionTokenCost Float // Cost per completion token in USD
|
||||
effectiveFrom DateTime // When this pricing becomes effective
|
||||
effectiveUntil DateTime? // When this pricing expires (null = current)
|
||||
|
||||
// Relationships
|
||||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@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) // Is this the default model for the company?
|
||||
|
||||
// Relationships
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([companyId, aiModelId]) // Prevent duplicate assignments
|
||||
@@index([companyId, isDefault])
|
||||
}
|
||||
|
||||
127
prisma/seed.ts
127
prisma/seed.ts
@ -1,4 +1,4 @@
|
||||
// seed.ts - Create initial admin user and company
|
||||
// seed.ts - Create initial admin user, company, and AI models
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
@ -6,30 +6,133 @@ const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Create a company
|
||||
console.log("🌱 Starting database seeding...");
|
||||
|
||||
// Create the Jumbo company
|
||||
const company = await prisma.company.create({
|
||||
data: {
|
||||
name: "Demo Company",
|
||||
csvUrl: "https://proto.notso.ai/jumbo/chats", // Replace with a real URL if available
|
||||
name: "Jumbo Bas Bobbeldijk",
|
||||
csvUrl: "https://proto.notso.ai/jumbo/chats",
|
||||
csvUsername: "jumboadmin",
|
||||
csvPassword: "jumboadmin",
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created company: ${company.name}`);
|
||||
|
||||
// Create an admin user
|
||||
const hashedPassword = await bcrypt.hash("admin123", 10);
|
||||
await prisma.user.create({
|
||||
// Create admin user
|
||||
const hashedPassword = await bcrypt.hash("8QbL26tB7fWS", 10);
|
||||
const adminUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "admin@demo.com",
|
||||
email: "max.kowalski.contact@gmail.com",
|
||||
password: hashedPassword,
|
||||
role: "ADMIN",
|
||||
companyId: company.id,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created admin user: ${adminUser.email}`);
|
||||
|
||||
// Create AI Models
|
||||
const aiModels = [
|
||||
{
|
||||
name: "gpt-4o",
|
||||
provider: "openai",
|
||||
maxTokens: 128000,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4o-2024-08-06",
|
||||
provider: "openai",
|
||||
maxTokens: 128000,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-turbo",
|
||||
provider: "openai",
|
||||
maxTokens: 128000,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-4o-mini",
|
||||
provider: "openai",
|
||||
maxTokens: 128000,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
const createdModels: any[] = [];
|
||||
for (const modelData of aiModels) {
|
||||
const model = await prisma.aIModel.create({
|
||||
data: modelData,
|
||||
});
|
||||
createdModels.push(model);
|
||||
console.log(`✅ Created AI model: ${model.name}`);
|
||||
}
|
||||
|
||||
// Create current pricing for AI models (as of December 2024)
|
||||
const currentTime = new Date();
|
||||
const pricingData = [
|
||||
{
|
||||
modelName: "gpt-4o",
|
||||
promptTokenCost: 0.0000025, // $2.50 per 1M tokens
|
||||
completionTokenCost: 0.00001, // $10.00 per 1M tokens
|
||||
},
|
||||
{
|
||||
modelName: "gpt-4o-2024-08-06",
|
||||
promptTokenCost: 0.0000025, // $2.50 per 1M tokens
|
||||
completionTokenCost: 0.00001, // $10.00 per 1M tokens
|
||||
},
|
||||
{
|
||||
modelName: "gpt-4-turbo",
|
||||
promptTokenCost: 0.00001, // $10.00 per 1M tokens
|
||||
completionTokenCost: 0.00003, // $30.00 per 1M tokens
|
||||
},
|
||||
{
|
||||
modelName: "gpt-4o-mini",
|
||||
promptTokenCost: 0.00000015, // $0.15 per 1M tokens
|
||||
completionTokenCost: 0.0000006, // $0.60 per 1M tokens
|
||||
},
|
||||
];
|
||||
|
||||
for (const pricing of pricingData) {
|
||||
const model = createdModels.find(m => m.name === pricing.modelName);
|
||||
if (model) {
|
||||
await prisma.aIModelPricing.create({
|
||||
data: {
|
||||
aiModelId: model.id,
|
||||
promptTokenCost: pricing.promptTokenCost,
|
||||
completionTokenCost: pricing.completionTokenCost,
|
||||
effectiveFrom: currentTime,
|
||||
effectiveUntil: null, // Current pricing
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created pricing for: ${model.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign default AI model to company (gpt-4o)
|
||||
const defaultModel = createdModels.find(m => m.name === "gpt-4o");
|
||||
if (defaultModel) {
|
||||
await prisma.companyAIModel.create({
|
||||
data: {
|
||||
companyId: company.id,
|
||||
aiModelId: defaultModel.id,
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Set default AI model for company: ${defaultModel.name}`);
|
||||
}
|
||||
|
||||
console.log("\n🎉 Database seeding completed successfully!");
|
||||
console.log("\n📋 Summary:");
|
||||
console.log(`Company: ${company.name}`);
|
||||
console.log(`Admin user: ${adminUser.email}`);
|
||||
console.log(`Password: 8QbL26tB7fWS`);
|
||||
console.log(`AI Models: ${createdModels.length} models created with current pricing`);
|
||||
console.log(`Default model: ${defaultModel?.name}`);
|
||||
console.log("\n🚀 Ready to start importing CSV data!");
|
||||
|
||||
console.log("Seed data created successfully:");
|
||||
console.log("Company: Demo Company");
|
||||
console.log("Admin user: admin@demo.com (password: admin123)");
|
||||
} catch (error) {
|
||||
console.error("Error seeding database:", error);
|
||||
console.error("❌ Error seeding database:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
|
||||
Reference in New Issue
Block a user