From d3e3326832d9f892a1f0614b505fab2c63817ad5 Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 14:15:10 -0500 Subject: [PATCH 1/6] Remove unneeded files from client --- mflix/client/.env.example | 6 --- mflix/client/README.md | 91 --------------------------------------- 2 files changed, 97 deletions(-) delete mode 100644 mflix/client/.env.example delete mode 100644 mflix/client/README.md diff --git a/mflix/client/.env.example b/mflix/client/.env.example deleted file mode 100644 index 61ed0e9..0000000 --- a/mflix/client/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Client Environment Variables -# Copy this file to .env.local and update the values as needed - -# URL of the Express API server -# Default: http://localhost:3001 (for local development) -API_URL=http://localhost:3001 \ No newline at end of file diff --git a/mflix/client/README.md b/mflix/client/README.md deleted file mode 100644 index 8c46ce4..0000000 --- a/mflix/client/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# Next.js MFlix Frontend Application - -This is a Next.js frontend application that demonstrates a modern movie browsing experience by connecting to a backend API. The application showcases Server Side Rendering (SSR), responsive design, and interactive features while displaying movie data from the MongoDB sample_mflix dataset. - -## Project Structure - -``` -client/ -├── README.md -├── package.json -├── next.config.ts -├── tsconfig.json -├── .env.example -└── app/ - ├── layout.tsx # Root layout with navigation - ├── page.tsx # Home page - ├── globals.css # Global styles - ├── movies/ # Movies listing page - ├── movie/[id]/ # Individual movie details - ├── aggregations/ # Data aggregations page - ├── components/ # Reusable UI components - ├── lib/ # API utilities and constants - └── types/ # TypeScript type definitions -``` - -## Prerequisites - -- **Backend API Server**: The backend server must be running on `http://localhost:3001` -- **MongoDB Database**: The backend must be connected to a MongoDB instance with the `sample_mflix` dataset loaded - - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) -- **Node.js 18** or higher -- **npm** (included with Node.js) - -## Getting Started - -### 1. Configure the Environment - -Copy the environment example file: - -```bash -cp .env.example .env.local -``` - -Edit `.env.local` and update the API URL if needed: - -```env -API_URL=http://localhost:3001 -``` - -### 2. Install Dependencies - -```bash -npm install -``` - -### 3. Start the Development Server - -```bash -npm run dev -``` - -The Next.js application will start on `http://localhost:3000`. - -### 4. Access the Application - -Open your browser and navigate to: -- **Frontend:** http://localhost:3000 -- **Movies Page:** http://localhost:3000/movies -- **Aggregations:** http://localhost:3000/aggregations - -## Features - -- **Browse Movies:** View a paginated list of movies from the sample_mflix dataset with server-side rendering -- **Movie Details:** Access comprehensive movie information including plot, cast, ratings, and metadata -- **CRUD Operations:** Create, read, update, and delete movies -- **Search & Filters:** Search movies with various filters and sorting - options using MongoDB Search -- **Vector Search:** Find movies with similar plot descriptions using - MongoDB Vector Search -- **Data Aggregations:** View analytics and insights built with MongoDB aggregation pipelines - -## Issues - -If you have problems running the sample app, please check the following: - -- [ ] Verify that you have started the backend server on port 3001. -- [ ] Verify that the backend is connected to MongoDB with the sample_mflix dataset. -- [ ] Verify that you have set the correct API_URL in your `.env.local` file. -- [ ] Verify that you have no firewalls blocking access to ports 3000 or 3001. - -If you have verified the above and still have issues, please [open an issue](https://github.com/mongodb/docs-sample-apps/issues/new/choose) on the source repository `mongodb/docs-sample-apps`. From cb2cf038d83867d06859cfbd6672cbd2329cc507 Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 14:18:55 -0500 Subject: [PATCH 2/6] Run FastAPI server on port 3001 --- mflix/README-PYTHON-FASTAPI.md | 20 ++++++++++---------- mflix/server/python-fastapi/.env.example | 2 +- mflix/server/python-fastapi/main.py | 8 ++++++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md index 8225b9a..f3be9de 100644 --- a/mflix/README-PYTHON-FASTAPI.md +++ b/mflix/README-PYTHON-FASTAPI.md @@ -22,7 +22,7 @@ This is a full-stack movie browsing application built with Python FastAPI and Ne - **Python 3.10** to **Python 3.13** - **Node.js 20** or higher - **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded - - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) - **pip** for Python package management - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) @@ -56,7 +56,7 @@ VOYAGE_API_KEY=your_voyage_api_key # CORS Configuration # Comma-separated list of allowed origins for CORS -CORS_ORIGINS=http://localhost:3000,http://localhost:8000 +CORS_ORIGINS=http://localhost:3000,http://localhost:3001 ``` **Note:** Replace `username`, `password`, and `cluster` with your actual MongoDB Atlas @@ -69,7 +69,7 @@ python -m venv .venv ``` Activate the virtual environment: - + ```bash source .venv/bin/activate ``` @@ -85,12 +85,13 @@ pip install -r requirements.txt From the `server/` directory, run: ```bash -fastapi dev main.py --reload +uvicorn main:app --reload --port 3001 ``` -The server will start on `http://localhost:8000`. You can verify it's running by visiting: -- API root: http://localhost:8000/api/movies -- API documentation (Swagger UI): http://localhost:8000/docs +The server will start on `http://localhost:3001`. You can verify it's running by visiting: +- API root: http://localhost:3001/api/movies +- API documentation (Swagger UI): http://localhost:3001/docs +- Interactive API documentation (ReDoc): http://localhost:3001/redoc ### 3. Configure and Start the Frontend @@ -118,8 +119,8 @@ The Next.js application will start on `http://localhost:3000`. Open your browser and navigate to: - **Frontend:** http://localhost:3000 -- **Backend API:** http://localhost:8000 -- **API Documentation:** http://localhost:8000/docs +- **Backend API:** http://localhost:3001 +- **API Documentation:** http://localhost:3001/docs ## Features @@ -204,4 +205,3 @@ If you have problems running the sample app, please check the following: If you have verified the above and still have issues, please [open an issue](https://github.com/mongodb/docs-sample-apps/issues/new/choose) on the source repository `mongodb/docs-sample-apps`. - diff --git a/mflix/server/python-fastapi/.env.example b/mflix/server/python-fastapi/.env.example index 59ac463..d42141d 100644 --- a/mflix/server/python-fastapi/.env.example +++ b/mflix/server/python-fastapi/.env.example @@ -9,4 +9,4 @@ VOYAGE_API_KEY=your_voyage_api_key # CORS Configuration # Comma-separated list of allowed origins for CORS -CORS_ORIGINS="http://localhost:3000,http://localhost:8000" +CORS_ORIGINS="http://localhost:3000,http://localhost:3001" diff --git a/mflix/server/python-fastapi/main.py b/mflix/server/python-fastapi/main.py index 61e6ca4..9d2290e 100644 --- a/mflix/server/python-fastapi/main.py +++ b/mflix/server/python-fastapi/main.py @@ -17,6 +17,14 @@ async def lifespan(app: FastAPI): # Startup: Create search indexes await ensure_search_index() await vector_search_index() + + # Print server information + print(f"\n{'='*60}") + print(f" Server started at http://127.0.0.1:3001") + print(f" Documentation at http://127.0.0.1:3001/docs") + print(f" Interactive API docs at http://127.0.0.1:3001/redoc") + print(f"{'='*60}\n") + yield # Shutdown: Clean up resources if needed # Add any cleanup code here From d002f2cd2173b44dd0ac9284a8f4803cbafc9735 Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 14:25:18 -0500 Subject: [PATCH 3/6] Remove unneeded comments --- .../src/database/mongo_client.py | 1 - .../python-fastapi/src/models/models.py | 11 ------- .../python-fastapi/src/routers/movies.py | 33 +------------------ .../python-fastapi/src/utils/errorHandler.py | 24 ++------------ 4 files changed, 3 insertions(+), 66 deletions(-) diff --git a/mflix/server/python-fastapi/src/database/mongo_client.py b/mflix/server/python-fastapi/src/database/mongo_client.py index da2d2b9..0ff350e 100644 --- a/mflix/server/python-fastapi/src/database/mongo_client.py +++ b/mflix/server/python-fastapi/src/database/mongo_client.py @@ -8,7 +8,6 @@ client = AsyncMongoClient(os.getenv("MONGO_URI")) db = client[os.getenv("MONGO_DB")] -# Set the API key but don't instantiate the client here voyage_api_key = os.getenv("VOYAGE_API_KEY") if voyage_api_key: voyageai.api_key = voyage_api_key diff --git a/mflix/server/python-fastapi/src/models/models.py b/mflix/server/python-fastapi/src/models/models.py index 8c2528a..f9494c7 100644 --- a/mflix/server/python-fastapi/src/models/models.py +++ b/mflix/server/python-fastapi/src/models/models.py @@ -38,17 +38,6 @@ class Movie(BaseModel): "populate_by_name" : True } - -''' -So this an interesting conversion. Pydanic doesn't cleanly support constructing -models that have MongoDB query operators as field names. This becomes an issue when -we want to convert the validated model back to a dictionary to use as a MongoDB query filter. -For example, if a user leaves the 'q' parameter blank, we don't want to include the '$text' operator -in the filter at all but validation will send an empty value for it and that causes errors. So -I am handling the validation in the query router itself, but leaving this here as an example of how -it could be done, if I am wrong about Pydantic's capabilities. -''' - class TextFilter(BaseModel): search: str = Field(..., alias="$search") diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index 4bcbdbf..8ec10af 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -300,7 +300,7 @@ async def search_movies( - plot: Movie plot text - score: Vector search similarity score (0.0 to 1.0, higher = more similar) """ -# Specify your Voyage API key and embedding model +# Specify your Voyage AI embedding model model = "voyage-3-large" outputDimension = 2048 #Set to 2048 to match the dimensions of the collection's embeddings @@ -403,10 +403,6 @@ async def vector_search_movies( details=str(e) ) -#------------------------------------ -# Place get_movie_by_id endpoint here -#------------------------------------ - """ GET /api/movies/{id} Retrieve a single movie by its ID. @@ -539,10 +535,6 @@ async def get_all_movies( # Return the results wrapped in a SuccessResponse return create_success_response(movies, f"Found {len(movies)} movies.") -#------------------------------------ -# Place create_movie endpoint here -#------------------------------------ - """ POST /api/movies/ Create a new movie. @@ -600,10 +592,6 @@ async def create_movie(movie: CreateMovieRequest): return create_success_response(created_movie, f"Movie '{movie_data['title']}' created successfully") -#------------------------------------ -# Place create_movies_batch endpoint here -#------------------------------------ - """ POST /api/movies/batch @@ -670,10 +658,6 @@ async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessRespons details=str(e) ) -#------------------------------------ -# Place update_movie endpoint here -#------------------------------------ - """ PATCH /api/movies/{id} @@ -744,10 +728,6 @@ async def update_movie( return create_success_response(updatedMovie, f"Movie updated successfully. Modified {len(update_dict)} fields.") -#------------------------------------ -# Place update_movies_by_batch endpoint here -#------------------------------------ - """ PATCH /api/movies @@ -810,10 +790,6 @@ async def update_movies_batch( f"Update operation completed. Matched {result.matched_count} movie(s), modified {result.modified_count} movie(s)." ) -#------------------------------------ -# Place delete_movie endpoint here -#------------------------------------ - """ DELETE /api/movies/{id} Delete a single movie by its ID. @@ -860,9 +836,6 @@ async def delete_movie_by_id(id: str): "Movie deleted successfully" ) -#------------------------------------ -# Place delete_movies_by_batch endpoint here -#------------------------------------ """ DELETE /api/movies/ @@ -922,10 +895,6 @@ async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse f'Delete operation completed. Removed {result.deleted_count} movies.' ) -#------------------------------------ -# Place find_and_delete_movie endpoint here -#------------------------------------ - """ DELETE /api/movies/{id}/find-and-delete Finds and deletes a movie in a single atomic operation. diff --git a/mflix/server/python-fastapi/src/utils/errorHandler.py b/mflix/server/python-fastapi/src/utils/errorHandler.py index 4258e3a..5e235ec 100644 --- a/mflix/server/python-fastapi/src/utils/errorHandler.py +++ b/mflix/server/python-fastapi/src/utils/errorHandler.py @@ -2,18 +2,9 @@ from fastapi.responses import JSONResponse from pymongo.errors import PyMongoError, DuplicateKeyError, WriteError from datetime import datetime, timezone -from typing import Any, Dict, Optional +from typing import Any, Optional from src.models.models import ErrorDetails, ErrorResponse, SuccessResponse, T - -''' -Open to having a conversation about parity in the code. From my understanding exception handeling, validation errors and enforcement(Pydantic), -and error response formatting are all handled natively by FastAPI. So I don't believe I need to create the ValidationError -class, middleware, or exception handlers present in the TS code. - -''' - - ''' Creates a standardized success response. @@ -26,8 +17,6 @@ SuccessResponse[T]: A standardized success response object. ''' - -# TODO: Verify the timestamp format is acceptable. def create_success_response(data:T, message: Optional[str] = None) -> SuccessResponse[T]: return SuccessResponse( message=message or "Operation completed successfully.", @@ -36,7 +25,6 @@ def create_success_response(data:T, message: Optional[str] = None) -> SuccessRes ) - ''' Creates a standardized error response. @@ -50,7 +38,6 @@ def create_success_response(data:T, message: Optional[str] = None) -> SuccessRes ''' -# TODO: Verify the timestamp format is acceptable. def create_error_response(message: str, code: Optional[str]=None, details: Optional[Any]=None) -> ErrorResponse: return ErrorResponse( message=message, @@ -63,13 +50,6 @@ def create_error_response(message: str, code: Optional[str]=None, details: Optio ) - -''' -This is interesting, I am not sure if this is worth explaining that compared to Node, you are -not going to get exceptions thrown from MongoDB operations in the same way. You are not getting -error codes back from operations, you are getting exceptions. -''' - def parse_mongo_exception(exc: Exception) -> dict: if isinstance(exc, DuplicateKeyError): return{ @@ -126,4 +106,4 @@ async def generic_exception_handler(request: Request, exc: Exception): code="INTERNAL_SERVER_ERROR", details=getattr(exc, 'detail', None) or getattr(exc, 'args', None) ).model_dump() - ) \ No newline at end of file + ) From 92472c351bd01f406fc6dc1bba6696668837bb2d Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 14:28:33 -0500 Subject: [PATCH 4/6] Clean up tests dir --- mflix/server/python-fastapi/tests/README.md | 2 +- mflix/server/python-fastapi/tests/test_movie_routes.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mflix/server/python-fastapi/tests/README.md b/mflix/server/python-fastapi/tests/README.md index 6a151d4..8df57cc 100644 --- a/mflix/server/python-fastapi/tests/README.md +++ b/mflix/server/python-fastapi/tests/README.md @@ -41,7 +41,7 @@ The test suite is organized into three categories: 1. **For all tests:** ```bash - cd server/python-fastapi + cd server/ source .venv/bin/activate # or `.venv\Scripts\activate` on Windows ``` diff --git a/mflix/server/python-fastapi/tests/test_movie_routes.py b/mflix/server/python-fastapi/tests/test_movie_routes.py index 9a04b06..b98937f 100644 --- a/mflix/server/python-fastapi/tests/test_movie_routes.py +++ b/mflix/server/python-fastapi/tests/test_movie_routes.py @@ -9,7 +9,6 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch from bson import ObjectId -from bson.errors import InvalidId from src.models.models import CreateMovieRequest, UpdateMovieRequest From 26ec76697be028b26b2107a7a354cef517e5908e Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 14:37:24 -0500 Subject: [PATCH 5/6] Teeny tweak --- mflix/README-JAVASCRIPT-EXPRESS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-JAVASCRIPT-EXPRESS.md index 39bd092..130bf5f 100644 --- a/mflix/README-JAVASCRIPT-EXPRESS.md +++ b/mflix/README-JAVASCRIPT-EXPRESS.md @@ -18,7 +18,7 @@ This is a full-stack movie browsing application built with Express.js and Next.j - **Node.js 22** or higher - **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded - - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) - **npm** (included with Node.js) - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) @@ -54,7 +54,6 @@ VOYAGE_API_KEY=your_voyage_api_key PORT=3001 NODE_ENV=development - # CORS Configuration # Allowed origin for cross-origin requests (frontend URL) # For multiple origins, separate with commas From 91a134ad43aa5219e18df115feb8ea0e995f5353 Mon Sep 17 00:00:00 2001 From: dacharyc Date: Fri, 7 Nov 2025 15:27:29 -0500 Subject: [PATCH 6/6] Remove unneeded comment --- .../python-fastapi/src/routers/movies.py | 198 +++++++++--------- 1 file changed, 98 insertions(+), 100 deletions(-) diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index 8ec10af..4cc2154 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -78,9 +78,7 @@ # MongoDB Search # # MongoDB Search based on searching the plot, fullplot, directors, writers, and cast fields. -# This function was made with the assumption that the UI will have fields for plot,fullplot, -# directors, writers, and cast to search on. Or some sort of combined search field. -# Also this fuzzy operator is being used to allow for some misspellings in the search terms +# This fuzzy operator is being used to allow for some misspellings in the search terms # but that allows for very generous matching. This can be adjusted as needed. #---------------------------------------------------------------------------------------------------------- """ @@ -98,7 +96,7 @@ cast (str, optional): Text to search against the cast field. limit (int, optional): Number of results to return (default: 20) skip (int, optional): Number of results to skip for pagination (default: 0) - search_operator (str, optional): How to combine multiple search fields. + search_operator (str, optional): How to combine multiple search fields. Must be one of "must", "should", "mustNot", or "filter". Default is "must". Returns: @@ -120,7 +118,7 @@ async def search_movies( skip:int = Query(default=0, ge=0), search_operator: str = Query(default="must", alias="searchOperator") ) -> SuccessResponse[SearchMoviesResponse]: - + search_phrases = [] # Validate the search_operator parameter to ensure it's a valid compound operator @@ -247,24 +245,24 @@ async def search_movies( message="An error occurred while performing the search.", code="DATABASE_ERROR", details=str(e) - ) + ) # Extract total count and movies from facet results with proper bounds checking if not results or len(results) == 0: return create_success_response( - SearchMoviesResponse(movies=[], totalCount=0), + SearchMoviesResponse(movies=[], totalCount=0), "No movies found matching the search criteria." ) - + facet_result = results[0] - + # Safely extract total count total_count_array = facet_result.get("totalCount", []) total_count = total_count_array[0].get("count", 0) if total_count_array else 0 - + # Safely extract movies data movies_data = facet_result.get("results", []) - + # Convert ObjectId to string for each movie in the results movies = [] for movie in movies_data: @@ -272,7 +270,7 @@ async def search_movies( movies.append(movie) return create_success_response( - SearchMoviesResponse(movies=movies, totalCount=total_count), + SearchMoviesResponse(movies=movies, totalCount=total_count), f"Found {total_count} movies matching the search criteria." ) @@ -296,7 +294,7 @@ async def search_movies( SuccessResponse[List[VectorSearchResult]]: A response object containing movies with similarity scores. Each result includes: - _id: Movie ObjectId - - title: Movie title + - title: Movie title - plot: Movie plot text - score: Vector search similarity score (0.0 to 1.0, higher = more similar) """ @@ -312,11 +310,11 @@ async def vector_search_movies( ): """ Vector search endpoint for finding movies with similar plots. - + Args: q: The search query string limit: Maximum number of results to return - + Returns: SuccessResponse containing a list of movies with similarity scores """ @@ -326,18 +324,18 @@ async def vector_search_movies( code="SERVICE_UNAVAILABLE", details="VOYAGE_API_KEY not configured. Please add your API key to your .env file." ) - + try: # Initialize the client here to avoid import-time errors vo = voyageai.Client() - + # The vector search index was already created at startup time # Generate embedding for the search query query_embedding = get_embedding(q, input_type="query", client=vo) - + # Get the embedded movies collection embedded_movies_collection = get_collection("embedded_movies") - + # Define vector search pipeline pipeline = [ { @@ -378,7 +376,7 @@ async def vector_search_movies( ] raw_results = await execute_aggregation_on_collection(embedded_movies_collection, pipeline) - + # Convert ObjectId to string and create VectorSearchResult objects for result in raw_results: if "_id" in result and result["_id"]: @@ -387,15 +385,15 @@ async def vector_search_movies( except (InvalidId, TypeError): # Handle invalid ObjectId conversion result["_id"] = str(result["_id"]) if result["_id"] else None - + # This code converts the raw results into VectorSearchResult objects results = [VectorSearchResult(**doc) for doc in raw_results] - + return create_success_response( - results, + results, f"Found {len(results)} similar movies for query: '{q}'" ) - + except Exception as e: return create_error_response( message="Vector search failed", @@ -445,7 +443,7 @@ async def get_movie_by_id(id: str): ) movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - + return create_success_response(movie, "Movie retrieved successfully") """ @@ -488,9 +486,9 @@ async def get_all_movies( movies_collection = get_collection("movies") filter_dict = {} if q: - filter_dict["$text"] = {"$search": q} + filter_dict["$text"] = {"$search": q} if title: - filter_dict["title"] = {"$regex": title, "$options": "i"} + filter_dict["title"] = {"$regex": title, "$options": "i"} if genre: filter_dict["genres"] = {"$regex": genre, "$options": "i"} if year: @@ -502,7 +500,7 @@ async def get_all_movies( if max_rating is not None: rating_filter["$lte"] = max_rating filter_dict["imdb.rating"] = rating_filter - + # Building the sort object based on user input sort_order = -1 if sort_order == "desc" else 1 @@ -511,13 +509,13 @@ async def get_all_movies( # Query the database with the constructed filter, sort, skip, and limit. try: - result = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) + result = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) except Exception as e: return create_error_response( message="An error occurred while fetching movies.", code="DATABASE_ERROR", details=str(e) - ) + ) movies = [] @@ -531,8 +529,8 @@ async def get_all_movies( except ValueError: movie["year"] = None - movies.append(movie) - # Return the results wrapped in a SuccessResponse + movies.append(movie) + # Return the results wrapped in a SuccessResponse return create_success_response(movies, f"Found {len(movies)} movies.") """ @@ -545,14 +543,14 @@ async def get_all_movies( SuccessResponse[Movie]: A response object containing the created movie data. """ -@router.post("/", +@router.post("/", response_model=SuccessResponse[Movie], status_code=201, summary="Creates a new movie in the database.") async def create_movie(movie: CreateMovieRequest): # Pydantic automatically validates the structure movie_data = movie.model_dump(by_alias=True, exclude_none=True) - + movies_collection = get_collection("movies") try: result = await movies_collection.insert_one(movie_data) @@ -562,7 +560,7 @@ async def create_movie(movie: CreateMovieRequest): code="INTERNAL_SERVER_ERROR", details=str(e) ) - + # Verify that the document was created before querying it if not result.acknowledged: return create_error_response( @@ -570,7 +568,7 @@ async def create_movie(movie: CreateMovieRequest): code="INTERNAL_SERVER_ERROR", details="The database did not acknowledge the insert operation" ) - + try: # Retrieve the created document to return complete data created_movie = await movies_collection.find_one({"_id": result.inserted_id}) @@ -580,7 +578,7 @@ async def create_movie(movie: CreateMovieRequest): code="INTERNAL_SERVER_ERROR", details=str(e) ) - + if created_movie is None: return create_error_response( message="Movie creation verification failed", @@ -589,13 +587,13 @@ async def create_movie(movie: CreateMovieRequest): ) created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string - + return create_success_response(created_movie, f"Movie '{movie_data['title']}' created successfully") """ POST /api/movies/batch -Create multiple movies in a single request. +Create multiple movies in a single request. Request Body: movies (List[CreateMovieRequest]): A list of movie objects to insert. Each object should include: @@ -634,7 +632,7 @@ async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessRespons code="INVALID_INPUT", details=None ) - + movies_dicts = [] for movie in movies: @@ -658,7 +656,7 @@ async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessRespons details=str(e) ) -""" +""" PATCH /api/movies/{id} Update a single movie by its ID. @@ -683,7 +681,7 @@ async def update_movie( ) -> SuccessResponse[Movie]: movies_collection = get_collection("movies") - + # Validate the ObjectId try: movie_id = ObjectId(movie_id) @@ -692,8 +690,8 @@ async def update_movie( message="Invalid movie_id format.", code="INVALID_OBJECT_ID", details=str(movie_id) - ) - + ) + update_dict = movie_data.model_dump(exclude_unset=True, exclude_none=True) # Validate that the dict is not empty @@ -714,7 +712,7 @@ async def update_movie( message="An error occurred while updating the movie.", code="DATABASE_ERROR", details=str(e) - ) + ) if result.matched_count == 0: return create_error_response( @@ -722,7 +720,7 @@ async def update_movie( code="MOVIE_NOT_FOUND", details=str(movie_id) ) - + updatedMovie = await movies_collection.find_one({"_id": movie_id}) updatedMovie["_id"] = str(updatedMovie["_id"]) @@ -782,7 +780,7 @@ async def update_movies_batch( code="DATABASE_ERROR", details=str(e) ) - + return create_success_response({ "matchedCount": result.matched_count, "modifiedCount": result.modified_count @@ -827,12 +825,12 @@ async def delete_movie_by_id(id: str): if result.deleted_count == 0: return create_error_response( message="Movie not found", - code="INTERNAL_SERVER_ERROR", + code="INTERNAL_SERVER_ERROR", details=f"No movie found with ID: {id}" ) - + return create_success_response( - {"deletedCount": result.deleted_count}, + {"deletedCount": result.deleted_count}, "Movie deleted successfully" ) @@ -857,7 +855,7 @@ async def delete_movie_by_id(id: str): async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse[dict]: movies_collection = get_collection("movies") - + # Extract filter from the request body filter_data = request_body.get("filter", {}) @@ -935,11 +933,11 @@ async def find_and_delete_movie(id: str): if deleted_movie is None: return create_error_response( message="Movie not found", - code="INTERNAL_SERVER_ERROR", + code="INTERNAL_SERVER_ERROR", details=f"No movie found with ID: {id}" ) deleted_movie["_id"] = str(deleted_movie["_id"]) # Convert ObjectId to string - + return create_success_response(deleted_movie, "Movie found and deleted successfully") """ @@ -961,7 +959,7 @@ async def aggregate_movies_recent_commented( limit: int = Query(default=10, ge=1, le=50), movie_id: str = Query(default=None) ): - + # Add a multi-stage aggregation that: # 1. Filters movies by valid year range # 2. Joins with comments collection (like SQL JOIN) @@ -969,7 +967,7 @@ async def aggregate_movies_recent_commented( # 4. Sorts comments by date and extracts most recent ones # 5. Sorts movies by their most recent comment date # 6. Shapes the final output with transformed comment structure - + pipeline: list[dict[str, Any]] =[ # STAGE 1: $match - Initial Filter # Filter movies to only those with valid year data @@ -991,7 +989,7 @@ async def aggregate_movies_recent_commented( code="INTERNAL_SERVER_ERROR", details="The provided movie_id is not a valid ObjectId" ) - + # Add remaining pipeline stages pipeline.extend([ # STAGE 2: $lookup - Join with the 'comments' Collection @@ -1086,12 +1084,12 @@ async def aggregate_movies_recent_commented( for result in results: if "_id" in result: result["_id"] = str(result["_id"]) - + # Calculate total comments from all movies total_comments = sum(result.get("totalComments", 0) for result in results) - + return create_success_response( - results, + results, f"Found {total_comments} comments from movie{'s' if len(results) != 1 else ''}" ) @@ -1116,7 +1114,7 @@ async def aggregate_movies_by_year(): # 2. Groups movies by release year and calculates statistics per year # 3. Shapes the final output with clean field names and rounded averages # 4. Sorts results by year (newest first) for chronological presentation - + pipeline = [ # STAGE 1: $match - Data Quality Filter # Clean data: ensure year is an integer and within reasonable range @@ -1126,14 +1124,14 @@ async def aggregate_movies_by_year(): "year": {"$type": "number", "$gte": 1800, "$lte": 2030} } }, - + # STAGE 2: $group - Aggregate Movies by Year # Group all movies by their release year and calculate various statistics { "$group": { "_id": "$year", # Group by year field "movieCount": {"$sum": 1}, # Count total movies per year - + # Calculate average rating (only for valid numeric ratings) "averageRating": { "$avg": { @@ -1148,7 +1146,7 @@ async def aggregate_movies_by_year(): ] } }, - + # Find highest rating for the year (same validation as average rating) "highestRating": { "$max": { @@ -1163,7 +1161,7 @@ async def aggregate_movies_by_year(): ] } }, - + # Find lowest rating for the year (same validation as average and highest rating) "lowestRating": { "$min": { @@ -1178,12 +1176,12 @@ async def aggregate_movies_by_year(): ] } }, - + # Sum total votes across all movies in the year "totalVotes": {"$sum": "$imdb.votes"} } }, - + # STAGE 3: $project - Shape Final Output # Transform the grouped data into a clean, readable format { @@ -1197,7 +1195,7 @@ async def aggregate_movies_by_year(): "_id": 0 # Exclude the _id field from output } }, - + # STAGE 4: $sort - Sort by Year (Newest First) # Sort results in descending order to show most recent years first {"$sort": {"year": -1}} # -1 = descending order @@ -1212,9 +1210,9 @@ async def aggregate_movies_by_year(): code="INTERNAL_SERVER_ERROR", details=str(e) ) - + return create_success_response( - results, + results, f"Aggregated statistics for {len(results)} years" ) @@ -1237,7 +1235,7 @@ async def aggregate_directors_most_movies( ): # Define aggregation pipeline to find directors with the most movies # This pipeline demonstrates array unwinding, filtering, and ranking - + # Add a multi-stage aggregation that: # 1. Filters movies with valid directors and year data (data quality filter) # 2. Unwinds directors array to create separate documents per director @@ -1256,14 +1254,14 @@ async def aggregate_directors_most_movies( "year": {"$type": "number", "$gte": 1800, "$lte": 2030} # Valid year range } }, - + # STAGE 2: $unwind - Flatten Directors Array # Convert each movie's directors array into separate documents # Example: Movie with ["Director A", "Director B"] becomes 2 documents { "$unwind": "$directors" }, - + # STAGE 3: $match - Clean Director Names # Filter out any null or empty director names after unwinding { @@ -1271,7 +1269,7 @@ async def aggregate_directors_most_movies( "directors": {"$ne": None, "$ne": ""} } }, - + # STAGE 4: $group - Aggregate by Director # Group all movies by director name and calculate statistics { @@ -1281,15 +1279,15 @@ async def aggregate_directors_most_movies( "averageRating": {"$avg": "$imdb.rating"} # Average rating of director's movies } }, - + # STAGE 5: $sort - Rank Directors by Movie Count # Sort directors by number of movies (highest first) {"$sort": {"movieCount": -1}}, # -1 = descending (most movies first) - + # STAGE 6: $limit - Restrict Results # Limit to top N directors based on user input {"$limit": limit}, - + # STAGE 7: $project - Shape Final Output # Transform the grouped data into a clean, readable format { @@ -1311,9 +1309,9 @@ async def aggregate_directors_most_movies( code="INTERNAL_SERVER_ERROR", details=str(e) ) - + return create_success_response( - results, + results, f"Found {len(results)} directors with most movies" ) @@ -1321,36 +1319,36 @@ async def aggregate_directors_most_movies( #Helper Functions #------------------------------------ -""" - Helper function to execute aggregation pipeline and return results. +""" + Helper function to execute aggregation pipeline and return results. - Args: - pipeline: MongoDB aggregation pipeline stages + Args: + pipeline: MongoDB aggregation pipeline stages - Returns: - List of documents from aggregation result -""" + Returns: + List of documents from aggregation result +""" async def execute_aggregation(pipeline: list) -> list: """Helper function to execute aggregation pipeline and return results""" - + movies_collection = get_collection("movies") # For the async Pymongo driver, we need to await the aggregate call cursor = await movies_collection.aggregate(pipeline) results = await cursor.to_list(length=None) # Convert cursor to list to collect all data at once rather than processing data per document - + return results -""" - Helper function to execute aggregation pipeline and return results from a specified collection. +""" + Helper function to execute aggregation pipeline and return results from a specified collection. - Args: + Args: collection: The MongoDB collection to run the aggregation on - pipeline: MongoDB aggregation pipeline stages + pipeline: MongoDB aggregation pipeline stages - Returns: - List of documents from aggregation result -""" + Returns: + List of documents from aggregation result +""" async def execute_aggregation_on_collection(collection, pipeline: list) -> list: """Helper function to execute aggregation pipeline on a specified collection and return results""" @@ -1360,15 +1358,15 @@ async def execute_aggregation_on_collection(collection, pipeline: list) -> list: return results -""" +""" Helper function to generate vector embeddings from an input. - Args: + Args: data: Input data to generate embeddings for input_type: Type of input data client: Voyage AI client instance - Returns: + Returns: Vector embeddings for the given input """ @@ -1376,17 +1374,17 @@ def get_embedding(data, input_type = "document", client=None): """ Helper function to generate vector embeddings from an input. - Args: + Args: data: Input data to generate embeddings for input_type: Type of input data client: Voyage AI client instance - Returns: + Returns: Vector embeddings for the given input """ if client is None: client = voyageai.Client() - + embeddings = client.embed( data, model = model, output_dimension = outputDimension, input_type = input_type ).embeddings