This commit is contained in:
60
.github/workflows/deploy.yml
vendored
Normal file
60
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
name: Deploy Apartment API
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build -t apartment-api:${{ github.sha }} .
|
||||
docker tag apartment-api:${{ github.sha }} apartment-api:latest
|
||||
|
||||
- name: Deploy to server
|
||||
run: |
|
||||
# Copy files to deployment directory
|
||||
mkdir -p /media/stephen/Storage_Linux/infrastructure/services/apartment-api
|
||||
cp -r . /media/stephen/Storage_Linux/infrastructure/services/apartment-api/
|
||||
|
||||
# Navigate to deployment directory
|
||||
cd /media/stephen/Storage_Linux/infrastructure/services/apartment-api
|
||||
|
||||
# Stop existing container if running
|
||||
docker compose down || true
|
||||
|
||||
# Start new container
|
||||
docker compose up -d
|
||||
|
||||
# Clean up old images
|
||||
docker image prune -f
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Wait for container to start
|
||||
sleep 15
|
||||
|
||||
# Check if container is running
|
||||
docker ps | grep apartment-api
|
||||
|
||||
# Test health endpoint
|
||||
curl -f http://localhost:3000/health || echo "API may still be starting..."
|
||||
|
||||
# Test API endpoint
|
||||
curl -f http://localhost:3000/api/daily-summary || echo "API may still be loading data..."
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
|
||||
# Change ownership of the app directory
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
USER nodejs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
||||
services:
|
||||
apartment-api:
|
||||
build: .
|
||||
container_name: apartment-api
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=8080 # Changed from 3000 to 8080
|
||||
- MONGO_URI=mongodb://admin:password123@mongodb:27017
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.apartment-api.rule=Host(`apartments.maverickapplications.com`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.apartment-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.apartment-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.apartment-api.loadbalancer.server.port=8080" # Updated to match
|
||||
- "traefik.http.middlewares.apartment-api-stripprefix.stripprefix.prefixes=/api"
|
||||
- "traefik.http.routers.apartment-api.middlewares=apartment-api-stripprefix"
|
||||
networks:
|
||||
- traefik
|
||||
- mongodb_mongodb_network
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
mongodb_mongodb_network:
|
||||
external: true
|
||||
1399
package-lock.json
generated
Normal file
1399
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "apartment-api",
|
||||
"version": "1.0.0",
|
||||
"description": "API backend for Country Club Towers & Gardens apartment dashboard",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"apartments",
|
||||
"api",
|
||||
"real-estate",
|
||||
"monitoring"
|
||||
],
|
||||
"author": "Stephen",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongodb": "^6.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"express-rate-limit": "^7.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
504
server.js
Normal file
504
server.js
Normal file
@ -0,0 +1,504 @@
|
||||
const express = require('express');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const cors = require('cors');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Configuration
|
||||
const MONGO_URI = process.env.MONGO_URI || "mongodb://admin:password123@localhost:27017";
|
||||
const DB_NAME = "apartments";
|
||||
const UNITS_COLLECTION = "units_migration_test";
|
||||
const PRICES_COLLECTION = "unit_prices_migration_test";
|
||||
const DAILY_SUMMARIES_COLLECTION = "daily_summaries";
|
||||
|
||||
console.log(`🔗 Connecting to MongoDB: ${MONGO_URI.includes('localhost') ? 'Local MongoDB' : 'Atlas MongoDB'}`);
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// MongoDB connection
|
||||
let db;
|
||||
let client;
|
||||
|
||||
async function connectToMongoDB() {
|
||||
try {
|
||||
client = new MongoClient(MONGO_URI);
|
||||
await client.connect();
|
||||
db = client.db(DB_NAME);
|
||||
console.log('✅ Successfully connected to MongoDB');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to MongoDB:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get today's date
|
||||
const getTodayDate = () => new Date().toISOString().split('T')[0];
|
||||
|
||||
// Helper function to get yesterday's date
|
||||
const getYesterdayDate = () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return yesterday.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'apartment-api'
|
||||
});
|
||||
});
|
||||
|
||||
// Get daily summary (matches your daily_summaries collection)
|
||||
app.get('/api/daily-summary', async (req, res) => {
|
||||
try {
|
||||
const today = getTodayDate();
|
||||
|
||||
// Try to get today's summary first
|
||||
let summary = await db.collection(DAILY_SUMMARIES_COLLECTION)
|
||||
.findOne({ date: today });
|
||||
|
||||
// If no summary for today, get the most recent one
|
||||
if (!summary) {
|
||||
summary = await db.collection(DAILY_SUMMARIES_COLLECTION)
|
||||
.findOne({}, { sort: { date: -1 } });
|
||||
}
|
||||
|
||||
// If still no summary, create a basic one
|
||||
if (!summary) {
|
||||
// Get basic counts for today
|
||||
const todayPriceCount = await db.collection(PRICES_COLLECTION)
|
||||
.countDocuments({ date_checked: today });
|
||||
|
||||
summary = {
|
||||
date: today,
|
||||
newUnits: 0,
|
||||
rentedUnits: 0,
|
||||
staleUnits: 0,
|
||||
netChange: 0,
|
||||
turnoverRate: 0,
|
||||
totalAvailable: todayPriceCount
|
||||
};
|
||||
}
|
||||
|
||||
res.json(summary);
|
||||
} catch (error) {
|
||||
console.error('Error fetching daily summary:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch daily summary' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get price history (last 15 days of average prices)
|
||||
app.get('/api/price-history', async (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 15;
|
||||
|
||||
// Generate date range
|
||||
const dates = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(date.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
const priceHistory = [];
|
||||
|
||||
for (const date of dates) {
|
||||
// Get all prices for this date (excluding furnished units)
|
||||
const dailyPrices = await db.collection(PRICES_COLLECTION).aggregate([
|
||||
{ $match: { date_checked: date } },
|
||||
{
|
||||
$lookup: {
|
||||
from: UNITS_COLLECTION,
|
||||
localField: "unit_code",
|
||||
foreignField: "unit_code",
|
||||
as: "unit_info"
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"unit_info.plan_name": { $not: { $regex: "Furnished", $options: "i" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgPrice: { $avg: "$price" },
|
||||
unitCount: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
]).toArray();
|
||||
|
||||
priceHistory.push({
|
||||
date: date,
|
||||
avgPrice: dailyPrices.length > 0 ? Math.round(dailyPrices[0].avgPrice) : null,
|
||||
unitCount: dailyPrices.length > 0 ? dailyPrices[0].unitCount : 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json(priceHistory);
|
||||
} catch (error) {
|
||||
console.error('Error fetching price history:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch price history' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get available units with current prices
|
||||
app.get('/api/available-units', async (req, res) => {
|
||||
try {
|
||||
const today = getTodayDate();
|
||||
|
||||
// Get units that have prices today (excluding furnished units)
|
||||
const availableUnits = await db.collection(UNITS_COLLECTION).aggregate([
|
||||
{
|
||||
$match: {
|
||||
available: true,
|
||||
plan_name: { $not: { $regex: "Furnished", $options: "i" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: PRICES_COLLECTION,
|
||||
localField: "unit_code",
|
||||
foreignField: "unit_code",
|
||||
as: "price_info"
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"price_info.date_checked": today
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
currentPrice: {
|
||||
$arrayElemAt: [
|
||||
{ $filter: { input: "$price_info", cond: { $eq: ["$$this.date_checked", today] } } },
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: PRICES_COLLECTION,
|
||||
localField: "unit_code",
|
||||
foreignField: "unit_code",
|
||||
as: "all_prices"
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
priceStats: {
|
||||
$let: {
|
||||
vars: {
|
||||
prices: { $map: { input: "$all_prices", as: "p", in: "$$p.price" } }
|
||||
},
|
||||
in: {
|
||||
minPrice: { $min: "$$prices" },
|
||||
maxPrice: { $max: "$$prices" },
|
||||
priceHistory: {
|
||||
$slice: [
|
||||
{ $sortArray: { input: "$all_prices", sortBy: { date_checked: -1 } } },
|
||||
5
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
unitCode: "$unit_code",
|
||||
planName: "$plan_name",
|
||||
bedCount: "$bed_count",
|
||||
bathCount: "$bath_count",
|
||||
area: "$area",
|
||||
community: "$community",
|
||||
available: "$available",
|
||||
currentPrice: "$currentPrice.price",
|
||||
minPrice: "$priceStats.minPrice",
|
||||
maxPrice: "$priceStats.maxPrice",
|
||||
daysAvailable: {
|
||||
$dateDiff: {
|
||||
startDate: { $dateFromString: { dateString: { $arrayElemAt: ["$priceStats.priceHistory.date_checked", -1] } } },
|
||||
endDate: { $dateFromString: { dateString: today } },
|
||||
unit: "day"
|
||||
}
|
||||
},
|
||||
priceHistory: {
|
||||
$map: {
|
||||
input: "$priceStats.priceHistory",
|
||||
as: "ph",
|
||||
in: {
|
||||
date: "$$ph.date_checked",
|
||||
price: "$$ph.price"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $sort: { planName: 1, unitCode: 1 } }
|
||||
]).toArray();
|
||||
|
||||
res.json(availableUnits);
|
||||
} catch (error) {
|
||||
console.error('Error fetching available units:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch available units' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent activity (new units, rented units, price changes)
|
||||
app.get('/api/recent-activity', async (req, res) => {
|
||||
try {
|
||||
const today = getTodayDate();
|
||||
const yesterday = getYesterdayDate();
|
||||
|
||||
// Get today's summary for new/rented units
|
||||
const todaySummary = await db.collection(DAILY_SUMMARIES_COLLECTION)
|
||||
.findOne({ date: today });
|
||||
|
||||
const activity = [];
|
||||
|
||||
if (todaySummary) {
|
||||
// Add new units
|
||||
if (todaySummary.new_units && todaySummary.new_units.length > 0) {
|
||||
for (const unitCode of todaySummary.new_units.slice(0, 5)) { // Limit to 5 most recent
|
||||
const unitInfo = await db.collection(UNITS_COLLECTION)
|
||||
.findOne({ unit_code: unitCode });
|
||||
const priceInfo = await db.collection(PRICES_COLLECTION)
|
||||
.findOne({ unit_code: unitCode, date_checked: today });
|
||||
|
||||
if (unitInfo) {
|
||||
activity.push({
|
||||
type: 'new',
|
||||
unitCode: unitCode,
|
||||
planName: unitInfo.plan_name,
|
||||
price: priceInfo ? priceInfo.price : null,
|
||||
date: today,
|
||||
bedCount: unitInfo.bed_count,
|
||||
bathCount: unitInfo.bath_count
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add rented units
|
||||
if (todaySummary.rented_units && todaySummary.rented_units.length > 0) {
|
||||
for (const unitCode of todaySummary.rented_units.slice(0, 5)) { // Limit to 5 most recent
|
||||
const unitInfo = await db.collection(UNITS_COLLECTION)
|
||||
.findOne({ unit_code: unitCode });
|
||||
const priceInfo = await db.collection(PRICES_COLLECTION)
|
||||
.findOne({ unit_code: unitCode, date_checked: yesterday });
|
||||
|
||||
if (unitInfo) {
|
||||
activity.push({
|
||||
type: 'rented',
|
||||
unitCode: unitCode,
|
||||
planName: unitInfo.plan_name,
|
||||
price: priceInfo ? priceInfo.price : null,
|
||||
date: today,
|
||||
bedCount: unitInfo.bed_count,
|
||||
bathCount: unitInfo.bath_count
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get recent price changes (units with different prices yesterday vs today)
|
||||
const priceChanges = await db.collection(PRICES_COLLECTION).aggregate([
|
||||
{
|
||||
$match: {
|
||||
date_checked: { $in: [today, yesterday] }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$unit_code",
|
||||
prices: { $push: { date: "$date_checked", price: "$price" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"prices.1": { $exists: true } // Must have at least 2 price records
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
todayPrice: {
|
||||
$arrayElemAt: [
|
||||
{ $map: { input: { $filter: { input: "$prices", cond: { $eq: ["$$this.date", today] } } }, as: "p", in: "$$p.price" } },
|
||||
0
|
||||
]
|
||||
},
|
||||
yesterdayPrice: {
|
||||
$arrayElemAt: [
|
||||
{ $map: { input: { $filter: { input: "$prices", cond: { $eq: ["$$this.date", yesterday] } } }, as: "p", in: "$$p.price" } },
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
$expr: { $ne: ["$todayPrice", "$yesterdayPrice"] }
|
||||
}
|
||||
},
|
||||
{ $limit: 3 } // Limit to 3 price changes
|
||||
]).toArray();
|
||||
|
||||
// Add price changes to activity
|
||||
for (const change of priceChanges) {
|
||||
const unitInfo = await db.collection(UNITS_COLLECTION)
|
||||
.findOne({ unit_code: change._id });
|
||||
|
||||
if (unitInfo && change.todayPrice < change.yesterdayPrice) { // Only show price drops
|
||||
activity.push({
|
||||
type: 'price_drop',
|
||||
unitCode: change._id,
|
||||
planName: unitInfo.plan_name,
|
||||
oldPrice: change.yesterdayPrice,
|
||||
newPrice: change.todayPrice,
|
||||
date: today,
|
||||
bedCount: unitInfo.bed_count,
|
||||
bathCount: unitInfo.bath_count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by most recent first
|
||||
activity.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
res.json(activity.slice(0, 10)); // Return max 10 activities
|
||||
} catch (error) {
|
||||
console.error('Error fetching recent activity:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch recent activity' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get plan statistics
|
||||
app.get('/api/plan-stats', async (req, res) => {
|
||||
try {
|
||||
const today = getTodayDate();
|
||||
|
||||
const planStats = await db.collection(UNITS_COLLECTION).aggregate([
|
||||
{
|
||||
$match: {
|
||||
available: true,
|
||||
plan_name: { $not: { $regex: "Furnished", $options: "i" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: PRICES_COLLECTION,
|
||||
localField: "unit_code",
|
||||
foreignField: "unit_code",
|
||||
as: "price_info"
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"price_info.date_checked": today
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
currentPrice: {
|
||||
$arrayElemAt: [
|
||||
{ $map: { input: { $filter: { input: "$price_info", cond: { $eq: ["$$this.date_checked", today] } } }, as: "p", in: "$$p.price" } },
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$plan_name",
|
||||
count: { $sum: 1 },
|
||||
avgPrice: { $avg: "$currentPrice" },
|
||||
avgArea: { $avg: "$area" }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
name: "$_id",
|
||||
count: 1,
|
||||
avgPrice: { $round: ["$avgPrice", 0] },
|
||||
avgArea: { $round: ["$avgArea", 0] },
|
||||
_id: 0
|
||||
}
|
||||
},
|
||||
{ $sort: { count: -1 } }
|
||||
]).toArray();
|
||||
|
||||
res.json(planStats);
|
||||
} catch (error) {
|
||||
console.error('Error fetching plan stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch plan stats' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((error, req, res, next) => {
|
||||
console.error('Unhandled error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Endpoint not found' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
async function startServer() {
|
||||
await connectToMongoDB();
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`🚀 Apartment API server running on port ${PORT}`);
|
||||
console.log(`📊 Available endpoints:`);
|
||||
console.log(` GET /health - Health check`);
|
||||
console.log(` GET /api/daily-summary - Daily apartment summary`);
|
||||
console.log(` GET /api/price-history - Price trends (15 days)`);
|
||||
console.log(` GET /api/available-units - Currently available units`);
|
||||
console.log(` GET /api/recent-activity - Recent market activity`);
|
||||
console.log(` GET /api/plan-stats - Statistics by floor plan`);
|
||||
});
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('🔄 Shutting down gracefully...');
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('✅ MongoDB connection closed');
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('🔄 Received SIGTERM, shutting down gracefully...');
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('✅ MongoDB connection closed');
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
startServer().catch(error => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user