diff --git a/backend/.env.example b/backend/.env.example index a881675..849bb45 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,8 +2,8 @@ NODE_ENV=development PORT=3000 -# Database (future) -# DATABASE_URL=postgresql://user:password@db:5432/spotlightcam +# Database +DATABASE_URL=postgresql://spotlightcam:spotlightcam123@db:5432/spotlightcam # JWT (future) # JWT_SECRET=your-secret-key-here diff --git a/backend/Dockerfile b/backend/Dockerfile index 1baca55..2cc6ad5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,8 @@ FROM node:20-alpine +# Install OpenSSL for Prisma +RUN apk add --no-cache openssl + # Set working directory WORKDIR /app diff --git a/backend/package-lock.json b/backend/package-lock.json index 5baa012..a389b39 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@prisma/client": "^5.8.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" @@ -16,6 +17,7 @@ "devDependencies": { "jest": "^29.7.0", "nodemon": "^3.0.2", + "prisma": "^5.8.0", "supertest": "^6.3.3" } }, @@ -977,6 +979,74 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3930,6 +4000,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/backend/package.json b/backend/package.json index 45d9274..6e79da2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,11 @@ "start": "node src/server.js", "dev": "nodemon src/server.js", "test": "jest --coverage", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "node prisma/seed.js", + "prisma:studio": "prisma studio" }, "keywords": ["webrtc", "p2p", "video", "dance", "matchmaking"], "author": "", @@ -15,12 +19,14 @@ "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "@prisma/client": "^5.8.0" }, "devDependencies": { "nodemon": "^3.0.2", "jest": "^29.7.0", - "supertest": "^6.3.3" + "supertest": "^6.3.3", + "prisma": "^5.8.0" }, "jest": { "testEnvironment": "node", diff --git a/backend/prisma/migrations/20251112205214_init/migration.sql b/backend/prisma/migrations/20251112205214_init/migration.sql new file mode 100644 index 0000000..1f84557 --- /dev/null +++ b/backend/prisma/migrations/20251112205214_init/migration.sql @@ -0,0 +1,139 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "username" VARCHAR(50) NOT NULL, + "email" VARCHAR(255) NOT NULL, + "password_hash" VARCHAR(255) NOT NULL, + "avatar" VARCHAR(255), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "events" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255) NOT NULL, + "location" VARCHAR(255) NOT NULL, + "start_date" DATE NOT NULL, + "end_date" DATE NOT NULL, + "worldsdc_id" VARCHAR(100), + "participants_count" INTEGER NOT NULL DEFAULT 0, + "description" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "chat_rooms" ( + "id" SERIAL NOT NULL, + "event_id" INTEGER, + "type" VARCHAR(20) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_rooms_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "messages" ( + "id" SERIAL NOT NULL, + "room_id" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "type" VARCHAR(20) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "messages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "matches" ( + "id" SERIAL NOT NULL, + "user1_id" INTEGER NOT NULL, + "user2_id" INTEGER NOT NULL, + "event_id" INTEGER NOT NULL, + "room_id" INTEGER, + "status" VARCHAR(20) NOT NULL DEFAULT 'pending', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "matches_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ratings" ( + "id" SERIAL NOT NULL, + "match_id" INTEGER NOT NULL, + "rater_id" INTEGER NOT NULL, + "rated_id" INTEGER NOT NULL, + "score" INTEGER NOT NULL, + "comment" TEXT, + "would_collaborate_again" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ratings_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "events_worldsdc_id_key" ON "events"("worldsdc_id"); + +-- CreateIndex +CREATE INDEX "messages_room_id_idx" ON "messages"("room_id"); + +-- CreateIndex +CREATE INDEX "messages_created_at_idx" ON "messages"("created_at"); + +-- CreateIndex +CREATE INDEX "matches_user1_id_idx" ON "matches"("user1_id"); + +-- CreateIndex +CREATE INDEX "matches_user2_id_idx" ON "matches"("user2_id"); + +-- CreateIndex +CREATE INDEX "matches_event_id_idx" ON "matches"("event_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "matches_user1_id_user2_id_event_id_key" ON "matches"("user1_id", "user2_id", "event_id"); + +-- CreateIndex +CREATE INDEX "ratings_rated_id_idx" ON "ratings"("rated_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "ratings_match_id_rater_id_rated_id_key" ON "ratings"("match_id", "rater_id", "rated_id"); + +-- AddForeignKey +ALTER TABLE "chat_rooms" ADD CONSTRAINT "chat_rooms_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "chat_rooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_user1_id_fkey" FOREIGN KEY ("user1_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_user2_id_fkey" FOREIGN KEY ("user2_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "chat_rooms"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ratings" ADD CONSTRAINT "ratings_match_id_fkey" FOREIGN KEY ("match_id") REFERENCES "matches"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ratings" ADD CONSTRAINT "ratings_rater_id_fkey" FOREIGN KEY ("rater_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ratings" ADD CONSTRAINT "ratings_rated_id_fkey" FOREIGN KEY ("rated_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..29ceb9a --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,128 @@ +// Prisma schema for spotlight.cam +// Database: PostgreSQL 15 + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// Users table +model User { + id Int @id @default(autoincrement()) + username String @unique @db.VarChar(50) + email String @unique @db.VarChar(255) + passwordHash String @map("password_hash") @db.VarChar(255) + avatar String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relations + messages Message[] + matchesAsUser1 Match[] @relation("MatchUser1") + matchesAsUser2 Match[] @relation("MatchUser2") + ratingsGiven Rating[] @relation("RaterRatings") + ratingsReceived Rating[] @relation("RatedRatings") + + @@map("users") +} + +// Events table (dance events from worldsdc.com) +model Event { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + location String @db.VarChar(255) + startDate DateTime @map("start_date") @db.Date + endDate DateTime @map("end_date") @db.Date + worldsdcId String? @unique @map("worldsdc_id") @db.VarChar(100) + participantsCount Int @default(0) @map("participants_count") + description String? @db.Text + createdAt DateTime @default(now()) @map("created_at") + + // Relations + chatRooms ChatRoom[] + matches Match[] + + @@map("events") +} + +// Chat rooms (event chat and private 1:1 chat) +model ChatRoom { + id Int @id @default(autoincrement()) + eventId Int? @map("event_id") + type String @db.VarChar(20) // 'event' or 'private' + createdAt DateTime @default(now()) @map("created_at") + + // Relations + event Event? @relation(fields: [eventId], references: [id]) + messages Message[] + matches Match[] + + @@map("chat_rooms") +} + +// Messages (text messages and video links) +model Message { + id Int @id @default(autoincrement()) + roomId Int @map("room_id") + userId Int @map("user_id") + content String @db.Text + type String @db.VarChar(20) // 'text', 'link', 'video' + createdAt DateTime @default(now()) @map("created_at") + + // Relations + room ChatRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@index([roomId]) + @@index([createdAt]) + @@map("messages") +} + +// Matches (pairs of users for collaboration) +model Match { + id Int @id @default(autoincrement()) + user1Id Int @map("user1_id") + user2Id Int @map("user2_id") + eventId Int @map("event_id") + roomId Int? @map("room_id") + status String @default("pending") @db.VarChar(20) // 'pending', 'accepted', 'completed' + createdAt DateTime @default(now()) @map("created_at") + + // Relations + user1 User @relation("MatchUser1", fields: [user1Id], references: [id]) + user2 User @relation("MatchUser2", fields: [user2Id], references: [id]) + event Event @relation(fields: [eventId], references: [id]) + room ChatRoom? @relation(fields: [roomId], references: [id]) + ratings Rating[] + + @@unique([user1Id, user2Id, eventId]) + @@index([user1Id]) + @@index([user2Id]) + @@index([eventId]) + @@map("matches") +} + +// Ratings (user ratings after collaboration) +model Rating { + id Int @id @default(autoincrement()) + matchId Int @map("match_id") + raterId Int @map("rater_id") + ratedId Int @map("rated_id") + score Int // 1-5 + comment String? @db.Text + wouldCollaborateAgain Boolean @default(false) @map("would_collaborate_again") + createdAt DateTime @default(now()) @map("created_at") + + // Relations + match Match @relation(fields: [matchId], references: [id]) + rater User @relation("RaterRatings", fields: [raterId], references: [id]) + rated User @relation("RatedRatings", fields: [ratedId], references: [id]) + + @@unique([matchId, raterId, ratedId]) + @@index([ratedId]) + @@map("ratings") +} diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js new file mode 100644 index 0000000..df25f21 --- /dev/null +++ b/backend/prisma/seed.js @@ -0,0 +1,86 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Seeding database...'); + + // Create events + const events = await Promise.all([ + prisma.event.create({ + data: { + name: 'Warsaw Dance Festival 2025', + location: 'Warsaw, Poland', + startDate: new Date('2025-03-15'), + endDate: new Date('2025-03-17'), + worldsdcId: 'wdf-2025', + participantsCount: 156, + description: 'The biggest West Coast Swing event in Central Europe', + }, + }), + prisma.event.create({ + data: { + name: 'Swing Camp Barcelona 2025', + location: 'Barcelona, Spain', + startDate: new Date('2025-04-20'), + endDate: new Date('2025-04-23'), + worldsdcId: 'scb-2025', + participantsCount: 203, + description: 'International swing dance camp with workshops and socials', + }, + }), + prisma.event.create({ + data: { + name: 'Blues Week Herräng 2025', + location: 'Herräng, Sweden', + startDate: new Date('2025-07-14'), + endDate: new Date('2025-07-20'), + worldsdcId: 'bwh-2025', + participantsCount: 89, + description: 'Week-long blues dance intensive in the heart of Sweden', + }, + }), + prisma.event.create({ + data: { + name: 'Krakow Swing Connection 2025', + location: 'Krakow, Poland', + startDate: new Date('2025-05-10'), + endDate: new Date('2025-05-12'), + worldsdcId: 'ksc-2025', + participantsCount: 127, + description: 'Three days of swing dancing in historic Krakow', + }, + }), + ]); + + console.log(`✅ Created ${events.length} events`); + + // Create event chat rooms for each event + const chatRooms = await Promise.all( + events.map((event) => + prisma.chatRoom.create({ + data: { + eventId: event.id, + type: 'event', + }, + }) + ) + ); + + console.log(`✅ Created ${chatRooms.length} event chat rooms`); + + console.log('🎉 Seeding completed successfully!'); + console.log(''); + console.log('Created:'); + console.log(` - ${events.length} events`); + console.log(` - ${chatRooms.length} chat rooms`); +} + +main() + .catch((e) => { + console.error('❌ Seeding failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/src/app.js b/backend/src/app.js index f1db16e..0bcaaa4 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -27,10 +27,10 @@ app.get('/api/health', (req, res) => { }); }); -// API routes (future) +// API routes +app.use('/api/events', require('./routes/events')); // app.use('/api/auth', require('./routes/auth')); // app.use('/api/users', require('./routes/users')); -// app.use('/api/events', require('./routes/events')); // app.use('/api/matches', require('./routes/matches')); // app.use('/api/ratings', require('./routes/ratings')); diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js new file mode 100644 index 0000000..9a57a55 --- /dev/null +++ b/backend/src/routes/events.js @@ -0,0 +1,71 @@ +const express = require('express'); +const { prisma } = require('../utils/db'); + +const router = express.Router(); + +// GET /api/events - List all events +router.get('/', async (req, res, next) => { + try { + const events = await prisma.event.findMany({ + orderBy: { + startDate: 'asc', + }, + select: { + id: true, + name: true, + location: true, + startDate: true, + endDate: true, + worldsdcId: true, + participantsCount: true, + description: true, + createdAt: true, + }, + }); + + res.json({ + success: true, + count: events.length, + data: events, + }); + } catch (error) { + next(error); + } +}); + +// GET /api/events/:id - Get event by ID +router.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + const event = await prisma.event.findUnique({ + where: { + id: parseInt(id), + }, + include: { + chatRooms: true, + _count: { + select: { + matches: true, + }, + }, + }, + }); + + if (!event) { + return res.status(404).json({ + success: false, + error: 'Event not found', + }); + } + + res.json({ + success: true, + data: event, + }); + } catch (error) { + next(error); + } +}); + +module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js index 24e86c6..075260f 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,31 +1,40 @@ require('dotenv').config(); const app = require('./app'); +const { testConnection, disconnect } = require('./utils/db'); const PORT = process.env.PORT || 3000; -const server = app.listen(PORT, '0.0.0.0', () => { - console.log('================================='); - console.log('🚀 spotlight.cam Backend Started'); - console.log('================================='); - console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); - console.log(`Server running on port: ${PORT}`); - console.log(`Health check: http://localhost:${PORT}/api/health`); - console.log('================================='); +async function startServer() { + // Test database connection + await testConnection(); + + const server = app.listen(PORT, '0.0.0.0', () => { + console.log('================================='); + console.log('🚀 spotlight.cam Backend Started'); + console.log('================================='); + console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); + console.log(`Server running on port: ${PORT}`); + console.log(`Health check: http://localhost:${PORT}/api/health`); + console.log('================================='); + }); + + return server; +} + +startServer().catch((err) => { + console.error('Failed to start server:', err); + process.exit(1); }); // Graceful shutdown -process.on('SIGTERM', () => { +process.on('SIGTERM', async () => { console.log('SIGTERM received, shutting down gracefully...'); - server.close(() => { - console.log('Server closed'); - process.exit(0); - }); + await disconnect(); + process.exit(0); }); -process.on('SIGINT', () => { +process.on('SIGINT', async () => { console.log('SIGINT received, shutting down gracefully...'); - server.close(() => { - console.log('Server closed'); - process.exit(0); - }); + await disconnect(); + process.exit(0); }); diff --git a/backend/src/utils/db.js b/backend/src/utils/db.js new file mode 100644 index 0000000..9cae6bc --- /dev/null +++ b/backend/src/utils/db.js @@ -0,0 +1,41 @@ +const { PrismaClient } = require('@prisma/client'); + +// Singleton instance of Prisma Client +let prisma; + +if (process.env.NODE_ENV === 'production') { + prisma = new PrismaClient(); +} else { + // In development, use a global variable to prevent multiple instances + // during hot reload + if (!global.prisma) { + global.prisma = new PrismaClient({ + log: ['error', 'warn'], + }); + } + prisma = global.prisma; +} + +// Test database connection +async function testConnection() { + try { + await prisma.$connect(); + console.log('✅ Database connected successfully'); + return true; + } catch (error) { + console.error('❌ Database connection failed:', error.message); + return false; + } +} + +// Graceful shutdown +async function disconnect() { + await prisma.$disconnect(); + console.log('Database disconnected'); +} + +module.exports = { + prisma, + testConnection, + disconnect, +}; diff --git a/docker-compose.yml b/docker-compose.yml index cc6fda1..effa3dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,4 +43,23 @@ services: - NODE_ENV=development - PORT=3000 - CORS_ORIGIN=http://localhost:8080 + - DATABASE_URL=postgresql://spotlightcam:spotlightcam123@db:5432/spotlightcam + depends_on: + - db restart: unless-stopped + + db: + image: postgres:15-alpine + container_name: spotlightcam-db + environment: + - POSTGRES_USER=spotlightcam + - POSTGRES_PASSWORD=spotlightcam123 + - POSTGRES_DB=spotlightcam + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: unless-stopped + +volumes: + postgres_data: