diff --git a/docs/postgresql-migration.md b/docs/postgresql-migration.md new file mode 100644 index 0000000..aa47b92 --- /dev/null +++ b/docs/postgresql-migration.md @@ -0,0 +1,130 @@ +# PostgreSQL Migration Documentation + +## Overview + +Successfully migrated the livedash-node application from SQLite to PostgreSQL using Neon as the database provider. This migration provides better scalability, performance, and production-readiness. + +## Migration Summary + +### What Was Changed + +1. **Database Provider**: Changed from SQLite to PostgreSQL in `prisma/schema.prisma` +2. **Environment Configuration**: Updated to use environment-based database URL selection +3. **Test Setup**: Configured separate test database using `DATABASE_URL_TEST` +4. **Migration History**: Reset and created fresh PostgreSQL migrations + +### Database Configuration + +#### Production/Development + +- **Provider**: PostgreSQL (Neon) +- **Environment Variable**: `DATABASE_URL` +- **Connection**: Neon PostgreSQL cluster + +#### Testing + +- **Provider**: PostgreSQL (Neon - separate database) +- **Environment Variable**: `DATABASE_URL_TEST` +- **Test Setup**: Automatically switches to test database during test runs + +### Files Modified + +1. **`prisma/schema.prisma`** + +- Changed provider from `sqlite` to `postgresql` +- Updated URL to use `env("DATABASE_URL")` + +2. **`tests/setup.ts`** + +- Added logic to use `DATABASE_URL_TEST` when available +- Ensures test isolation with separate database + +3. **`.env`** (created) + +- Contains `DATABASE_URL` for Prisma CLI operations + +4. **`.env.local`** (existing) + +- Contains both `DATABASE_URL` and `DATABASE_URL_TEST` + +### Database Schema + +All existing models and relationships were preserved: + +- **Company**: Multi-tenant root entity +- **User**: Authentication and authorization +- **Session**: Processed session data +- **SessionImport**: Raw CSV import data +- **Message**: Individual conversation messages +- **Question**: Normalized question storage +- **SessionQuestion**: Session-question relationships +- **AIProcessingRequest**: AI cost tracking + +### Migration Process + +1. **Schema Update**: Changed provider to PostgreSQL +2. **Migration Reset**: Removed SQLite migration history +3. **Fresh Migration**: Created new PostgreSQL migration +4. **Client Generation**: Generated new Prisma client for PostgreSQL +5. **Database Seeding**: Applied initial seed data +6. **Testing**: Verified all functionality works with PostgreSQL + +### Benefits Achieved + +✅ **Production-Ready**: PostgreSQL is enterprise-grade and scalable +✅ **Better Performance**: Superior query performance and optimization +✅ **Advanced Features**: Full JSON support, arrays, advanced indexing +✅ **Test Isolation**: Separate test database prevents data conflicts +✅ **Consistency**: Same database engine across all environments +✅ **Cloud-Native**: Neon provides managed PostgreSQL with excellent DX + +### Environment Variables + +```env +# Production/Development Database +DATABASE_URL="postgresql://user:pass@host/database?sslmode=require" + +# Test Database (separate Neon database) +DATABASE_URL_TEST="postgresql://user:pass@test-host/test-database?sslmode=require" +``` + +### Test Configuration + +Tests automatically use the test database when `DATABASE_URL_TEST` is set: + +```typescript +// In tests/setup.ts +if (process.env.DATABASE_URL_TEST) { + process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; +} +``` + +### Verification + +All tests pass successfully: + +- ✅ Environment configuration tests +- ✅ Transcript fetcher tests +- ✅ Database connection tests +- ✅ Schema validation tests +- ✅ CRUD operation tests + +### Next Steps + +1. **Data Import**: Import production data if needed +2. **Performance Monitoring**: Monitor query performance in production +3. **Backup Strategy**: Configure automated backups via Neon +4. **Connection Pooling**: Consider connection pooling for high-traffic scenarios + +### Rollback Plan + +If rollback is needed: + +1. Revert `prisma/schema.prisma` to SQLite configuration +2. Restore SQLite migration files from git history +3. Update environment variables +4. Run `prisma migrate reset` and `prisma generate` + +## Conclusion + +The PostgreSQL migration was successful and provides a solid foundation for production deployment. The application now benefits from PostgreSQL's advanced features while maintaining full test isolation and development workflow compatibility. diff --git a/prisma/migrations/20250627185818_initial_refactored_schema/migration.sql b/prisma/migrations/20250627185818_initial_refactored_schema/migration.sql deleted file mode 100644 index 86dd815..0000000 --- a/prisma/migrations/20250627185818_initial_refactored_schema/migration.sql +++ /dev/null @@ -1,183 +0,0 @@ --- CreateTable -CREATE TABLE "Company" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL, - "csvUrl" TEXT NOT NULL, - "csvUsername" TEXT, - "csvPassword" TEXT, - "sentimentAlert" REAL, - "dashboardOpts" JSONB, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL PRIMARY KEY, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" TEXT NOT NULL DEFAULT 'USER', - "companyId" TEXT NOT NULL, - "resetToken" TEXT, - "resetTokenExpiry" DATETIME, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL PRIMARY KEY, - "companyId" TEXT NOT NULL, - "importId" TEXT, - "startTime" DATETIME NOT NULL, - "endTime" DATETIME NOT NULL, - "ipAddress" TEXT, - "country" TEXT, - "fullTranscriptUrl" TEXT, - "avgResponseTime" REAL, - "initialMsg" TEXT, - "language" TEXT, - "messagesSent" INTEGER, - "sentiment" TEXT, - "escalated" BOOLEAN, - "forwardedHr" BOOLEAN, - "category" TEXT, - "summary" TEXT, - "processed" BOOLEAN NOT NULL DEFAULT false, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Session_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "Session_importId_fkey" FOREIGN KEY ("importId") REFERENCES "SessionImport" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "SessionImport" ( - "id" TEXT NOT NULL PRIMARY KEY, - "companyId" TEXT NOT NULL, - "externalSessionId" TEXT NOT NULL, - "startTimeRaw" TEXT NOT NULL, - "endTimeRaw" TEXT NOT NULL, - "ipAddress" TEXT, - "countryCode" TEXT, - "language" TEXT, - "messagesSent" INTEGER, - "sentimentRaw" TEXT, - "escalatedRaw" TEXT, - "forwardedHrRaw" TEXT, - "fullTranscriptUrl" TEXT, - "avgResponseTimeSeconds" REAL, - "tokens" INTEGER, - "tokensEur" REAL, - "category" TEXT, - "initialMessage" TEXT, - "rawTranscriptContent" TEXT, - "status" TEXT NOT NULL DEFAULT 'QUEUED', - "errorMsg" TEXT, - "processedAt" DATETIME, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "SessionImport_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Message" ( - "id" TEXT NOT NULL PRIMARY KEY, - "sessionId" TEXT NOT NULL, - "timestamp" DATETIME, - "role" TEXT NOT NULL, - "content" TEXT NOT NULL, - "order" INTEGER NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Message_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Question" ( - "id" TEXT NOT NULL PRIMARY KEY, - "content" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- CreateTable -CREATE TABLE "SessionQuestion" ( - "id" TEXT NOT NULL PRIMARY KEY, - "sessionId" TEXT NOT NULL, - "questionId" TEXT NOT NULL, - "order" INTEGER NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "SessionQuestion_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "SessionQuestion_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "AIProcessingRequest" ( - "id" TEXT NOT NULL PRIMARY KEY, - "sessionId" TEXT NOT NULL, - "openaiRequestId" TEXT, - "model" TEXT NOT NULL, - "serviceTier" TEXT, - "systemFingerprint" TEXT, - "promptTokens" INTEGER NOT NULL, - "completionTokens" INTEGER NOT NULL, - "totalTokens" INTEGER NOT NULL, - "cachedTokens" INTEGER, - "audioTokensPrompt" INTEGER, - "reasoningTokens" INTEGER, - "audioTokensCompletion" INTEGER, - "acceptedPredictionTokens" INTEGER, - "rejectedPredictionTokens" INTEGER, - "promptTokenCost" REAL NOT NULL, - "completionTokenCost" REAL NOT NULL, - "totalCostEur" REAL NOT NULL, - "processingType" TEXT NOT NULL, - "success" BOOLEAN NOT NULL, - "errorMessage" TEXT, - "requestedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "completedAt" DATETIME, - CONSTRAINT "AIProcessingRequest_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Session_importId_key" ON "Session"("importId"); - --- CreateIndex -CREATE INDEX "Session_companyId_startTime_idx" ON "Session"("companyId", "startTime"); - --- CreateIndex -CREATE UNIQUE INDEX "SessionImport_externalSessionId_key" ON "SessionImport"("externalSessionId"); - --- CreateIndex -CREATE INDEX "SessionImport_status_idx" ON "SessionImport"("status"); - --- CreateIndex -CREATE UNIQUE INDEX "SessionImport_companyId_externalSessionId_key" ON "SessionImport"("companyId", "externalSessionId"); - --- CreateIndex -CREATE INDEX "Message_sessionId_order_idx" ON "Message"("sessionId", "order"); - --- CreateIndex -CREATE UNIQUE INDEX "Message_sessionId_order_key" ON "Message"("sessionId", "order"); - --- CreateIndex -CREATE UNIQUE INDEX "Question_content_key" ON "Question"("content"); - --- CreateIndex -CREATE INDEX "SessionQuestion_sessionId_idx" ON "SessionQuestion"("sessionId"); - --- CreateIndex -CREATE UNIQUE INDEX "SessionQuestion_sessionId_questionId_key" ON "SessionQuestion"("sessionId", "questionId"); - --- CreateIndex -CREATE UNIQUE INDEX "SessionQuestion_sessionId_order_key" ON "SessionQuestion"("sessionId", "order"); - --- CreateIndex -CREATE INDEX "AIProcessingRequest_sessionId_idx" ON "AIProcessingRequest"("sessionId"); - --- CreateIndex -CREATE INDEX "AIProcessingRequest_requestedAt_idx" ON "AIProcessingRequest"("requestedAt"); - --- CreateIndex -CREATE INDEX "AIProcessingRequest_model_idx" ON "AIProcessingRequest"("model"); diff --git a/prisma/migrations/20250627192234_initial_postgresql_schema/migration.sql b/prisma/migrations/20250627192234_initial_postgresql_schema/migration.sql new file mode 100644 index 0000000..f58beb8 --- /dev/null +++ b/prisma/migrations/20250627192234_initial_postgresql_schema/migration.sql @@ -0,0 +1,227 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER', 'AUDITOR'); + +-- CreateEnum +CREATE TYPE "SentimentCategory" AS ENUM ('POSITIVE', 'NEUTRAL', 'NEGATIVE'); + +-- CreateEnum +CREATE TYPE "SessionCategory" AS ENUM ('SCHEDULE_HOURS', 'LEAVE_VACATION', 'SICK_LEAVE_RECOVERY', 'SALARY_COMPENSATION', 'CONTRACT_HOURS', 'ONBOARDING', 'OFFBOARDING', 'WORKWEAR_STAFF_PASS', 'TEAM_CONTACTS', 'PERSONAL_QUESTIONS', 'ACCESS_LOGIN', 'SOCIAL_QUESTIONS', 'UNRECOGNIZED_OTHER'); + +-- CreateEnum +CREATE TYPE "ImportStatus" AS ENUM ('QUEUED', 'PROCESSING', 'DONE', 'ERROR'); + +-- CreateTable +CREATE TABLE "Company" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "csvUrl" TEXT NOT NULL, + "csvUsername" TEXT, + "csvPassword" TEXT, + "sentimentAlert" DOUBLE PRECISION, + "dashboardOpts" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'USER', + "companyId" TEXT NOT NULL, + "resetToken" TEXT, + "resetTokenExpiry" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "companyId" TEXT NOT NULL, + "importId" TEXT, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "ipAddress" TEXT, + "country" TEXT, + "fullTranscriptUrl" TEXT, + "avgResponseTime" DOUBLE PRECISION, + "initialMsg" TEXT, + "language" TEXT, + "messagesSent" INTEGER, + "sentiment" "SentimentCategory", + "escalated" BOOLEAN, + "forwardedHr" BOOLEAN, + "category" "SessionCategory", + "summary" TEXT, + "processed" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SessionImport" ( + "id" TEXT NOT NULL, + "companyId" TEXT NOT NULL, + "externalSessionId" TEXT NOT NULL, + "startTimeRaw" TEXT NOT NULL, + "endTimeRaw" TEXT NOT NULL, + "ipAddress" TEXT, + "countryCode" TEXT, + "language" TEXT, + "messagesSent" INTEGER, + "sentimentRaw" TEXT, + "escalatedRaw" TEXT, + "forwardedHrRaw" TEXT, + "fullTranscriptUrl" TEXT, + "avgResponseTimeSeconds" DOUBLE PRECISION, + "tokens" INTEGER, + "tokensEur" DOUBLE PRECISION, + "category" TEXT, + "initialMessage" TEXT, + "rawTranscriptContent" TEXT, + "status" "ImportStatus" NOT NULL DEFAULT 'QUEUED', + "errorMsg" TEXT, + "processedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SessionImport_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "timestamp" TIMESTAMP(3), + "role" TEXT NOT NULL, + "content" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Question" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Question_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SessionQuestion" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SessionQuestion_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AIProcessingRequest" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "openaiRequestId" TEXT, + "model" TEXT NOT NULL, + "serviceTier" TEXT, + "systemFingerprint" TEXT, + "promptTokens" INTEGER NOT NULL, + "completionTokens" INTEGER NOT NULL, + "totalTokens" INTEGER NOT NULL, + "cachedTokens" INTEGER, + "audioTokensPrompt" INTEGER, + "reasoningTokens" INTEGER, + "audioTokensCompletion" INTEGER, + "acceptedPredictionTokens" INTEGER, + "rejectedPredictionTokens" INTEGER, + "promptTokenCost" DOUBLE PRECISION NOT NULL, + "completionTokenCost" DOUBLE PRECISION NOT NULL, + "totalCostEur" DOUBLE PRECISION NOT NULL, + "processingType" TEXT NOT NULL, + "success" BOOLEAN NOT NULL, + "errorMessage" TEXT, + "requestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completedAt" TIMESTAMP(3), + + CONSTRAINT "AIProcessingRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_importId_key" ON "Session"("importId"); + +-- CreateIndex +CREATE INDEX "Session_companyId_startTime_idx" ON "Session"("companyId", "startTime"); + +-- CreateIndex +CREATE UNIQUE INDEX "SessionImport_externalSessionId_key" ON "SessionImport"("externalSessionId"); + +-- CreateIndex +CREATE INDEX "SessionImport_status_idx" ON "SessionImport"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "SessionImport_companyId_externalSessionId_key" ON "SessionImport"("companyId", "externalSessionId"); + +-- CreateIndex +CREATE INDEX "Message_sessionId_order_idx" ON "Message"("sessionId", "order"); + +-- CreateIndex +CREATE UNIQUE INDEX "Message_sessionId_order_key" ON "Message"("sessionId", "order"); + +-- CreateIndex +CREATE UNIQUE INDEX "Question_content_key" ON "Question"("content"); + +-- CreateIndex +CREATE INDEX "SessionQuestion_sessionId_idx" ON "SessionQuestion"("sessionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SessionQuestion_sessionId_questionId_key" ON "SessionQuestion"("sessionId", "questionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SessionQuestion_sessionId_order_key" ON "SessionQuestion"("sessionId", "order"); + +-- CreateIndex +CREATE INDEX "AIProcessingRequest_sessionId_idx" ON "AIProcessingRequest"("sessionId"); + +-- CreateIndex +CREATE INDEX "AIProcessingRequest_requestedAt_idx" ON "AIProcessingRequest"("requestedAt"); + +-- CreateIndex +CREATE INDEX "AIProcessingRequest_model_idx" ON "AIProcessingRequest"("model"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_importId_fkey" FOREIGN KEY ("importId") REFERENCES "SessionImport"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SessionImport" ADD CONSTRAINT "SessionImport_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SessionQuestion" ADD CONSTRAINT "SessionQuestion_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SessionQuestion" ADD CONSTRAINT "SessionQuestion_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "Question"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AIProcessingRequest" ADD CONSTRAINT "AIProcessingRequest_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 2a5a444..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "sqlite" +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index adc58cb..b8690d6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,8 +3,8 @@ generator client { } datasource db { - provider = "sqlite" // still OK for local/dev; use Postgres in prod - url = "file:./dev.db" + provider = "postgresql" + url = env("DATABASE_URL") } /** diff --git a/tests/setup.ts b/tests/setup.ts index 162248a..3af0e8b 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -13,6 +13,11 @@ global.console = { process.env.NEXTAUTH_SECRET = 'test-secret'; process.env.NEXTAUTH_URL = 'http://localhost:3000'; +// Use test database for all database operations during tests +if (process.env.DATABASE_URL_TEST) { + process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; +} + // Mock node-fetch for transcript fetcher tests vi.mock('node-fetch', () => ({ default: vi.fn(), diff --git a/tests/unit/database.test.ts b/tests/unit/database.test.ts new file mode 100644 index 0000000..a131c9b --- /dev/null +++ b/tests/unit/database.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { PrismaClient } from '@prisma/client'; + +describe('Database Configuration', () => { + let prisma: PrismaClient; + + beforeAll(() => { + prisma = new PrismaClient(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + it('should connect to the test database', async () => { + // Verify we can connect to the database + const result = await prisma.$queryRaw`SELECT 1 as test`; + expect(result).toBeDefined(); + }); + + it('should use PostgreSQL as the database provider', async () => { + // Query the database to verify it's PostgreSQL + const result = await prisma.$queryRaw`SELECT version()` as any[]; + expect(result[0].version).toContain('PostgreSQL'); + }); + + it('should be using the test database URL', () => { + // Verify that DATABASE_URL is set to the test database + expect(process.env.DATABASE_URL).toBeDefined(); + expect(process.env.DATABASE_URL).toContain('postgresql://'); + + // If DATABASE_URL_TEST is set, DATABASE_URL should match it (from our test setup) + if (process.env.DATABASE_URL_TEST) { + expect(process.env.DATABASE_URL).toBe(process.env.DATABASE_URL_TEST); + } + }); + + it('should have all required tables', async () => { + // Verify all our tables exist + const tables = await prisma.$queryRaw` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + ` as any[]; + + const tableNames = tables.map(t => t.table_name); + + expect(tableNames).toContain('Company'); + expect(tableNames).toContain('User'); + expect(tableNames).toContain('Session'); + expect(tableNames).toContain('SessionImport'); + expect(tableNames).toContain('Message'); + expect(tableNames).toContain('Question'); + expect(tableNames).toContain('SessionQuestion'); + expect(tableNames).toContain('AIProcessingRequest'); + }); + + it('should be able to create and query data', async () => { + // Test basic CRUD operations + const company = await prisma.company.create({ + data: { + name: 'Test Company', + csvUrl: 'https://example.com/test.csv', + }, + }); + + expect(company.id).toBeDefined(); + expect(company.name).toBe('Test Company'); + + // Clean up + await prisma.company.delete({ + where: { id: company.id }, + }); + }); +});