Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions server/express/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": ["<rootDir>/src", "<rootDir>/tests"],
"testMatch": [
"**/__tests__/**/*.ts",
"**/?(*.)+(spec|test).ts"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
"collectCoverageFrom": [
"src/**/*.ts",
"!src/**/*.d.ts"
],
"coverageDirectory": "coverage",
"coverageReporters": [
"text",
"lcov",
"html"
],
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"]
}
24 changes: 14 additions & 10 deletions server/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@
"license": "Apache-2.0",
"author": "Jordan Smith",
"type": "commonjs",
"main": "dist/app.ts",
"main": "dist/app.js",
"scripts": {
"build": "tsc",
"start": "node dist/app.js",
"start": "node dist/src/app.js",
"dev": "ts-node src/app.ts",
"test-setup": "ts-node src/test-setup.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"dependencies": {
"express": "^5.1.0",
"mongodb": "^6.3.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"cors": "^2.8.5"
"express": "^5.1.0",
"mongodb": "^6.3.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.14",
"@types/node": "^20.10.5",
"@types/cors": "^2.8.17",
"typescript": "^5.3.3",
"ts-node": "^10.9.2"
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
78 changes: 42 additions & 36 deletions server/express/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
/**
* Express.js Backend for MongoDB Sample MFlix Application
*
*
* This application demonstrates MongoDB operations using the Node.js driver
* with TypeScript. The code prioritizes readability and educational value
* over performance optimization.
*/

import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { closeDatabaseConnection, connectToDatabase, verifyRequirements } from './config/database';
import { errorHandler } from './utils/errorHandler';
import moviesRouter from './routes/movies';
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import {
closeDatabaseConnection,
connectToDatabase,
verifyRequirements,
} from "./config/database";
import { errorHandler } from "./utils/errorHandler";
import moviesRouter from "./routes/movies";

// Load environment variables from .env file
// This must be called before any other imports that use environment variables
Expand All @@ -25,37 +29,40 @@ const PORT = process.env.PORT || 3001;
* Allows the frontend to communicate with this Express backend
* In production, this should be configured to only allow specific origins
*/
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
credentials: true
}));
app.use(
cors({
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
credentials: true,
})
);

/**
* Middleware Configuration
* Express.json() parses incoming JSON requests and puts the parsed data in req.body
* The limit is set to handle potentially large movie documents
*/
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));

/**
* API Routes
* All movie-related CRUD operations are handled by the movies router
*/
app.use('/api/movies', moviesRouter);
app.use("/api/movies", moviesRouter);

/**
* Root Endpoint
* Provides basic information about the API
*/
app.get('/', (req, res) => {
app.get("/", (req, res) => {
res.json({
name: 'MongoDB Sample MFlix API',
version: '1.0.0',
description: 'Express.js backend demonstrating MongoDB operations with the sample_mflix dataset',
name: "MongoDB Sample MFlix API",
version: "1.0.0",
description:
"Express.js backend demonstrating MongoDB operations with the sample_mflix dataset",
endpoints: {
movies: '/api/movies'
}
movies: "/api/movies",
},
});
});

Expand All @@ -72,27 +79,26 @@ app.use(errorHandler);
*/
async function startServer() {
try {
console.log('Starting MongoDB Sample MFlix API...');
console.log("Starting MongoDB Sample MFlix API...");

// Connect to MongoDB database
console.log('Connecting to MongoDB...');
console.log("Connecting to MongoDB...");
await connectToDatabase();
console.log('Connected to MongoDB successfully');
console.log("Connected to MongoDB successfully");

// Verify that all required indexes and sample data exist
console.log('Verifying requirements (indexes and sample data)...');
console.log("Verifying requirements (indexes and sample data)...");
await verifyRequirements();
console.log('All requirements verified successfully');
console.log("All requirements verified successfully");

// Start the Express server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`API documentation available at http://localhost:${PORT}`);
});

} catch (error) {
console.error('Failed to start server:', error);
console.error("Failed to start server:", error);

// Exit the process if we can't start properly
// This ensures the application doesn't run in a broken state
process.exit(1);
Expand All @@ -103,17 +109,17 @@ async function startServer() {
* Graceful Shutdown Handler
* Ensures the application shuts down cleanly when terminated
*/
process.on('SIGINT', () => {
console.log('\nReceived SIGINT. Shutting down...');
process.on("SIGINT", () => {
console.log("\nReceived SIGINT. Shutting down...");
closeDatabaseConnection();
process.exit(0);
});

process.on('SIGTERM', () => {
console.log('\nReceived SIGTERM. Shutting down...');
process.on("SIGTERM", () => {
console.log("\nReceived SIGTERM. Shutting down...");
closeDatabaseConnection();
process.exit(0);
});

// Start the server
startServer();
startServer();
74 changes: 44 additions & 30 deletions server/express/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Database Configuration and Connection Management
*
*
* This module handles MongoDB connection setup using the Node.js driver
* and implements pre-flight checks to ensure the application has all
* necessary indexes and sample data.
*/

import { MongoClient, Db, Collection, Document } from 'mongodb';
import { MongoClient, Db, Collection, Document } from "mongodb";

let client: MongoClient;
let database: Db;
Expand All @@ -20,27 +20,26 @@ async function _connectToDatabase(): Promise<Db> {

// Retrieve MongoDB connection string from environment variables
const uri = process.env.MONGODB_URI;

if (!uri) {
throw new Error(
'MONGODB_URI environment variable is not defined. Please check your .env file and ensure it contains a valid MongoDB connection string.'
"MONGODB_URI environment variable is not defined. Please check your .env file and ensure it contains a valid MongoDB connection string."
);
}

try {
// Create new MongoDB client instance
client = new MongoClient(uri);

// Connect to MongoDB
await client.connect();

// Get reference to the sample_mflix database
database = client.db('sample_mflix');
database = client.db("sample_mflix");

console.log(`Connected to database: ${database.databaseName}`);

return database;

} catch (error) {
throw error;
}
Expand All @@ -49,30 +48,30 @@ async function _connectToDatabase(): Promise<Db> {
let connect$: Promise<Db>;
/**
* Establishes connection to MongoDB by using the connection string from environment variables
*
*
* @returns Promise<Db> - The connected database instance
* @throws Error if connection fails or if MONGODB_URI is not provided
*/
export async function connectToDatabase(): Promise<Db> {
// connect$ only gets assigned exactly once on the first request, ensuring all subsequent requests use the same connect$ promise.
connect$ ??= _connectToDatabase();
return await connect$;
// connect$ only gets assigned exactly once on the first request, ensuring all subsequent requests use the same connect$ promise.
connect$ ??= _connectToDatabase();
return await connect$;
}

/**
* Gets a reference to a specific collection in the database
*
*
* @param collectionName - Name of the collection to access
* @returns Collection instance
* @throws Error if database is not connected
*/
export function getCollection<T extends Document>(collectionName: string): Collection<T> {
export function getCollection<T extends Document>(
collectionName: string
): Collection<T> {
if (!database) {
throw new Error(
'Database not connected.'
);
throw new Error("Database not connected.");
}

return database.collection(collectionName);
}

Expand All @@ -83,25 +82,24 @@ export function getCollection<T extends Document>(collectionName: string): Colle
export async function closeDatabaseConnection(): Promise<void> {
if (client) {
await client.close();
console.log('Database connection closed');
console.log("Database connection closed");
}
}

/**
* Verifies that all required indexes exist and sample data is present
*
*
* If any requirements are missing, this function will attempt to create them.
*/
export async function verifyRequirements(): Promise<void> {
try {
const db = await connectToDatabase();

// Check if the movies collection exists and has data
await verifyMoviesCollection(db);
console.log('All database requirements verified successfully');

console.log("All database requirements verified successfully");
} catch (error) {
console.error('Requirements verification failed:', error);
console.error("Requirements verification failed:", error);
throw error;
}
}
Expand All @@ -110,12 +108,28 @@ export async function verifyRequirements(): Promise<void> {
* Verifies the movies collection and creates necessary indexes
*/
async function verifyMoviesCollection(db: Db): Promise<void> {
const moviesCollection = db.collection('movies');
const moviesCollection = db.collection("movies");

// Check if collection has documents
const movieCount = await moviesCollection.estimatedDocumentCount();

if (movieCount === 0) {
console.warn('Movies collection is empty. Please ensure sample_mflix data is loaded.');
console.warn(
"Movies collection is empty. Please ensure sample_mflix data is loaded."
);
}

// Create text search index on plot field for full-text search
try {
await moviesCollection.createIndex(
{ plot: "text", title: "text", fullplot: "text" },
{
name: "text_search_index",
background: true,
}
);
console.log("Text search index created for movies collection");
} catch (error) {
console.error("Could not create text search index:", error);
}
}
Loading