From 5ba4f9edcaa2933af288799a0baf93208d367765 Mon Sep 17 00:00:00 2001 From: Angela Date: Fri, 24 Oct 2025 15:16:55 -0400 Subject: [PATCH 001/110] add new methods --- server/python/src/models/models.py | 14 +- server/python/src/routers/movies.py | 431 +++++++++++++++++++++++++++- 2 files changed, 441 insertions(+), 4 deletions(-) diff --git a/server/python/src/models/models.py b/server/python/src/models/models.py index 9e66a82..e027116 100644 --- a/server/python/src/models/models.py +++ b/server/python/src/models/models.py @@ -92,9 +92,18 @@ class CreateMovieRequest(BaseModel): rated: Optional[str] = None runtime: Optional[int] = None poster: Optional[str] = None + +class Comment(BaseModel): + id: Optional[str] = Field(alias="_id") + name: str + email: str + movie_id: str + text: str + date: datetime - - + model_config = { + "populate_by_name": True + } class SuccessResponse(BaseModel, Generic[T]): success: bool = True message: Optional[str] @@ -113,4 +122,3 @@ class ErrorResponse(BaseModel): message: str error: ErrorDetails timestamp: str - \ No newline at end of file diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index efdf6dd..fab2e73 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,10 +1,12 @@ -from fastapi import APIRouter, Query +from fastapi import APIRouter, Query, Body, HTTPException from src.database.mongo_client import db, get_collection from src.models.models import CreateMovieRequest, Movie, SuccessResponse from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response import re +from bson import ObjectId +from bson.errors import InvalidId ''' This file contains all the business logic for movie operations. @@ -22,7 +24,26 @@ # Place get_movie_by_id endpoint here #------------------------------------ +""" + GET /api/movies/{id} + Retrieve a single movie by its ID. + Path Parameters: + id (str): The ObjectId of the movie to retrieve. + Returns: + SuccessResponse[Movie]: A response object containing the movie data. +""" + +@router.get("/{id}", response_model=SuccessResponse[Movie]) +async def get_movie_by_id(id: str): + # Validate ObjectId format + object_id = ObjectId(id) + + # Use findOne() to get a single document by _id + movie = await db.movies.find_one({"_id": object_id}) + movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + + return create_success_response(movie, "Movie retrieved successfully") """ GET /api/movies/ @@ -101,6 +122,33 @@ async def get_all_movies( # Place create_movie endpoint here #------------------------------------ +""" + POST /api/movies/ + Create a new movie. + Request Body: + title (str): The title of the movie. + genre (str): The genre of the movie. + year (int): The year the movie was released. + min_rating (float): The minimum IMDB rating. + max_rating (float): The maximum IMDB rating. + Returns: + SuccessResponse[Movie]: A response object containing the created movie data. +""" + +@router.post("/", response_model=SuccessResponse[CreateMovieRequest], status_code=201) +async def create_movie(movie: CreateMovieRequest): + # Pydantic will automatically validate the structure + movie_data = movie.model_dump(by_alias=True, exclude_none=True) + + result = await db.movies.insert_one(movie_data) + + # Retrieve the created document to return complete data + created_movie = await db.movies.find_one({"_id": result.inserted_id}) + + created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string + + return create_success_response(created_movie, f"Movie '{movie_data['title']}' created successfully") + #------------------------------------ # Place create_movies_batch endpoint here #------------------------------------ @@ -159,6 +207,31 @@ async def create_movies_batch(movies: List[CreateMovieRequest]): # Place delete_movie endpoint here #------------------------------------ + +""" + DELETE /api/movies/{id} + Delete a single movie by its ID. + Path Parameters: + id (str): The ObjectId of the movie to delete. + Returns: + SuccessResponse[dict]: A response object containing deletion details. +""" + +@router.delete("/{id}", response_model=SuccessResponse[dict]) +async def delete_movie_by_id(id: str): + object_id = ObjectId(id) + + # Use deleteOne() to remove a single document + result = await db.movies.delete_one({"_id": object_id}) + + if result.deleted_count == 0: + raise HTTPException(status_code=404, detail="Movie not found") + + return create_success_response( + {"deletedCount": result.deleted_count}, + "Movie deleted successfully" + ) + #------------------------------------ # Place delete_movies_by_batch endpoint here #------------------------------------ @@ -167,6 +240,362 @@ async def create_movies_batch(movies: List[CreateMovieRequest]): # Place find_and_delete_movie endpoint here #------------------------------------ +""" + DELETE /api/movies/{id}/find-and-delete + Finds and deletes a movie in a single atomic operation. + Demonstrates the findOneAndDelete() operation. + Path Parameters: + id (str): The ObjectId of the movie to find and delete. + Returns: + SuccessResponse[Movie]: A response object containing the deleted movie data. +""" + +@router.delete("/{id}/find-and-delete", response_model=SuccessResponse[Movie]) +async def find_and_delete_movie(id: str): + object_id = ObjectId(id) + + # Use find_one_and_delete() to find and delete in a single atomic operation + # This is useful when you need to return the deleted document + # or ensure the document exists before deletion + deleted_movie = await db.movies.find_one_and_delete({"_id": object_id}) + + if deleted_movie is None: + raise HTTPException(status_code=404, detail="Movie not found") + + deleted_movie["_id"] = str(deleted_movie["_id"]) # Convert ObjectId to string + + return create_success_response(deleted_movie, "Movie found and deleted successfully") + +async def execute_aggregation(pipeline: list) -> list: + """Helper function to execute aggregation pipeline and return results""" + print(f"Executing pipeline: {pipeline}") # Debug logging + print(f"Database name: {db.name if hasattr(db, 'name') else 'unknown'}") + print(f"Collection name: movies") + + # For motor (async MongoDB driver), we need to await the aggregate call + cursor = await db.movies.aggregate(pipeline) + results = await cursor.to_list(length=None) # Convert cursor to list + + print(f"Aggregation returned {len(results)} results") # Debug logging + if len(results) <= 3: # Log first few results for debugging + for i, doc in enumerate(results): + print(f"Result {i+1}: {doc}") + + return results + + +""" + GET /api/movies/aggregate/by-genre + Aggregate movies by genre with statistics using MongoDB aggregation pipeline. + Demonstrates grouping values from multiple documents and performing operations on grouped data. + Returns: + SuccessResponse[List[dict]]: A response object containing aggregated genre statistics. +""" + +@router.get("/aggregate/by-genre", response_model=SuccessResponse[List[dict]]) +async def aggregate_movies_by_genre(): + # Define an aggregation pipeline with match, unwind, group, and sort stages + pipeline = [ + # Clean data: ensure year is an integer + { + "$match": { + "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + } + }, + {"$unwind": "$genres"}, + { + "$group": { + "_id": "$genres", + "count": {"$sum": 1}, + "avgRating": {"$avg": "$imdb.rating"}, + "minYear": {"$min": "$year"}, + "maxYear": {"$max": "$year"}, + "totalVotes": {"$sum": "$imdb.votes"} + } + }, + {"$sort": {"count": -1}}, + { + "$project": { + "genre": "$_id", + "movieCount": "$count", + "averageRating": {"$round": ["$avgRating", 2]}, + "yearRange": { + "min": "$minYear", + "max": "$maxYear" + }, + "totalVotes": "$totalVotes", + "_id": 0 + } + } + ] + + # Execute the aggregation + results = await execute_aggregation(pipeline) + + return create_success_response( + results, + f"Aggregated statistics for {len(results)} genres" + ) + + +""" + GET /api/movies/aggregate/recent-commented + Aggregate movies with their most recent comments using MongoDB $lookup aggregation. + Joins movies with comments collection to show recent comment activity. + Query Parameters: + limit (int, optional): Number of results to return (default: 10, max: 50). + movie_id (str, optional): Filter by specific movie ObjectId. + Returns: + SuccessResponse[List[dict]]: A response object containing movies with their most recent comments. +""" + +@router.get("/aggregate/recent-commented", response_model=SuccessResponse[List[dict]]) +async def aggregate_movies_recent_commented( + limit: int = Query(default=10, ge=1, le=50), + movie_id: str = Query(default=None) +): + # Define aggregation pipeline to join movies with their most recent comments + pipeline = [ + { + "$match": { + "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + } + } + ] + + # Add movie_id filter if provided + if movie_id: + try: + object_id = ObjectId(movie_id) + pipeline[0]["$match"]["_id"] = object_id + except Exception: + raise HTTPException(status_code=400, detail="Invalid movie_id format") + + # Add lookup and additional pipeline stages + pipeline.extend([ + { + "$lookup": { + "from": "comments", + "localField": "_id", + "foreignField": "movie_id", + "as": "comments" + } + }, + { + "$match": { + "comments": {"$ne": []} + } + }, + { + "$addFields": { + "recentComments": { + "$slice": [ + { + "$sortArray": { + "input": "$comments", + "sortBy": {"date": -1} + } + }, + limit + ] + }, + "mostRecentCommentDate": { + "$max": "$comments.date" + } + } + }, + { + "$sort": {"mostRecentCommentDate": -1} + }, + { + "$limit": 50 if movie_id else 20 + }, + { + "$project": { + "title": 1, + "year": 1, + "genres": 1, + "imdbRating": "$imdb.rating", + "recentComments": { + "$map": { + "input": "$recentComments", + "as": "comment", + "in": { + "userName": "$$comment.name", + "userEmail": "$$comment.email", + "text": "$$comment.text", + "date": "$$comment.date" + } + } + }, + "totalComments": {"$size": "$comments"}, + "_id": 1 + } + } + ]) + + # Execute the aggregation + results = await execute_aggregation(pipeline) + + # Convert ObjectId to string for response + 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, + f"Found {total_comments} comments from movie{'s' if len(results) != 1 else ''}" + ) + + +""" + GET /api/movies/aggregate/by-year + Aggregate movies by year with average rating and movie count. + Reports yearly statistics including average rating and total movies per year. + Returns: + SuccessResponse[List[dict]]: A response object containing yearly movie statistics. +""" + +@router.get("/aggregate/by-year", response_model=SuccessResponse[List[dict]]) +async def aggregate_movies_by_year(): + # Define aggregation pipeline to group movies by year + pipeline = [ + # Clean data: ensure year is an integer and within reasonable range + { + "$match": { + "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + } + }, + # Group by year and calculate statistics + { + "$group": { + "_id": "$year", + "movieCount": {"$sum": 1}, + "averageRating": { + "$avg": { + "$cond": [ + {"$and": [ + {"$ne": ["$imdb.rating", None]}, + {"$ne": ["$imdb.rating", ""]}, + {"$eq": [{"$type": "$imdb.rating"}, "double"]} + ]}, + "$imdb.rating", + "$$REMOVE" + ] + } + }, + "highestRating": { + "$max": { + "$cond": [ + {"$and": [ + {"$ne": ["$imdb.rating", None]}, + {"$ne": ["$imdb.rating", ""]}, + {"$eq": [{"$type": "$imdb.rating"}, "double"]} + ]}, + "$imdb.rating", + "$$REMOVE" + ] + } + }, + "lowestRating": { + "$min": { + "$cond": [ + {"$and": [ + {"$ne": ["$imdb.rating", None]}, + {"$ne": ["$imdb.rating", ""]}, + {"$eq": [{"$type": "$imdb.rating"}, "double"]} + ]}, + "$imdb.rating", + "$$REMOVE" + ] + } + }, + "totalVotes": {"$sum": "$imdb.votes"} + } + }, + { + "$project": { + "year": "$_id", + "movieCount": 1, + "averageRating": {"$round": ["$averageRating", 2]}, + "highestRating": 1, + "lowestRating": 1, + "totalVotes": 1, + "_id": 0 + } + }, + {"$sort": {"year": -1}} + ] + + # Execute the aggregation + results = await execute_aggregation(pipeline) + + return create_success_response( + results, + f"Aggregated statistics for {len(results)} years" + ) + + +""" + GET /api/movies/aggregate/directors + Aggregate directors with the most movies and their statistics. + Reports directors sorted by number of movies directed. + Query Parameters: + limit (int, optional): Number of results to return (default: 20, max: 100). + min_movies (int, optional): Minimum number of movies to include director (default: 1). + Returns: + SuccessResponse[List[dict]]: A response object containing director statistics. +""" + +@router.get("/aggregate/directors", response_model=SuccessResponse[List[dict]]) +async def aggregate_directors_most_movies( + limit: int = Query(default=20, ge=1, le=100) +): + # Define aggregation pipeline to find directors with most movies + pipeline = [ + { + "$match": { + "directors": {"$exists": True, "$ne": None, "$ne": []}, + "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + } + }, + { + "$unwind": "$directors" + }, + { + "$match": { + "directors": {"$ne": None, "$ne": ""} + } + }, + { + "$group": { + "_id": "$directors", + "movieCount": {"$sum": 1}, + "averageRating": {"$avg": "$imdb.rating"} + } + }, + {"$sort": {"movieCount": -1}}, + {"$limit": limit}, + { + "$project": { + "director": "$_id", + "movieCount": 1, + "averageRating": {"$round": ["$averageRating", 2]}, + "_id": 0 + } + } + ] + + # Execute the aggregation + results = await execute_aggregation(pipeline) + + return create_success_response( + results, + f"Found {len(results)} directors with most movies" + ) # ---- Old testing endpoint, will be removed later ---- ''' From 1731299c18f763342cf259d1aea1d5bb26667c11 Mon Sep 17 00:00:00 2001 From: Angela Date: Fri, 24 Oct 2025 15:20:57 -0400 Subject: [PATCH 002/110] remove comment line --- server/python/src/routers/movies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index fab2e73..7c60599 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -545,7 +545,6 @@ async def aggregate_movies_by_year(): Reports directors sorted by number of movies directed. Query Parameters: limit (int, optional): Number of results to return (default: 20, max: 100). - min_movies (int, optional): Minimum number of movies to include director (default: 1). Returns: SuccessResponse[List[dict]]: A response object containing director statistics. """ From 303fecc160007e75004cdd53df931cfc9619b724 Mon Sep 17 00:00:00 2001 From: Angela Date: Fri, 24 Oct 2025 15:32:57 -0400 Subject: [PATCH 003/110] add comment for vector search --- server/python/src/routers/movies.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 7c60599..237ef9d 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -609,4 +609,7 @@ async def test_error(): code="TEST_ERROR", details=str(e) ) -''' \ No newline at end of file +''' + +# ---- Place Vector Search Here ---- + From e6942b2b8296fb5c28542cceb47084418841acd5 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Oct 2025 15:40:37 -0400 Subject: [PATCH 004/110] feedback --- server/python/src/routers/movies.py | 95 +++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 237ef9d..b4e5128 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -36,10 +36,19 @@ @router.get("/{id}", response_model=SuccessResponse[Movie]) async def get_movie_by_id(id: str): # Validate ObjectId format - object_id = ObjectId(id) + try: + object_id = ObjectId(id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid movie ID format") + + movies_collection = get_collection("movies") + try: + movie = await movies_collection.find_one({"_id": object_id}) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") - # Use findOne() to get a single document by _id - movie = await db.movies.find_one({"_id": object_id}) + if movie is None: + raise HTTPException(status_code=400, detail="Movie not found") movie["_id"] = str(movie["_id"]) # Convert ObjectId to string @@ -105,7 +114,10 @@ async def get_all_movies( cursor = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) movies = [] async for movie in cursor: + if movie is None: + raise HTTPException(status_code=400, detail="Movie not found") movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + # Ensure that the year field contains int value. if "year" in movie and not isinstance(movie["year"], int): cleaned_year = re.sub(r"\D", "", str(movie["year"])) @@ -126,24 +138,35 @@ async def get_all_movies( POST /api/movies/ Create a new movie. Request Body: - title (str): The title of the movie. - genre (str): The genre of the movie. - year (int): The year the movie was released. - min_rating (float): The minimum IMDB rating. - max_rating (float): The maximum IMDB rating. + movie (CreateMovieRequest): A movie object containing the movie data. + See CreateMovieRequest model for available fields. Returns: SuccessResponse[Movie]: A response object containing the created movie data. """ -@router.post("/", response_model=SuccessResponse[CreateMovieRequest], status_code=201) +@router.post("/", response_model=SuccessResponse[Movie], status_code=201) async def create_movie(movie: CreateMovieRequest): # Pydantic will automatically validate the structure movie_data = movie.model_dump(by_alias=True, exclude_none=True) - result = await db.movies.insert_one(movie_data) - - # Retrieve the created document to return complete data - created_movie = await db.movies.find_one({"_id": result.inserted_id}) + movies_collection = get_collection("movies") + try: + result = await movies_collection.insert_one(movie_data) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + + # Verify that the document was created before querying it + if not result.acknowledged: + raise HTTPException(status_code=500, detail="Failed to create movie") + + try: + # Retrieve the created document to return complete data + created_movie = await movies_collection.find_one({"_id": result.inserted_id}) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + + if created_movie is None: + raise HTTPException(status_code=500, detail="Movie was created but could not be retrieved") created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string @@ -219,13 +242,24 @@ async def create_movies_batch(movies: List[CreateMovieRequest]): @router.delete("/{id}", response_model=SuccessResponse[dict]) async def delete_movie_by_id(id: str): - object_id = ObjectId(id) + try: + object_id = ObjectId(id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid movie ID format") - # Use deleteOne() to remove a single document - result = await db.movies.delete_one({"_id": object_id}) + movies_collection = get_collection("movies") + try: + # Use deleteOne() to remove a single document + result = await movies_collection.delete_one({"_id": object_id}) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") if result.deleted_count == 0: - raise HTTPException(status_code=404, detail="Movie not found") + return create_error_response( + message="Movie not found", + code="MOVIE_NOT_FOUND", + details=f"No movie found with ID: {id}" + ) return create_success_response( {"deletedCount": result.deleted_count}, @@ -252,16 +286,26 @@ async def delete_movie_by_id(id: str): @router.delete("/{id}/find-and-delete", response_model=SuccessResponse[Movie]) async def find_and_delete_movie(id: str): - object_id = ObjectId(id) + try: + object_id = ObjectId(id) + except InvalidId: + raise HTTPException(status_code=400, detail="Invalid movie ID format") + movies_collection = get_collection("movies") # Use find_one_and_delete() to find and delete in a single atomic operation # This is useful when you need to return the deleted document # or ensure the document exists before deletion - deleted_movie = await db.movies.find_one_and_delete({"_id": object_id}) + try: + deleted_movie = await movies_collection.find_one_and_delete({"_id": object_id}) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") if deleted_movie is None: - raise HTTPException(status_code=404, detail="Movie not found") - + return create_error_response( + message="Movie not found", + code="MOVIE_NOT_FOUND", + 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") @@ -269,12 +313,11 @@ async def find_and_delete_movie(id: str): async def execute_aggregation(pipeline: list) -> list: """Helper function to execute aggregation pipeline and return results""" print(f"Executing pipeline: {pipeline}") # Debug logging - print(f"Database name: {db.name if hasattr(db, 'name') else 'unknown'}") - print(f"Collection name: movies") - # For motor (async MongoDB driver), we need to await the aggregate call - cursor = await db.movies.aggregate(pipeline) - results = await cursor.to_list(length=None) # Convert cursor to list + 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 print(f"Aggregation returned {len(results)} results") # Debug logging if len(results) <= 3: # Log first few results for debugging From aa1db244e7b5ee72024db786c00c12d25cf65dc1 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Oct 2025 16:33:25 -0400 Subject: [PATCH 005/110] pr feedback --- server/python/src/routers/movies.py | 222 ++++++++++++++++------------ 1 file changed, 126 insertions(+), 96 deletions(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index b4e5128..6c4895f 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -39,16 +39,28 @@ async def get_movie_by_id(id: str): try: object_id = ObjectId(id) except InvalidId: - raise HTTPException(status_code=400, detail="Invalid movie ID format") + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) movies_collection = get_collection("movies") try: movie = await movies_collection.find_one({"_id": object_id}) except Exception as e: - raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) if movie is None: - raise HTTPException(status_code=400, detail="Movie not found") + return create_error_response( + message="Movie not found", + code="INTERNAL_SERVER_ERROR", + details=f"No movie found with ID: {id}" + ) movie["_id"] = str(movie["_id"]) # Convert ObjectId to string @@ -111,21 +123,28 @@ async def get_all_movies( sort = [(sort_by, sort_order)] # Query the database with the constructed filter, sort, skip, and limit. - cursor = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) - movies = [] - async for movie in cursor: - if movie is None: - raise HTTPException(status_code=400, detail="Movie not found") - movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - - # Ensure that the year field contains int value. - if "year" in movie and not isinstance(movie["year"], int): - cleaned_year = re.sub(r"\D", "", str(movie["year"])) - try: - movie["year"] = int(cleaned_year) if cleaned_year else None - except ValueError: - movie["year"] = None - movies.append(movie) + try: + cursor = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) + movies = [] + async for movie in cursor: + if movie is None: + continue # Skip null movies instead of raising exception + movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + + # Ensure that the year field contains int value. + if "year" in movie and not isinstance(movie["year"], int): + cleaned_year = re.sub(r"\D", "", str(movie["year"])) + try: + movie["year"] = int(cleaned_year) if cleaned_year else None + except ValueError: + movie["year"] = None + movies.append(movie) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) # Return the results wrapped in a SuccessResponse return create_success_response(movies, f"Found {len(movies)} movies.") @@ -153,20 +172,36 @@ async def create_movie(movie: CreateMovieRequest): try: result = await movies_collection.insert_one(movie_data) except Exception as e: - raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) # Verify that the document was created before querying it if not result.acknowledged: - raise HTTPException(status_code=500, detail="Failed to create movie") + return create_error_response( + message="Failed to create movie", + 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}) except Exception as e: - raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) if created_movie is None: - raise HTTPException(status_code=500, detail="Movie was created but could not be retrieved") + return create_error_response( + message="Movie creation verification failed", + code="INTERNAL_SERVER_ERROR", + details="Movie was created but could not be retrieved for verification" + ) created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string @@ -208,7 +243,16 @@ async def create_movies_batch(movies: List[CreateMovieRequest]): movies_dicts = [] for movie in movies: movies_dicts.append(movie.model_dump(exclude_unset=True, exclude_none=True)) - result = await movies_collection.insert_many(movies_dicts) + + try: + result = await movies_collection.insert_many(movies_dicts) + except Exception as e: + return create_error_response( + message="Database error occurred during batch creation", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + return create_success_response({ "insertedCount": len(result.inserted_ids), "insertedIds": [str(_id) for _id in result.inserted_ids] @@ -245,19 +289,27 @@ async def delete_movie_by_id(id: str): try: object_id = ObjectId(id) except InvalidId: - raise HTTPException(status_code=400, detail="Invalid movie ID format") + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) movies_collection = get_collection("movies") try: # Use deleteOne() to remove a single document result = await movies_collection.delete_one({"_id": object_id}) except Exception as e: - raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) if result.deleted_count == 0: return create_error_response( message="Movie not found", - code="MOVIE_NOT_FOUND", + code="INTERNAL_SERVER_ERROR", details=f"No movie found with ID: {id}" ) @@ -289,7 +341,11 @@ async def find_and_delete_movie(id: str): try: object_id = ObjectId(id) except InvalidId: - raise HTTPException(status_code=400, detail="Invalid movie ID format") + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) movies_collection = get_collection("movies") # Use find_one_and_delete() to find and delete in a single atomic operation @@ -298,12 +354,16 @@ async def find_and_delete_movie(id: str): try: deleted_movie = await movies_collection.find_one_and_delete({"_id": object_id}) except Exception as e: - raise HTTPException(status_code=500, detail=f"Database error occurred: {str(e)}") + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) if deleted_movie is None: return create_error_response( message="Movie not found", - code="MOVIE_NOT_FOUND", + code="INTERNAL_SERVER_ERROR", details=f"No movie found with ID: {id}" ) deleted_movie["_id"] = str(deleted_movie["_id"]) # Convert ObjectId to string @@ -326,63 +386,8 @@ async def execute_aggregation(pipeline: list) -> list: return results - -""" - GET /api/movies/aggregate/by-genre - Aggregate movies by genre with statistics using MongoDB aggregation pipeline. - Demonstrates grouping values from multiple documents and performing operations on grouped data. - Returns: - SuccessResponse[List[dict]]: A response object containing aggregated genre statistics. -""" - -@router.get("/aggregate/by-genre", response_model=SuccessResponse[List[dict]]) -async def aggregate_movies_by_genre(): - # Define an aggregation pipeline with match, unwind, group, and sort stages - pipeline = [ - # Clean data: ensure year is an integer - { - "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} - } - }, - {"$unwind": "$genres"}, - { - "$group": { - "_id": "$genres", - "count": {"$sum": 1}, - "avgRating": {"$avg": "$imdb.rating"}, - "minYear": {"$min": "$year"}, - "maxYear": {"$max": "$year"}, - "totalVotes": {"$sum": "$imdb.votes"} - } - }, - {"$sort": {"count": -1}}, - { - "$project": { - "genre": "$_id", - "movieCount": "$count", - "averageRating": {"$round": ["$avgRating", 2]}, - "yearRange": { - "min": "$minYear", - "max": "$maxYear" - }, - "totalVotes": "$totalVotes", - "_id": 0 - } - } - ] - - # Execute the aggregation - results = await execute_aggregation(pipeline) - - return create_success_response( - results, - f"Aggregated statistics for {len(results)} genres" - ) - - """ - GET /api/movies/aggregate/recent-commented + GET /api/movies/reportingByComments Aggregate movies with their most recent comments using MongoDB $lookup aggregation. Joins movies with comments collection to show recent comment activity. Query Parameters: @@ -392,7 +397,7 @@ async def aggregate_movies_by_genre(): SuccessResponse[List[dict]]: A response object containing movies with their most recent comments. """ -@router.get("/aggregate/recent-commented", response_model=SuccessResponse[List[dict]]) +@router.get("/api/movies/reportingByComments", response_model=SuccessResponse[List[dict]]) async def aggregate_movies_recent_commented( limit: int = Query(default=10, ge=1, le=50), movie_id: str = Query(default=None) @@ -412,7 +417,11 @@ async def aggregate_movies_recent_commented( object_id = ObjectId(movie_id) pipeline[0]["$match"]["_id"] = object_id except Exception: - raise HTTPException(status_code=400, detail="Invalid movie_id format") + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details="The provided movie_id is not a valid ObjectId" + ) # Add lookup and additional pipeline stages pipeline.extend([ @@ -451,7 +460,7 @@ async def aggregate_movies_recent_commented( "$sort": {"mostRecentCommentDate": -1} }, { - "$limit": 50 if movie_id else 20 + "$limit": limit }, { "$project": { @@ -478,8 +487,15 @@ async def aggregate_movies_recent_commented( ]) # Execute the aggregation - results = await execute_aggregation(pipeline) - + try: + results = await execute_aggregation(pipeline) + except Exception as e: + return create_error_response( + message="Database error occurred during aggregation", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + # Convert ObjectId to string for response for result in results: if "_id" in result: @@ -495,14 +511,14 @@ async def aggregate_movies_recent_commented( """ - GET /api/movies/aggregate/by-year + GET /api/movies/reportingByYear Aggregate movies by year with average rating and movie count. Reports yearly statistics including average rating and total movies per year. Returns: SuccessResponse[List[dict]]: A response object containing yearly movie statistics. """ -@router.get("/aggregate/by-year", response_model=SuccessResponse[List[dict]]) +@router.get("/api/movies/reportingByYear", response_model=SuccessResponse[List[dict]]) async def aggregate_movies_by_year(): # Define aggregation pipeline to group movies by year pipeline = [ @@ -574,7 +590,14 @@ async def aggregate_movies_by_year(): ] # Execute the aggregation - results = await execute_aggregation(pipeline) + try: + results = await execute_aggregation(pipeline) + except Exception as e: + return create_error_response( + message="Database error occurred during aggregation", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) return create_success_response( results, @@ -583,7 +606,7 @@ async def aggregate_movies_by_year(): """ - GET /api/movies/aggregate/directors + GET /api/movies/reportingByDirectors Aggregate directors with the most movies and their statistics. Reports directors sorted by number of movies directed. Query Parameters: @@ -592,7 +615,7 @@ async def aggregate_movies_by_year(): SuccessResponse[List[dict]]: A response object containing director statistics. """ -@router.get("/aggregate/directors", response_model=SuccessResponse[List[dict]]) +@router.get("/api/movies/reportingByDirectors", response_model=SuccessResponse[List[dict]]) async def aggregate_directors_most_movies( limit: int = Query(default=20, ge=1, le=100) ): @@ -632,7 +655,14 @@ async def aggregate_directors_most_movies( ] # Execute the aggregation - results = await execute_aggregation(pipeline) + try: + results = await execute_aggregation(pipeline) + except Exception as e: + return create_error_response( + message="Database error occurred during aggregation", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) return create_success_response( results, From 0362be78d9a54b3785e9f12983687d29cfcfedbc Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Oct 2025 16:39:04 -0400 Subject: [PATCH 006/110] remove unneeded imports --- server/python/src/routers/movies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 6c4895f..0d2f785 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,5 +1,5 @@ -from fastapi import APIRouter, Query, Body, HTTPException -from src.database.mongo_client import db, get_collection +from fastapi import APIRouter, Query +from src.database.mongo_client import get_collection from src.models.models import CreateMovieRequest, Movie, SuccessResponse from typing import List from datetime import datetime From 1cc125a0be7a7e9cc15e6e5b3503757749d01252 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 28 Oct 2025 16:01:12 -0400 Subject: [PATCH 007/110] add agg stage comments --- server/python/src/routers/movies.py | 164 ++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 35 deletions(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 0d2f785..bbde50f 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -165,7 +165,7 @@ async def get_all_movies( @router.post("/", response_model=SuccessResponse[Movie], status_code=201) async def create_movie(movie: CreateMovieRequest): - # Pydantic will automatically validate the structure + # Pydantic automatically validates the structure movie_data = movie.model_dump(by_alias=True, exclude_none=True) movies_collection = get_collection("movies") @@ -423,8 +423,29 @@ async def aggregate_movies_recent_commented( details="The provided movie_id is not a valid ObjectId" ) - # Add lookup and additional pipeline stages + # Add a multi-stage aggregation that: + # 1. Filters movies by valid year range + # 2. Joins with comments collection (like SQL JOIN) + # 3. Filters to only movies that have comments + # 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 = [ + # STAGE 1: $match - Initial Filter + # Filter movies to only those with valid year data + # Tip: Use $match early to reduce the initial dataset for better performance + { + "$match": { + "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + } + } + ] + + # Add remaining pipeline stages pipeline.extend([ + # STAGE 2: $lookup - Join with the 'comments' Collection + # This gives each movie document a 'comments' array containing all its comments { "$lookup": { "from": "comments", @@ -433,59 +454,74 @@ async def aggregate_movies_recent_commented( "as": "comments" } }, + # STAGE 3: $match - Filter Movies with at Least One Comment + # This helps reduces dataset to only movies with user engagement { "$match": { "comments": {"$ne": []} } }, + # STAGE 4: $addFields - Add New Computed Fields { "$addFields": { + # Add computed field 'recentComments' that extracts only the N most recent comments (up to 'limit') "recentComments": { "$slice": [ { "$sortArray": { "input": "$comments", - "sortBy": {"date": -1} + "sortBy": {"date": -1} # -1 = descending (newest first) } }, - limit + limit # Number of comments to keep ] }, + # Add computed field 'mostRecentCommentDate' that gets the date of the most recent comment (to use in the next $sort stage) "mostRecentCommentDate": { "$max": "$comments.date" } } }, + # STAGE 5: $sort - Sort Movies by Most Recent Comment Date { "$sort": {"mostRecentCommentDate": -1} }, + # STAGE 6: $limit - Restrict Result Set Size + # - If querying single movie: return up to 50 results + # - If querying all movies: return up to 20 results + # Tip: This prevents overwhelming the client with too much data { - "$limit": limit + "$limit": 50 if movie_id else 20 }, + # STAGE 7: $project - Shape Final Response Output { "$project": { + # Include basic movie fields "title": 1, "year": 1, "genres": 1, + "_id": 1, + # Extract nested field: imdb.rating -> imdbRating "imdbRating": "$imdb.rating", + # Use $map to reshape computed 'recentComments' field with cleaner field names "recentComments": { "$map": { "input": "$recentComments", "as": "comment", "in": { - "userName": "$$comment.name", - "userEmail": "$$comment.email", - "text": "$$comment.text", - "date": "$$comment.date" + "userName": "$$comment.name", # Rename: name -> userName + "userEmail": "$$comment.email", # Rename: email -> userEmail + "text": "$$comment.text", # Keep: text + "date": "$$comment.date" # Keep: date } } }, - "totalComments": {"$size": "$comments"}, - "_id": 1 + # Calculate the total number of comments into 'totalComments' (not just 'recentComments') + # Used in display (e.g., "Showing 5 of 127 comments") + "totalComments": {"$size": "$comments"} } } ]) - # Execute the aggregation try: results = await execute_aggregation(pipeline) @@ -520,32 +556,48 @@ async def aggregate_movies_recent_commented( @router.get("/api/movies/reportingByYear", response_model=SuccessResponse[List[dict]]) async def aggregate_movies_by_year(): - # Define aggregation pipeline to group movies by year + # Define aggregation pipeline to group movies by year with statistics + # This pipeline demonstrates grouping, statistical calculations, and data cleaning + + # Add a multi-stage aggregation that: + # 1. Filters movies by valid year range (data quality filter) + # 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 + # Tip: Filter early to reduce dataset size and improve performance { "$match": { "year": {"$type": "number", "$gte": 1800, "$lte": 2030} } }, - # Group by year and calculate statistics + + # STAGE 2: $group - Aggregate Movies by Year + # Group all movies by their release year and calculate various statistics { "$group": { - "_id": "$year", - "movieCount": {"$sum": 1}, + "_id": "$year", # Group by year field + "movieCount": {"$sum": 1}, # Count total movies per year + + # Calculate average rating (only for valid numeric ratings) "averageRating": { "$avg": { "$cond": [ {"$and": [ - {"$ne": ["$imdb.rating", None]}, - {"$ne": ["$imdb.rating", ""]}, - {"$eq": [{"$type": "$imdb.rating"}, "double"]} + {"$ne": ["$imdb.rating", None]}, # Not null + {"$ne": ["$imdb.rating", ""]}, # Not empty string + {"$eq": [{"$type": "$imdb.rating"}, "double"]} # Is numeric ]}, - "$imdb.rating", - "$$REMOVE" + "$imdb.rating", # Include valid IMDB ratings + "$$REMOVE" # Exclude invalid IMDB ratings ] } }, + + # Find highest rating for the year (same validation as average rating) "highestRating": { "$max": { "$cond": [ @@ -559,6 +611,8 @@ async def aggregate_movies_by_year(): ] } }, + + # Find lowest rating for the year (same validation as average and highest rating) "lowestRating": { "$min": { "$cond": [ @@ -572,21 +626,29 @@ 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 { "$project": { - "year": "$_id", + "year": "$_id", # Rename _id back to year because grouping was done by year but values were stored in _id "movieCount": 1, - "averageRating": {"$round": ["$averageRating", 2]}, + "averageRating": {"$round": ["$averageRating", 2]}, # Round to 2 decimal places "highestRating": 1, "lowestRating": 1, "totalVotes": 1, - "_id": 0 + "_id": 0 # Exclude the _id field from output } }, - {"$sort": {"year": -1}} + + # 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 ] # Execute the aggregation @@ -619,37 +681,69 @@ async def aggregate_movies_by_year(): async def aggregate_directors_most_movies( limit: int = Query(default=20, ge=1, le=100) ): - # Define aggregation pipeline to find directors with 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 + # 3. Cleans director names by filtering out null/empty names + # 4. Groups movies by individual director and calculates statistics per director + # 5. Sorts directors based on movie count + # 6. Limits results to top N directors + # 7. Shapes the final output with clean field names and rounded averages + pipeline = [ + # STAGE 1: $match - Initial Data Quality Filter + # Filter movies that have director information and valid years { "$match": { - "directors": {"$exists": True, "$ne": None, "$ne": []}, - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + "directors": {"$exists": True, "$ne": None, "$ne": []}, # Has directors array + "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 { "$match": { "directors": {"$ne": None, "$ne": ""} } }, + + # STAGE 4: $group - Aggregate by Director + # Group all movies by director name and calculate statistics { "$group": { - "_id": "$directors", - "movieCount": {"$sum": 1}, - "averageRating": {"$avg": "$imdb.rating"} + "_id": "$directors", # Group by individual director name + "movieCount": {"$sum": 1}, # Count movies per director + "averageRating": {"$avg": "$imdb.rating"} # Average rating of director's movies } }, - {"$sort": {"movieCount": -1}}, + + # 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 { "$project": { - "director": "$_id", + "director": "$_id", # Rename _id to director "movieCount": 1, - "averageRating": {"$round": ["$averageRating", 2]}, - "_id": 0 + "averageRating": {"$round": ["$averageRating", 2]}, # Round to 2 decimal places + "_id": 0 # Exclude the _id field from output } } ] From 49b967a841648beb1d21c7737eada963c4725732 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 28 Oct 2025 16:23:00 -0400 Subject: [PATCH 008/110] fix broken code --- server/python/src/routers/movies.py | 32 +++++++++++------------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index bbde50f..6e8d959 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -402,26 +402,6 @@ async def aggregate_movies_recent_commented( limit: int = Query(default=10, ge=1, le=50), movie_id: str = Query(default=None) ): - # Define aggregation pipeline to join movies with their most recent comments - pipeline = [ - { - "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} - } - } - ] - - # Add movie_id filter if provided - if movie_id: - try: - object_id = ObjectId(movie_id) - pipeline[0]["$match"]["_id"] = object_id - except Exception: - return create_error_response( - message="Invalid movie ID format", - code="INTERNAL_SERVER_ERROR", - details="The provided movie_id is not a valid ObjectId" - ) # Add a multi-stage aggregation that: # 1. Filters movies by valid year range @@ -441,6 +421,18 @@ async def aggregate_movies_recent_commented( } } ] + + # Add movie_id filter if provided + if movie_id: + try: + object_id = ObjectId(movie_id) + pipeline[0]["$match"]["_id"] = object_id + except Exception: + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details="The provided movie_id is not a valid ObjectId" + ) # Add remaining pipeline stages pipeline.extend([ From 9536cde3caae156cd6b0c884e7bfe6b1903eb176 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 14:06:21 -0400 Subject: [PATCH 009/110] feat: set up repo structure and backend connection --- .gitignore | 54 + JAVA-SPRING-IMPLEMENTATION-PLAN.md | 665 +++ client/next-env.d.ts | 6 + client/package-lock.json | 5007 +++++++++++++++++ server/express/package-lock.json | 4748 ++++++++++++++++ server/java-spring/.env.example | 13 + server/java-spring/.gitignore | 66 + .../.mvn/wrapper/maven-wrapper.properties | 3 + server/java-spring/README.md | 200 + server/java-spring/mvnw | 295 + server/java-spring/mvnw.cmd | 189 + server/java-spring/pom.xml | 85 + .../samplemflix/SampleMflixApplication.java | 23 + .../samplemflix/config/CorsConfig.java | 53 + .../config/DatabaseVerification.java | 145 + .../samplemflix/config/MongoConfig.java | 110 + .../controller/MovieController.java | 31 + .../exception/GlobalExceptionHandler.java | 26 + .../exception/ResourceNotFoundException.java | 20 + .../exception/ValidationException.java | 20 + .../com/mongodb/samplemflix/model/Movie.java | 38 + .../model/dto/CreateMovieRequest.java | 16 + .../model/dto/UpdateMovieRequest.java | 15 + .../model/response/ApiResponse.java | 15 + .../model/response/ErrorResponse.java | 15 + .../model/response/SuccessResponse.java | 15 + .../repository/MovieRepository.java | 27 + .../samplemflix/service/MovieService.java | 25 + .../samplemflix/util/ValidationUtils.java | 18 + .../src/main/resources/application.properties | 29 + .../controller/MovieControllerTest.java | 14 + .../integration/MovieIntegrationTest.java | 16 + .../samplemflix/service/MovieServiceTest.java | 14 + 33 files changed, 12016 insertions(+) create mode 100644 JAVA-SPRING-IMPLEMENTATION-PLAN.md create mode 100644 client/next-env.d.ts create mode 100644 client/package-lock.json create mode 100644 server/express/package-lock.json create mode 100644 server/java-spring/.env.example create mode 100644 server/java-spring/.gitignore create mode 100644 server/java-spring/.mvn/wrapper/maven-wrapper.properties create mode 100644 server/java-spring/README.md create mode 100755 server/java-spring/mvnw create mode 100644 server/java-spring/mvnw.cmd create mode 100644 server/java-spring/pom.xml create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ResourceNotFoundException.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ValidationException.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java create mode 100644 server/java-spring/src/main/resources/application.properties create mode 100644 server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java create mode 100644 server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java create mode 100644 server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java diff --git a/.gitignore b/.gitignore index e69de29..e90af98 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,54 @@ +# ============================================================================= +# Root .gitignore for MongoDB Sample Apps Monorepo +# ============================================================================= +# This file contains patterns common to all projects in the monorepo. +# Backend-specific patterns are defined in each backend's .gitignore file. +# ============================================================================= + +# Environment Variables (Global) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +!.env.example + +# Operating System Files +.DS_Store +Thumbs.db + +# IDE and Editor Files (Global) +.idea/ +.vscode/ +*.swp +*.swo +*.swn +*.bak +*.tmp +*.iml +.project +.classpath +.settings/ +*.sublime-project +*.sublime-workspace + +# Logs (Global) +logs/ +*.log + +# Temporary Files (Global) +*.tmp +*.temp +.cache/ + +# Node.js (Global - applies to all Node.js projects) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Test Coverage (Global) +coverage/ +*.lcov +.nyc_output diff --git a/JAVA-SPRING-IMPLEMENTATION-PLAN.md b/JAVA-SPRING-IMPLEMENTATION-PLAN.md new file mode 100644 index 0000000..e761fe8 --- /dev/null +++ b/JAVA-SPRING-IMPLEMENTATION-PLAN.md @@ -0,0 +1,665 @@ +# Java Spring Backend Implementation Plan + +## Executive Summary + +This document outlines the comprehensive implementation plan for translating the Express.js/TypeScript backend into a Java Spring Boot application. The implementation will maintain feature parity with the existing Express backend while following Spring Boot best practices and the MongoDB Java Driver conventions. + +--- + +## 1. Project Overview + +### 1.1 Current State Analysis + +**Existing Express Backend Features:** +- ✅ Basic CRUD operations (insertOne, insertMany, findOne, find, updateOne, updateMany, deleteOne, deleteMany, findOneAndDelete) +- ✅ MongoDB connection management with pre-flight verification +- ✅ Text search index creation and verification +- ✅ Comprehensive error handling +- ✅ Request validation +- ✅ CORS configuration +- ✅ Environment variable configuration +- ✅ Unit tests with Jest + +**Python Backend Status:** +- Partially implemented (only GET and batch POST endpoints) +- Uses FastAPI with PyMongo driver +- Async/await pattern + +### 1.2 Target Architecture + +**Java Spring Boot Stack:** +- **Framework:** Spring Boot 3.x +- **MongoDB Driver:** MongoDB Java Driver (latest stable version) +- **Build Tool:** Maven or Gradle +- **Java Version:** Java 17 or 21 (LTS) +- **Testing:** JUnit 5 + Mockito + Spring Boot Test +- **Documentation:** SpringDoc OpenAPI (Swagger) + +--- + +## 2. Project Structure + +### 2.1 Directory Layout + +``` +server/java-spring/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/ +│ │ │ └── mongodb/ +│ │ │ └── samplemflix/ +│ │ │ ├── SampleMflixApplication.java +│ │ │ ├── config/ +│ │ │ │ ├── MongoConfig.java +│ │ │ │ ├── CorsConfig.java +│ │ │ │ └── DatabaseVerification.java +│ │ │ ├── controller/ +│ │ │ │ └── MovieController.java +│ │ │ ├── service/ +│ │ │ │ └── MovieService.java +│ │ │ ├── repository/ +│ │ │ │ └── MovieRepository.java +│ │ │ ├── model/ +│ │ │ │ ├── Movie.java +│ │ │ │ ├── Theater.java +│ │ │ │ ├── Comment.java +│ │ │ │ ├── dto/ +│ │ │ │ │ ├── CreateMovieRequest.java +│ │ │ │ │ ├── UpdateMovieRequest.java +│ │ │ │ │ └── MovieSearchQuery.java +│ │ │ │ └── response/ +│ │ │ │ ├── ApiResponse.java +│ │ │ │ ├── SuccessResponse.java +│ │ │ │ └── ErrorResponse.java +│ │ │ ├── exception/ +│ │ │ │ ├── GlobalExceptionHandler.java +│ │ │ │ ├── ValidationException.java +│ │ │ │ └── ResourceNotFoundException.java +│ │ │ └── util/ +│ │ │ └── ValidationUtils.java +│ │ └── resources/ +│ │ ├── application.properties +│ │ └── application-dev.properties +│ └── test/ +│ └── java/ +│ └── com/ +│ └── mongodb/ +│ └── samplemflix/ +│ ├── controller/ +│ │ └── MovieControllerTest.java +│ ├── service/ +│ │ └── MovieServiceTest.java +│ └── integration/ +│ └── MovieIntegrationTest.java +├── pom.xml (or build.gradle) +├── README.md +└── .env.example +``` + +--- + +## 3. Implementation Phases + +### Phase 1: Project Setup and Configuration (Days 1-2) + +#### 1.1 Initialize Spring Boot Project +- [ ] Create Spring Boot project using Spring Initializr +- [ ] Configure Maven/Gradle with required dependencies +- [ ] Set up project structure following Spring conventions +- [ ] Configure `.gitignore` for Java/Spring projects + +#### 1.2 Dependencies Configuration + +**Required Dependencies:** +```xml + + + org.springframework.boot + spring-boot-starter-web + + + + + org.mongodb + mongodb-driver-sync + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + +``` + +#### 1.3 Environment Configuration +- [ ] Create `application.properties` with MongoDB connection settings +- [ ] Create `.env.example` file documenting required environment variables +- [ ] Implement environment variable loading (Spring Boot handles this natively) +- [ ] Configure CORS settings + +**application.properties:** +```properties +# MongoDB Configuration +spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.database=sample_mflix + +# Server Configuration +server.port=${PORT:3001} + +# CORS Configuration +cors.allowed.origins=${CORS_ORIGIN:http://localhost:3000} + +# Application Info +spring.application.name=MongoDB Sample MFlix API +``` + +--- + +### Phase 2: Database Configuration and Connection (Days 2-3) ✅ COMPLETE + +#### 2.1 MongoDB Configuration Class ✅ +- [x] Create `MongoConfig.java` to configure MongoDB client +- [x] Implement connection pooling and timeout settings +- [x] Add connection lifecycle management + +**Key Features:** +- ✅ Singleton MongoClient instance +- ✅ Connection string validation +- ✅ Database reference management +- ✅ Graceful shutdown handling (managed by Spring) +- ✅ Connection pool settings (max 100, min 10 connections) +- ✅ Socket timeouts (10s connect, 10s read) +- ✅ Server selection timeout (10s) + +#### 2.2 Database Verification Service ✅ +- [x] Create `DatabaseVerification.java` as a Spring component +- [x] Implement `@PostConstruct` method for startup verification +- [x] Check for movies collection and document count +- [x] Create text search indexes if missing +- [x] Log verification results + +**Verification Checklist:** +- ✅ Movies collection exists +- ✅ Movies collection has documents (with warning if empty) +- ✅ Text search index on plot, title, fullplot fields +- ✅ Log warnings if data is missing +- ✅ Non-blocking verification (app starts even if verification fails) + +--- + +### Phase 3: Model Layer Implementation (Days 3-4) + +#### 3.1 Domain Models +- [ ] Create `Movie.java` entity class +- [ ] Create `Theater.java` entity class +- [ ] Create `Comment.java` entity class +- [ ] Add BSON annotations for MongoDB mapping +- [ ] Implement nested objects (Awards, IMDB, Tomatoes) + +**Movie.java Structure:** +```java +@Document(collection = "movies") +public class Movie { + @Id + private ObjectId id; + + private String title; + private Integer year; + private String plot; + private String fullplot; + private List genres; + private List directors; + private List writers; + private List cast; + private List countries; + private List languages; + private String rated; + private Integer runtime; + private String poster; + + private Awards awards; + private IMDB imdb; + private Tomatoes tomatoes; + private Integer metacritic; + private String type; + + // Nested classes for complex fields + public static class Awards { ... } + public static class IMDB { ... } + public static class Tomatoes { ... } +} +``` + +#### 3.2 DTOs (Data Transfer Objects) +- [ ] Create `CreateMovieRequest.java` +- [ ] Create `UpdateMovieRequest.java` +- [ ] Create `MovieSearchQuery.java` +- [ ] Add validation annotations (@NotNull, @NotBlank, etc.) + +#### 3.3 Response Models +- [ ] Create generic `ApiResponse` interface +- [ ] Create `SuccessResponse` class +- [ ] Create `ErrorResponse` class +- [ ] Add timestamp and metadata fields + +--- + +### Phase 4: Repository Layer (Days 4-5) + +#### 4.1 Custom Repository Implementation +- [ ] Create `MovieRepository.java` interface +- [ ] Implement custom repository using MongoTemplate or MongoCollection +- [ ] Add methods for all CRUD operations + +**Repository Methods:** +```java +public interface MovieRepository { + // Basic CRUD + Movie insertOne(Movie movie); + List insertMany(List movies); + Optional findById(ObjectId id); + List find(Document filter, Document sort, int skip, int limit); + UpdateResult updateOne(ObjectId id, Document update); + UpdateResult updateMany(Document filter, Document update); + DeleteResult deleteOne(ObjectId id); + DeleteResult deleteMany(Document filter); + Optional findOneAndDelete(ObjectId id); + + // Utility + long countDocuments(); +} +``` + +--- + +### Phase 5: Service Layer (Days 5-7) + +#### 5.1 Movie Service Implementation +- [ ] Create `MovieService.java` with business logic +- [ ] Implement all CRUD operations +- [ ] Add query building logic for filtering +- [ ] Implement pagination and sorting +- [ ] Add validation logic + +**Service Responsibilities:** +- Business logic and validation +- Query construction (filters, sorts, pagination) +- Data transformation between DTOs and entities +- Error handling and exception throwing + +--- + +### Phase 6: Controller Layer (Days 7-9) + +#### 6.1 REST API Endpoints +- [ ] Create `MovieController.java` with REST endpoints +- [ ] Implement all endpoints matching Express routes +- [ ] Add request validation +- [ ] Add API documentation annotations + +**Endpoint Mapping:** +``` +GET /api/movies -> getAllMovies() +GET /api/movies/{id} -> getMovieById() +POST /api/movies -> createMovie() +POST /api/movies/batch -> createMoviesBatch() +PUT /api/movies/{id} -> updateMovie() +PATCH /api/movies -> updateMoviesBatch() +DELETE /api/movies/{id} -> deleteMovie() +DELETE /api/movies -> deleteMoviesBatch() +DELETE /api/movies/{id}/find-and-delete -> findAndDeleteMovie() +``` + +#### 6.2 Request/Response Handling +- [ ] Implement consistent response wrapping +- [ ] Add query parameter parsing +- [ ] Implement request body validation +- [ ] Add proper HTTP status codes + +--- + +### Phase 7: Error Handling (Days 9-10) + +#### 7.1 Exception Hierarchy +- [ ] Create custom exception classes +- [ ] Implement `GlobalExceptionHandler` with @ControllerAdvice +- [ ] Handle MongoDB-specific exceptions +- [ ] Handle validation exceptions + +**Exception Types:** +- `ResourceNotFoundException` (404) +- `ValidationException` (400) +- `DuplicateKeyException` (409) +- `DatabaseException` (500) + +#### 7.2 Error Response Format +- [ ] Standardize error response structure +- [ ] Include error codes and messages +- [ ] Add timestamp and request path +- [ ] Log errors appropriately + +--- + +### Phase 8: Testing (Days 10-12) + +#### 8.1 Unit Tests +- [ ] Test MovieService methods +- [ ] Test MovieController endpoints +- [ ] Mock repository layer +- [ ] Achieve >80% code coverage + +#### 8.2 Integration Tests +- [ ] Test with embedded MongoDB or Testcontainers +- [ ] Test full request/response cycle +- [ ] Test error scenarios +- [ ] Test database operations + +#### 8.3 Test Configuration +- [ ] Create test application.properties +- [ ] Set up test fixtures and data +- [ ] Configure test MongoDB instance + +--- + +### Phase 9: Documentation and Polish (Days 12-13) + +#### 9.1 Code Documentation +- [ ] Add comprehensive JavaDoc comments +- [ ] Document MongoDB operations clearly +- [ ] Add inline comments explaining driver usage +- [ ] Note deviations from standard practices + +#### 9.2 API Documentation +- [ ] Configure Swagger/OpenAPI +- [ ] Add endpoint descriptions +- [ ] Document request/response schemas +- [ ] Add example requests + +#### 9.3 README and Setup Guide +- [ ] Create comprehensive README.md +- [ ] Document prerequisites (Java version, Maven/Gradle) +- [ ] Provide setup instructions +- [ ] Document environment variables +- [ ] Add troubleshooting section + +--- + +## 4. Technical Implementation Details + +### 4.1 MongoDB Driver Usage Patterns + +**Connection Management:** +```java +@Configuration +public class MongoConfig { + @Bean + public MongoClient mongoClient(@Value("${spring.data.mongodb.uri}") String uri) { + return MongoClients.create(uri); + } + + @Bean + public MongoDatabase mongoDatabase(MongoClient client) { + return client.getDatabase("sample_mflix"); + } +} +``` + +**Collection Access:** +```java +@Repository +public class MovieRepositoryImpl implements MovieRepository { + private final MongoCollection collection; + + public MovieRepositoryImpl(MongoDatabase database) { + this.collection = database.getCollection("movies"); + } +} +``` + +### 4.2 Query Building Examples + +**Text Search:** +```java +Document filter = new Document("$text", new Document("$search", searchQuery)); +``` + +**Range Queries:** +```java +Document ratingFilter = new Document() + .append("$gte", minRating) + .append("$lte", maxRating); +filter.append("imdb.rating", ratingFilter); +``` + +**Sorting:** +```java +Document sort = new Document(sortBy, sortOrder.equals("desc") ? -1 : 1); +``` + +### 4.3 Error Handling Pattern + +```java +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MongoException.class) + public ResponseEntity handleMongoException(MongoException ex) { + // Parse MongoDB error codes + // Return appropriate HTTP status and error response + } +} +``` + +--- + +## 5. Migration Mapping + +### 5.1 Express to Spring Equivalents + +| Express Concept | Spring Boot Equivalent | +|----------------|------------------------| +| `app.ts` | `SampleMflixApplication.java` (main class) | +| Routes (`movies.ts`) | `@RestController` in `MovieController.java` | +| Controllers | `@Service` in `MovieService.java` | +| `database.ts` | `MongoConfig.java` + `DatabaseVerification.java` | +| `errorHandler.ts` | `GlobalExceptionHandler.java` | +| Types/Interfaces | Java classes with proper annotations | +| `asyncHandler` | Spring handles async automatically | +| `dotenv` | `application.properties` + environment variables | + +### 5.2 Code Pattern Translations + +**Express Pattern:** +```typescript +router.get("/", asyncHandler(movieController.getAllMovies)); +``` + +**Spring Pattern:** +```java +@GetMapping +public ResponseEntity>> getAllMovies( + @RequestParam(required = false) String q, + // ... other params +) { + // Implementation +} +``` + +--- + +## 6. Quality Assurance Checklist + +### 6.1 Functional Requirements +- [ ] All CRUD operations work correctly +- [ ] Text search functionality works +- [ ] Filtering and pagination work +- [ ] Sorting works correctly +- [ ] Batch operations work +- [ ] Error handling is comprehensive + +### 6.2 Code Quality +- [ ] Code follows Spring Boot best practices +- [ ] Comprehensive comments explaining MongoDB operations +- [ ] Proper separation of concerns (Controller/Service/Repository) +- [ ] No hardcoded values +- [ ] Proper exception handling + +### 6.3 Testing +- [ ] Unit tests pass +- [ ] Integration tests pass +- [ ] Code coverage >80% +- [ ] Edge cases tested + +### 6.4 Documentation +- [ ] README is comprehensive +- [ ] API documentation is complete +- [ ] Code comments are thorough +- [ ] Setup instructions are clear + +--- + +## 7. Timeline and Milestones + +| Phase | Duration | Deliverable | +|-------|----------|-------------| +| Phase 1: Setup | 2 days | Project structure, dependencies configured | +| Phase 2: Database | 1 day | MongoDB connection and verification working | +| Phase 3: Models | 1 day | All model classes implemented | +| Phase 4: Repository | 1 day | Repository layer complete | +| Phase 5: Service | 2 days | Business logic implemented | +| Phase 6: Controller | 2 days | All REST endpoints working | +| Phase 7: Error Handling | 1 day | Comprehensive error handling | +| Phase 8: Testing | 2 days | Tests written and passing | +| Phase 9: Documentation | 1 day | Documentation complete | +| **Total** | **13 days** | **Production-ready Java Spring backend** | + +--- + +## 8. Post-Implementation Tasks + +### 8.1 Integration with Frontend +- [ ] Test with existing Next.js frontend +- [ ] Verify CORS configuration +- [ ] Test all API endpoints from frontend +- [ ] Verify response format compatibility + +### 8.2 Performance Optimization +- [ ] Add connection pooling configuration +- [ ] Optimize query performance +- [ ] Add caching if needed +- [ ] Profile and optimize hot paths + +### 8.3 Deployment Preparation +- [ ] Create Docker configuration +- [ ] Document deployment process +- [ ] Create production configuration +- [ ] Add health check endpoints + +--- + +## 9. Success Criteria + +The Java Spring implementation will be considered complete when: + +1. ✅ All CRUD operations match Express functionality +2. ✅ All tests pass with >80% coverage +3. ✅ API responses match Express format exactly +4. ✅ Frontend works without modifications +5. ✅ Documentation is comprehensive +6. ✅ Code follows Spring Boot best practices +7. ✅ MongoDB operations are well-documented +8. ✅ Error handling is robust +9. ✅ Pre-flight verification works +10. ✅ README provides clear setup instructions + +--- + +## 10. Notes and Considerations + +### 10.1 Design Decisions + +**Why not Spring Data MongoDB?** +- The scope requires direct MongoDB Driver usage to demonstrate driver operations +- Spring Data MongoDB abstracts away driver details +- Educational value is in showing raw driver usage + +**Lombok Usage:** +- Optional but recommended for reducing boilerplate +- Helps maintain readability +- Can be removed if team prefers explicit code + +**Testing Strategy:** +- Unit tests for service layer +- Integration tests for full stack +- Consider Testcontainers for realistic MongoDB testing + +### 10.2 Future Enhancements (Post-MVP) + +- [ ] Aggregation pipeline endpoints +- [ ] Full-text search with Atlas Search +- [ ] Vector search implementation +- [ ] Geospatial queries for theaters +- [ ] Caching layer +- [ ] Rate limiting +- [ ] Metrics and monitoring + +--- + +## Appendix A: Key Files Reference + +### Express Backend Files to Reference +- `server/express/src/app.ts` - Main application setup +- `server/express/src/config/database.ts` - Database configuration +- `server/express/src/controllers/movieController.ts` - Business logic +- `server/express/src/routes/movies.ts` - Route definitions +- `server/express/src/types/index.ts` - Type definitions +- `server/express/src/utils/errorHandler.ts` - Error handling + +### Python Backend Files (for comparison) +- `server/python/main.py` - FastAPI application +- `server/python/src/routers/movies.py` - Route handlers +- `server/python/src/database/mongo_client.py` - Database connection + +--- + +## Appendix B: Environment Variables + +Required environment variables for Java Spring backend: + +```properties +# MongoDB Connection +MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority + +# Server Configuration +PORT=3001 + +# CORS Configuration +CORS_ORIGIN=http://localhost:3000 +``` + +--- + +**Document Version:** 1.0 +**Last Updated:** 2025-10-23 +**Author:** Implementation Plan Generator +**Status:** Ready for Implementation diff --git a/client/next-env.d.ts b/client/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/client/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..498a11c --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,5007 @@ +{ + "name": "sample-mflix-front-end", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample-mflix-front-end", + "version": "0.1.0", + "dependencies": { + "next": "15.5.5", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.5", + "typescript": "^5" + } + }, + "node_modules/@emnapi/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", + "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz", + "integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.5.tgz", + "integrity": "sha512-FMzm412l9oFB8zdRD+K6HQ1VzlS+sNNsdg0MfvTg0i8lfCyTgP/RFxiu/pGJqZ/IQnzn9xSiLkjOVI7Iv4nbdQ==", + "dev": true, + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz", + "integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz", + "integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz", + "integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz", + "integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz", + "integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz", + "integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz", + "integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz", + "integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz", + "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", + "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.5.tgz", + "integrity": "sha512-f8lRSSelp6cqrYjxEMjJ5En3WV913gTu/w9goYShnIujwDSQlKt4x9MwSDiduE9R5mmFETK44+qlQDxeSA0rUA==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "15.5.5", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz", + "integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==", + "dependencies": { + "@next/env": "15.5.5", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.5", + "@next/swc-darwin-x64": "15.5.5", + "@next/swc-linux-arm64-gnu": "15.5.5", + "@next/swc-linux-arm64-musl": "15.5.5", + "@next/swc-linux-x64-gnu": "15.5.5", + "@next/swc-linux-x64-musl": "15.5.5", + "@next/swc-win32-arm64-msvc": "15.5.5", + "@next/swc-win32-x64-msvc": "15.5.5", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/server/express/package-lock.json b/server/express/package-lock.json new file mode 100644 index 0000000..c9b2554 --- /dev/null +++ b/server/express/package-lock.json @@ -0,0 +1,4748 @@ +{ + "name": "sample-mflix-express-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample-mflix-express-backend", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "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", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", + "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz", + "integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/server/java-spring/.env.example b/server/java-spring/.env.example new file mode 100644 index 0000000..df7ce44 --- /dev/null +++ b/server/java-spring/.env.example @@ -0,0 +1,13 @@ +# MongoDB Connection +# Replace with your MongoDB Atlas connection string or local MongoDB URI +MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority + +# Server Configuration +# Port on which the Spring Boot application will run +PORT=3001 + +# CORS Configuration +# Allowed origin for cross-origin requests (frontend URL) +# For multiple origins, separate with commas +CORS_ORIGIN=http://localhost:3000 + diff --git a/server/java-spring/.gitignore b/server/java-spring/.gitignore new file mode 100644 index 0000000..4a4b992 --- /dev/null +++ b/server/java-spring/.gitignore @@ -0,0 +1,66 @@ +# Java/Spring Boot - Specific Ignores +# Common patterns (.env, .DS_Store, .idea/, .vscode/, logs) in root .gitignore + +# Compiled Files +*.class +*.ctxt +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear + +# Crash Logs +hs_err_pid* +replay_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +# IntelliJ IDEA +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +# Eclipse +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +# NetBeans +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +# Spring Boot +spring-boot-devtools.properties diff --git a/server/java-spring/.mvn/wrapper/maven-wrapper.properties b/server/java-spring/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7fb0190 --- /dev/null +++ b/server/java-spring/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip diff --git a/server/java-spring/README.md b/server/java-spring/README.md new file mode 100644 index 0000000..20c2f87 --- /dev/null +++ b/server/java-spring/README.md @@ -0,0 +1,200 @@ +# MongoDB Sample MFlix - Java Spring Boot Backend [DRAFT] + +A Spring Boot REST API demonstrating MongoDB CRUD operations using the MongoDB Java Driver with the sample_mflix database. + +## Overview + +This application provides a REST API for managing movie data from MongoDB's sample_mflix database. It demonstrates: + +- Direct usage of the MongoDB Java Driver (not Spring Data MongoDB) +- CRUD operations (Create, Read, Update, Delete) +- Text search functionality +- Filtering, sorting, and pagination +- Comprehensive error handling +- API documentation with Swagger/OpenAPI + +## Prerequisites + +- Java 17 or later +- Maven 3.6 or later +- MongoDB Atlas account or local MongoDB instance with sample_mflix database + +## Project Structure + +``` +server/java-spring/ +├── src/ +│ ├── main/ +│ │ ├── java/com/mongodb/samplemflix/ +│ │ │ ├── SampleMflixApplication.java # Main application class +│ │ │ ├── config/ # Configuration classes +│ │ │ │ ├── MongoConfig.java # MongoDB client configuration +│ │ │ │ ├── CorsConfig.java # CORS configuration +│ │ │ │ └── DatabaseVerification.java # Startup database verification +│ │ │ ├── controller/ # REST controllers +│ │ │ ├── service/ # Business logic layer +│ │ │ ├── repository/ # Data access layer +│ │ │ ├── model/ # Domain models and DTOs +│ │ │ ├── exception/ # Custom exceptions +│ │ │ └── util/ # Utility classes +│ │ └── resources/ +│ │ └── application.properties # Application configuration +│ └── test/ # Test classes +├── pom.xml # Maven dependencies +└── README.md +``` + +## Setup Instructions + +### 1. Clone the Repository + +```bash +git clone +cd server/java-spring +``` + +### 2. Configure Environment Variables + +Copy the example environment file and update with your MongoDB connection details: + +```bash +cp .env.example .env +``` + +Edit `.env` and set your MongoDB connection string: + +```properties +MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority +PORT=3001 +CORS_ORIGIN=http://localhost:3000 +``` + +### 3. Load Sample Data + +If you haven't already, load the `sample_mflix` database into your MongoDB instance: + +- **MongoDB Atlas**: Use the "Load Sample Dataset" option in your cluster +- **Local MongoDB**: Follow the [MongoDB sample data documentation](https://www.mongodb.com/docs/atlas/sample-data/) + +### 4. Build the Project + +```bash +mvn clean install +``` + +### 5. Run the Application + +```bash +mvn spring-boot:run +``` + +The application will start on `http://localhost:3001` (or the port specified in your `.env` file). + +## API Documentation + +Once the application is running, you can access: + +- **Swagger UI**: http://localhost:3001/swagger-ui.html +- **OpenAPI JSON**: http://localhost:3001/api-docs + +## API Endpoints + +### Movies + +- `GET /api/movies` - Get all movies (with filtering, sorting, pagination) +- `GET /api/movies/{id}` - Get a single movie by ID +- `POST /api/movies` - Create a new movie +- `POST /api/movies/batch` - Create multiple movies +- `PUT /api/movies/{id}` - Update a movie +- `PATCH /api/movies` - Update multiple movies +- `DELETE /api/movies/{id}` - Delete a movie +- `DELETE /api/movies` - Delete multiple movies +- `DELETE /api/movies/{id}/find-and-delete` - Find and delete a movie + +> **Note**: Full endpoint implementation is planned for later phases. See the implementation plan for details. + +## Development + +### Running Tests + +```bash +# Run all tests +mvn test + +# Run tests with coverage +mvn test jacoco:report +``` + +### Building for Production + +```bash +mvn clean package +java -jar target/sample-mflix-spring-1.0.0.jar +``` + +## Implementation Status + +This project is being implemented in phases: + +- ✅ **Phase 1**: Project Setup and Configuration (CURRENT) +- ⏳ **Phase 2**: Database Configuration and Connection +- ⏳ **Phase 3**: Model Layer Implementation +- ⏳ **Phase 4**: Repository Layer +- ⏳ **Phase 5**: Service Layer +- ⏳ **Phase 6**: Controller Layer +- ⏳ **Phase 7**: Error Handling +- ⏳ **Phase 8**: Testing +- ⏳ **Phase 9**: Documentation and Polish + +See `JAVA-SPRING-IMPLEMENTATION-PLAN.md` in the repository root for the complete implementation plan. + +## Technology Stack + +- **Framework**: Spring Boot 3.2.0 +- **Java Version**: 17 +- **MongoDB Driver**: MongoDB Java Driver 5.1.4 (Sync) +- **Build Tool**: Maven +- **API Documentation**: SpringDoc OpenAPI 2.3.0 +- **Testing**: JUnit 5, Mockito, Spring Boot Test + +## Educational Purpose + +This application is designed as an educational sample to demonstrate: + +1. How to use the MongoDB Java Driver directly (without Spring Data MongoDB) +2. Best practices for Spring Boot REST API development +3. Proper separation of concerns (Controller → Service → Repository) +4. MongoDB CRUD operations and query patterns +5. Error handling and validation in Spring Boot + +## Troubleshooting + +### Connection Issues + +If you encounter connection issues: + +1. Verify your `MONGODB_URI` is correct +2. Check that your IP address is whitelisted in MongoDB Atlas +3. Ensure the sample_mflix database exists and contains data +4. Check the application logs for detailed error messages + +### Build Issues + +If Maven build fails: + +1. Ensure you have Java 17 or later installed: `java -version` +2. Ensure Maven is installed: `mvn -version` +3. Clear Maven cache: `mvn clean` +4. Try rebuilding: `mvn clean install` + +## License + +[TBD] + +## Contributing + +[TBD] + +## Issues + +[TBD] diff --git a/server/java-spring/mvnw b/server/java-spring/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/server/java-spring/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/server/java-spring/mvnw.cmd b/server/java-spring/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/server/java-spring/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml new file mode 100644 index 0000000..3dc71af --- /dev/null +++ b/server/java-spring/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + com.mongodb + sample-mflix-spring + 1.0.0 + MongoDB Sample MFlix Spring API + Spring Boot backend for MongoDB sample mflix application demonstrating CRUD operations using MongoDB Java Driver + + + 17 + 5.6.1 + 2.8.13 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.mongodb + mongodb-driver-sync + ${mongodb.driver.version} + + + + + org.projectlombok + lombok + true + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java new file mode 100644 index 0000000..52f367b --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -0,0 +1,23 @@ +package com.mongodb.samplemflix; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Main Spring Boot application class for the MongoDB Sample MFlix API. + * + * This application demonstrates MongoDB CRUD operations using the MongoDB Java Driver + * in a Spring Boot environment. It provides a REST API for managing movie data from + * the sample_mflix database. + * + * @author MongoDB Documentation Team + * @version 1.0 + */ +@SpringBootApplication +public class SampleMflixApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleMflixApplication.class, args); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java new file mode 100644 index 0000000..f1a9ae6 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java @@ -0,0 +1,53 @@ +package com.mongodb.samplemflix.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +/** + * CORS (Cross-Origin Resource Sharing) configuration for the Sample MFlix API. + * + * This configuration allows the frontend application (typically running on a different port + * during development) to make requests to this backend API. + * + * The allowed origins are configured via the CORS_ORIGIN environment variable. + */ +@Configuration +public class CorsConfig { + + @Value("${cors.allowed.origins}") + private String allowedOrigins; + + /** + * Configures CORS filter to allow cross-origin requests from the frontend. + * + * @return configured CorsFilter + */ + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + + // Allow credentials (cookies, authorization headers) + config.setAllowCredentials(true); + + // Set allowed origins from environment variable + config.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); + + // Allow all headers + config.addAllowedHeader("*"); + + // Allow all HTTP methods + config.addAllowedMethod("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java new file mode 100644 index 0000000..38cb728 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java @@ -0,0 +1,145 @@ +package com.mongodb.samplemflix.config; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +/** + * Database verification component that runs on application startup. + * + * This component performs pre-flight checks to ensure the MongoDB database + * is properly configured and contains the expected data and indexes. + * + * Verification steps: + * 1. Check if the movies collection exists + * 2. Verify the collection contains documents + * 3. Check for text search indexes on plot, title, and fullplot fields + * 4. Create text search index if missing + * + * This matches the behavior of the Express.js backend's verifyRequirements() function. + * The verification is non-blocking - the application will start even if verification fails, + * but warnings will be logged to help developers identify configuration issues. + */ +@Component +public class DatabaseVerification { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseVerification.class); + + private static final String MOVIES_COLLECTION = "movies"; + private static final String TEXT_INDEX_NAME = "text_search_index"; + + private final MongoDatabase database; + + public DatabaseVerification(MongoDatabase database) { + this.database = database; + } + + /** + * Runs database verification checks after the bean is constructed. + * + * This method is called automatically by Spring after dependency injection + * is complete. It performs all verification steps and logs the results. + * + * The method catches all exceptions to prevent application startup failure, + * but logs errors to help developers identify issues. + */ + @PostConstruct + public void verifyDatabase() { + logger.info("Starting database verification for '{}'...", database.getName()); + + try { + // Verify movies collection exists and has data + verifyMoviesCollection(); + + logger.info("Database verification completed successfully"); + + } catch (Exception e) { + logger.error("Database verification failed: {}", e.getMessage(), e); + // Don't throw exception - allow application to start even if verification fails + // This allows developers to troubleshoot connection issues without preventing startup + } + } + + /** + * Verifies the movies collection exists, contains data, and has required indexes. + * + * This method: + * 1. Checks if the movies collection exists (implicitly by accessing it) + * 2. Counts documents to verify sample data is loaded + * 3. Creates a text search index on plot, title, and fullplot fields + * + * The text search index enables full-text search functionality across movie + * descriptions and titles, which is used by the search endpoint. + */ + private void verifyMoviesCollection() { + MongoCollection moviesCollection = database.getCollection(MOVIES_COLLECTION); + + // Check if collection has documents + // Using estimatedDocumentCount() for better performance (doesn't scan all documents) + long count = moviesCollection.estimatedDocumentCount(); + + logger.info("Movies collection found with {} documents", count); + + if (count == 0) { + logger.warn( + "Movies collection is empty. Please ensure sample_mflix data is loaded. " + + "Visit https://www.mongodb.com/docs/atlas/sample-data/ for instructions." + ); + } + + // Create text search index for full-text search functionality + createTextSearchIndex(moviesCollection); + } + + /** + * Creates a text search index on the movies collection if it doesn't already exist. + * + * The index is created on three fields: + * - plot: Short movie description + * - title: Movie title + * - fullplot: Full movie description + * + * This enables the $text search operator to perform full-text search across + * these fields, which is used by the search endpoint in the API. + * + * The index is created in the background to avoid blocking other operations. + * If the index already exists, MongoDB will ignore the duplicate creation request. + * + * @param moviesCollection the movies collection to create the index on + */ + private void createTextSearchIndex(MongoCollection moviesCollection) { + try { + // Create compound text index on plot, title, and fullplot fields + // The background option allows the index to be built without blocking other operations + IndexOptions indexOptions = new IndexOptions() + .name(TEXT_INDEX_NAME) + .background(true); + + // Create the text index + // MongoDB will automatically ignore this if the index already exists + moviesCollection.createIndex( + Indexes.compoundIndex( + Indexes.text("plot"), + Indexes.text("title"), + Indexes.text("fullplot") + ), + indexOptions + ); + + logger.info("Text search index '{}' created/verified for movies collection", TEXT_INDEX_NAME); + + } catch (Exception e) { + // Log error but don't fail - the application can still function without the index + // (though text search queries will fail) + logger.error("Could not create text search index: {}", e.getMessage()); + logger.warn("Text search functionality may not work without the index"); + } + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java new file mode 100644 index 0000000..ee6a58e --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -0,0 +1,110 @@ +package com.mongodb.samplemflix.config; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +/** + * MongoDB configuration class for the Sample MFlix application. + * + * This class configures the MongoDB client connection using the MongoDB Java Driver. + * It creates singleton beans for MongoClient and MongoDatabase that will be injected + * throughout the application. + * + * Key features: + * - Connection pooling with configurable settings (max 100 connections, min 10) + * - Connection timeout configuration (10 seconds for connect and read) + * - Graceful shutdown handling (managed by Spring's bean lifecycle) + * - Connection string validation + * + * The MongoClient is thread-safe and designed to be shared across the application. + * Spring automatically manages the lifecycle and closes the client on shutdown. + */ +@Configuration +public class MongoConfig { + + @Value("${spring.data.mongodb.uri}") + private String mongoUri; + + @Value("${spring.data.mongodb.database}") + private String databaseName; + + /** + * Creates and configures the MongoDB client with connection pooling and timeout settings. + * + * Connection Pool Settings: + * - Max pool size: 100 connections (handles high concurrent load) + * - Min pool size: 10 connections (maintains ready connections) + * - Max connection idle time: 60 seconds (releases idle connections) + * - Max wait time: 10 seconds (time to wait for available connection) + * + * Socket Settings: + * - Connect timeout: 10 seconds (time to establish connection) + * - Read timeout: 10 seconds (time to wait for server response) + * + * The MongoClient is thread-safe and should be shared across the application. + * Spring will automatically close this client when the application shuts down. + * + * @return configured MongoClient instance + * @throws IllegalArgumentException if connection string is invalid + */ + @Bean + public MongoClient mongoClient() { + // Validate connection string is not empty + if (mongoUri == null || mongoUri.trim().isEmpty()) { + throw new IllegalArgumentException( + "MONGODB_URI is not configured. Please check application.properties" + ); + } + + // Parse and validate the connection string + ConnectionString connectionString = new ConnectionString(mongoUri); + + // Build client settings with connection pooling and timeouts + // These settings optimize for both performance and resource management + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(connectionString) + // Configure connection pool for optimal performance + .applyToConnectionPoolSettings(builder -> + builder.maxSize(100) // Maximum connections in pool + .minSize(10) // Minimum connections to maintain + .maxConnectionIdleTime(60000, TimeUnit.MILLISECONDS) // Release idle connections after 60s + .maxWaitTime(10000, TimeUnit.MILLISECONDS) // Wait up to 10s for available connection + ) + // Configure socket timeouts to prevent hanging connections + .applyToSocketSettings(builder -> + builder.connectTimeout(10000, TimeUnit.MILLISECONDS) // 10s to establish connection + .readTimeout(10000, TimeUnit.MILLISECONDS) // 10s to wait for server response + ) + // Configure server selection timeout + .applyToClusterSettings(builder -> + builder.serverSelectionTimeout(10000, TimeUnit.MILLISECONDS) // 10s to select server + ) + .build(); + + return MongoClients.create(settings); + } + + /** + * Creates a reference to the MongoDB database. + * + * This bean provides access to the sample_mflix database and can be injected + * into repositories and services throughout the application. + * + * The database name is configured in application.properties and defaults to "sample_mflix". + * + * @param mongoClient the MongoDB client (injected by Spring) + * @return MongoDatabase instance for the configured database + */ + @Bean + public MongoDatabase mongoDatabase(MongoClient mongoClient) { + return mongoClient.getDatabase(databaseName); + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java new file mode 100644 index 0000000..463e27c --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java @@ -0,0 +1,31 @@ +package com.mongodb.samplemflix.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for movie-related endpoints. + * + * This controller handles all HTTP requests for movie operations including: + * - GET /api/movies - Get all movies with filtering, sorting, and pagination + * - GET /api/movies/{id} - Get a single movie by ID + * - POST /api/movies - Create a new movie + * - POST /api/movies/batch - Create multiple movies + * - PUT /api/movies/{id} - Update a movie + * - PATCH /api/movies - Update multiple movies + * - DELETE /api/movies/{id} - Delete a movie + * - DELETE /api/movies - Delete multiple movies + * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie + * + * TODO: Phase 6 - Implement all REST endpoints + * TODO: Phase 6 - Add request validation + * TODO: Phase 6 - Add API documentation annotations + */ +@RestController +@RequestMapping("/api/movies") +public class MovieController { + + // TODO: Phase 6 - Inject MovieService + // TODO: Phase 6 - Implement endpoint methods +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4fbfaf8 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,26 @@ +package com.mongodb.samplemflix.exception; + +import org.springframework.web.bind.annotation.ControllerAdvice; + +/** + * Global exception handler for the application. + * + * This class uses @ControllerAdvice to handle exceptions thrown by controllers + * and convert them into appropriate HTTP responses. + * + * Exception types to handle: + * - ResourceNotFoundException (404) + * - ValidationException (400) + * - DuplicateKeyException (409) + * - MongoException (500) + * - General exceptions (500) + * + * TODO: Phase 7 - Implement exception handler methods + * TODO: Phase 7 - Add @ExceptionHandler methods for each exception type + */ +@ControllerAdvice +public class GlobalExceptionHandler { + + // TODO: Phase 7 - Implement exception handlers +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ResourceNotFoundException.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..ac2c81c --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ResourceNotFoundException.java @@ -0,0 +1,20 @@ +package com.mongodb.samplemflix.exception; + +/** + * Exception thrown when a requested resource is not found. + * + * This exception results in a 404 Not Found response. + * + * TODO: Phase 7 - Implement custom exception + */ +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ValidationException.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ValidationException.java new file mode 100644 index 0000000..84175df --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/ValidationException.java @@ -0,0 +1,20 @@ +package com.mongodb.samplemflix.exception; + +/** + * Exception thrown when request validation fails. + * + * This exception results in a 400 Bad Request response. + * + * TODO: Phase 7 - Implement custom exception + */ +public class ValidationException extends RuntimeException { + + public ValidationException(String message) { + super(message); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java new file mode 100644 index 0000000..52fc785 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -0,0 +1,38 @@ +package com.mongodb.samplemflix.model; + +/** + * Domain model representing a movie document from the MongoDB movies collection. + * + * This class maps to the movies collection in the sample_mflix database. + * It includes all fields from the movie documents including nested objects + * for awards, IMDB ratings, and Tomatoes ratings. + * + * TODO: Phase 3 - Implement full Movie class with all fields + * TODO: Phase 3 - Add BSON annotations for MongoDB mapping + * TODO: Phase 3 - Implement nested classes (Awards, IMDB, Tomatoes) + */ +public class Movie { + + // TODO: Phase 3 - Add fields and annotations + // Fields to include: + // - ObjectId id + // - String title + // - Integer year + // - String plot + // - String fullplot + // - List genres + // - List directors + // - List writers + // - List cast + // - List countries + // - List languages + // - String rated + // - Integer runtime + // - String poster + // - Awards awards + // - IMDB imdb + // - Tomatoes tomatoes + // - Integer metacritic + // - String type +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java new file mode 100644 index 0000000..383d212 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java @@ -0,0 +1,16 @@ +package com.mongodb.samplemflix.model.dto; + +/** + * Data Transfer Object for creating a new movie. + * + * This DTO is used for POST /api/movies requests. + * It includes validation annotations to ensure required fields are present. + * + * TODO: Phase 3 - Implement with validation annotations + * TODO: Phase 3 - Add @NotNull, @NotBlank annotations as needed + */ +public class CreateMovieRequest { + + // TODO: Phase 3 - Add fields and validation annotations +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java new file mode 100644 index 0000000..9551482 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -0,0 +1,15 @@ +package com.mongodb.samplemflix.model.dto; + +/** + * Data Transfer Object for updating an existing movie. + * + * This DTO is used for PUT /api/movies/{id} requests. + * All fields are optional since partial updates are allowed. + * + * TODO: Phase 3 - Implement with optional fields + */ +public class UpdateMovieRequest { + + // TODO: Phase 3 - Add fields (all optional) +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java new file mode 100644 index 0000000..60acdf5 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java @@ -0,0 +1,15 @@ +package com.mongodb.samplemflix.model.response; + +/** + * Generic API response interface. + * + * This interface is implemented by both SuccessResponse and ErrorResponse + * to provide a consistent response structure. + * + * TODO: Phase 3 - Define interface methods + */ +public interface ApiResponse { + + // TODO: Phase 3 - Add common response methods +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java new file mode 100644 index 0000000..e328a56 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java @@ -0,0 +1,15 @@ +package com.mongodb.samplemflix.model.response; + +/** + * Error response wrapper for API error responses. + * + * This class wraps error responses with error codes, messages, and metadata. + * + * TODO: Phase 3 - Implement error response structure + * TODO: Phase 3 - Add error code, message, timestamp, path fields + */ +public class ErrorResponse implements ApiResponse { + + // TODO: Phase 3 - Add fields: error, message, timestamp, path, etc. +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java new file mode 100644 index 0000000..6bb47de --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java @@ -0,0 +1,15 @@ +package com.mongodb.samplemflix.model.response; + +/** + * Success response wrapper for API responses. + * + * This class wraps successful API responses with metadata like timestamp. + * + * TODO: Phase 3 - Implement with generic type parameter + * TODO: Phase 3 - Add timestamp and metadata fields + */ +public class SuccessResponse implements ApiResponse { + + // TODO: Phase 3 - Add fields: data, timestamp, etc. +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java new file mode 100644 index 0000000..a4efb4e --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java @@ -0,0 +1,27 @@ +package com.mongodb.samplemflix.repository; + +/** + * Repository interface for movie data access. + * + * This repository provides methods for all CRUD operations using the MongoDB Java Driver. + * + * Methods to implement: + * - insertOne(Movie movie) + * - insertMany(List movies) + * - findById(ObjectId id) + * - find(Document filter, Document sort, int skip, int limit) + * - updateOne(ObjectId id, Document update) + * - updateMany(Document filter, Document update) + * - deleteOne(ObjectId id) + * - deleteMany(Document filter) + * - findOneAndDelete(ObjectId id) + * - countDocuments() + * + * TODO: Phase 4 - Define repository interface methods + * TODO: Phase 4 - Create implementation class using MongoCollection + */ +public interface MovieRepository { + + // TODO: Phase 4 - Add method signatures +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java new file mode 100644 index 0000000..529b20d --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -0,0 +1,25 @@ +package com.mongodb.samplemflix.service; + +import org.springframework.stereotype.Service; + +/** + * Service layer for movie business logic. + * + * This service handles: + * - Business logic and validation + * - Query construction (filters, sorts, pagination) + * - Data transformation between DTOs and entities + * - Error handling and exception throwing + * + * TODO: Phase 5 - Implement all CRUD operations + * TODO: Phase 5 - Add query building logic for filtering + * TODO: Phase 5 - Implement pagination and sorting + * TODO: Phase 5 - Add validation logic + */ +@Service +public class MovieService { + + // TODO: Phase 5 - Inject MovieRepository + // TODO: Phase 5 - Implement service methods +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java new file mode 100644 index 0000000..4c28104 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java @@ -0,0 +1,18 @@ +package com.mongodb.samplemflix.util; + +/** + * Utility class for validation operations. + * + * This class provides helper methods for validating request data. + * + * TODO: Phase 5 - Implement validation utility methods + */ +public class ValidationUtils { + + private ValidationUtils() { + // Private constructor to prevent instantiation + } + + // TODO: Phase 5 - Add validation methods +} + diff --git a/server/java-spring/src/main/resources/application.properties b/server/java-spring/src/main/resources/application.properties new file mode 100644 index 0000000..276a15b --- /dev/null +++ b/server/java-spring/src/main/resources/application.properties @@ -0,0 +1,29 @@ +# MongoDB Configuration +# Connection URI should be provided via MONGODB_URI environment variable +spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.database=sample_mflix + +# Server Configuration +# Default port is 3001, can be overridden via PORT environment variable +server.port=${PORT:3001} + +# CORS Configuration +# Allowed origins for cross-origin requests (typically the frontend URL) +cors.allowed.origins=${CORS_ORIGIN:http://localhost:3000} + +# Application Info +spring.application.name=MongoDB Sample MFlix API + +# Logging Configuration +logging.level.com.mongodb.samplemflix=INFO +logging.level.org.mongodb.driver=WARN + +# Jackson Configuration (JSON serialization) +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.write-dates-as-timestamps=false + +# API Documentation (Swagger/OpenAPI) +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.operationsSorter=method + diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java new file mode 100644 index 0000000..f8c6da0 --- /dev/null +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -0,0 +1,14 @@ +package com.mongodb.samplemflix.controller; + +/** + * Unit tests for MovieController. + * + * TODO: Phase 8 - Implement controller unit tests + * TODO: Phase 8 - Mock service layer + * TODO: Phase 8 - Test all endpoints + */ +public class MovieControllerTest { + + // TODO: Phase 8 - Add test methods +} + diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java new file mode 100644 index 0000000..92d6e2b --- /dev/null +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java @@ -0,0 +1,16 @@ +package com.mongodb.samplemflix.integration; + +/** + * Integration tests for the movie API. + * + * These tests verify the full request/response cycle including database operations. + * + * TODO: Phase 8 - Implement integration tests + * TODO: Phase 8 - Set up test MongoDB instance (Testcontainers or embedded) + * TODO: Phase 8 - Test full request/response cycle + */ +public class MovieIntegrationTest { + + // TODO: Phase 8 - Add integration test methods +} + diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java new file mode 100644 index 0000000..bf8dabe --- /dev/null +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -0,0 +1,14 @@ +package com.mongodb.samplemflix.service; + +/** + * Unit tests for MovieService. + * + * TODO: Phase 8 - Implement service unit tests + * TODO: Phase 8 - Mock repository layer + * TODO: Phase 8 - Test business logic + */ +public class MovieServiceTest { + + // TODO: Phase 8 - Add test methods +} + From f55b1d6954425b3563a57399eaa674ccb05594a7 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 14:41:35 -0400 Subject: [PATCH 010/110] feat: add model layer --- JAVA-SPRING-IMPLEMENTATION-PLAN.md | 122 ++++---- PR_DESCRIPTION.md | 274 +++++++++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 10 + server/java-spring/pom.xml | 13 +- .../samplemflix/config/MongoConfig.java | 10 +- .../mongodb/samplemflix/model/Comment.java | 58 ++++ .../com/mongodb/samplemflix/model/Movie.java | 288 ++++++++++++++++-- .../mongodb/samplemflix/model/Theater.java | 109 +++++++ .../model/dto/CreateMovieRequest.java | 90 +++++- .../model/dto/MovieSearchQuery.java | 70 +++++ .../model/dto/UpdateMovieRequest.java | 86 +++++- .../model/response/ApiResponse.java | 29 +- .../model/response/ErrorResponse.java | 79 ++++- .../model/response/SuccessResponse.java | 89 +++++- 14 files changed, 1204 insertions(+), 123 deletions(-) create mode 100644 PR_DESCRIPTION.md create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java diff --git a/JAVA-SPRING-IMPLEMENTATION-PLAN.md b/JAVA-SPRING-IMPLEMENTATION-PLAN.md index e761fe8..206e08e 100644 --- a/JAVA-SPRING-IMPLEMENTATION-PLAN.md +++ b/JAVA-SPRING-IMPLEMENTATION-PLAN.md @@ -208,60 +208,74 @@ spring.application.name=MongoDB Sample MFlix API --- -### Phase 3: Model Layer Implementation (Days 3-4) - -#### 3.1 Domain Models -- [ ] Create `Movie.java` entity class -- [ ] Create `Theater.java` entity class -- [ ] Create `Comment.java` entity class -- [ ] Add BSON annotations for MongoDB mapping -- [ ] Implement nested objects (Awards, IMDB, Tomatoes) - -**Movie.java Structure:** -```java -@Document(collection = "movies") -public class Movie { - @Id - private ObjectId id; - - private String title; - private Integer year; - private String plot; - private String fullplot; - private List genres; - private List directors; - private List writers; - private List cast; - private List countries; - private List languages; - private String rated; - private Integer runtime; - private String poster; - - private Awards awards; - private IMDB imdb; - private Tomatoes tomatoes; - private Integer metacritic; - private String type; - - // Nested classes for complex fields - public static class Awards { ... } - public static class IMDB { ... } - public static class Tomatoes { ... } -} -``` - -#### 3.2 DTOs (Data Transfer Objects) -- [ ] Create `CreateMovieRequest.java` -- [ ] Create `UpdateMovieRequest.java` -- [ ] Create `MovieSearchQuery.java` -- [ ] Add validation annotations (@NotNull, @NotBlank, etc.) - -#### 3.3 Response Models -- [ ] Create generic `ApiResponse` interface -- [ ] Create `SuccessResponse` class -- [ ] Create `ErrorResponse` class -- [ ] Add timestamp and metadata fields +### Phase 3: Model Layer Implementation (Days 3-4) ✅ COMPLETE + +#### 3.1 Domain Models ✅ +- [x] Create `Movie.java` entity class +- [x] Create `Theater.java` entity class +- [x] Create `Comment.java` entity class +- [x] Add BSON annotations for MongoDB mapping (using Lombok) +- [x] Implement nested objects (Awards, IMDB, Tomatoes) + +**Movie.java Structure:** ✅ +- ✅ All fields from TypeScript Movie interface +- ✅ Nested class: Awards (wins, nominations, text) +- ✅ Nested class: Imdb (rating, votes, id) +- ✅ Nested class: Tomatoes (viewer, critic, fresh, rotten, production, lastUpdated) +- ✅ Nested classes: Tomatoes.Viewer and Tomatoes.Critic +- ✅ Lombok annotations (@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor) +- ✅ Comprehensive JavaDoc documentation + +**Theater.java Structure:** ✅ +- ✅ All fields from TypeScript Theater interface +- ✅ Nested class: Location (address, geo) +- ✅ Nested class: Location.Address (street1, city, state, zipcode) +- ✅ Nested class: Location.Geo (type, coordinates) +- ✅ GeoJSON format support for geospatial queries + +**Comment.java Structure:** ✅ +- ✅ All fields from TypeScript Comment interface +- ✅ Fields: id, name, email, movieId, text, date + +#### 3.2 DTOs (Data Transfer Objects) ✅ +- [x] Create `CreateMovieRequest.java` +- [x] Create `UpdateMovieRequest.java` +- [x] Create `MovieSearchQuery.java` +- [x] Add validation annotations (@NotBlank for required fields) + +**CreateMovieRequest.java:** ✅ +- ✅ Required field: title (with @NotBlank validation) +- ✅ Optional fields: year, plot, fullplot, genres, directors, writers, cast, countries, languages, rated, runtime, poster + +**UpdateMovieRequest.java:** ✅ +- ✅ All fields optional (for partial updates) +- ✅ Same fields as CreateMovieRequest + +**MovieSearchQuery.java:** ✅ +- ✅ Full-text search: q +- ✅ Filters: genre, year, minRating, maxRating +- ✅ Pagination: limit, skip +- ✅ Sorting: sortBy, sortOrder + +#### 3.3 Response Models ✅ +- [x] Create generic `ApiResponse` interface +- [x] Create `SuccessResponse` class +- [x] Create `ErrorResponse` class +- [x] Add timestamp and metadata fields + +**ApiResponse Interface:** ✅ +- ✅ Methods: isSuccess(), getTimestamp() + +**SuccessResponse:** ✅ +- ✅ Generic type parameter for data +- ✅ Fields: success (true), message, data, timestamp, pagination +- ✅ Nested class: Pagination (page, limit, total, pages) +- ✅ @JsonInclude(NON_NULL) to exclude null fields + +**ErrorResponse:** ✅ +- ✅ Fields: success (false), message, error, timestamp +- ✅ Nested class: ErrorDetails (message, code, details) +- ✅ Matches Express backend error format --- diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md new file mode 100644 index 0000000..2d46f00 --- /dev/null +++ b/PR_DESCRIPTION.md @@ -0,0 +1,274 @@ +# Java Spring Backend Implementation - Phases 2 & 3 + +## 📋 Overview + +This PR implements **Phase 2 (Database Configuration)** and **Phase 3 (Model Layer)** of the Java Spring Boot backend for the MongoDB Sample MFlix application. This backend will provide feature parity with the existing Express.js/TypeScript backend while demonstrating direct MongoDB Java Driver usage. + +**Branch:** `java-scaffolding-setup` +**Base:** `main` + +--- + +## ✅ What's Completed + +### Phase 2: Database Configuration and Connection ✅ + +#### MongoDB Configuration (`MongoConfig.java`) +- ✅ **Connection pooling** with production-ready settings: + - Max pool size: 100 connections + - Min pool size: 10 connections + - Max connection idle time: 60 seconds + - Max wait time: 10 seconds +- ✅ **Socket timeouts** to prevent hanging connections: + - Connect timeout: 10 seconds + - Read timeout: 10 seconds +- ✅ **Server selection timeout**: 10 seconds +- ✅ **Connection string validation** before attempting connection +- ✅ **Graceful shutdown** managed by Spring's bean lifecycle +- ✅ Singleton `MongoClient` and `MongoDatabase` beans + +#### Database Verification (`DatabaseVerification.java`) +- ✅ **Startup verification** via `@PostConstruct` +- ✅ **Collection verification**: Checks if movies collection exists and contains data +- ✅ **Text search index creation**: Creates compound text index on `plot`, `title`, and `fullplot` fields +- ✅ **Background index creation** to avoid blocking operations +- ✅ **Non-blocking verification**: Application starts even if verification fails +- ✅ **Comprehensive logging** with helpful error messages and links to MongoDB documentation + +### Phase 3: Model Layer Implementation ✅ + +#### Domain Models (3 classes) + +**1. Movie.java** (270 lines) +- ✅ All fields from TypeScript `Movie` interface +- ✅ Nested classes: + - `Awards` (wins, nominations, text) + - `Imdb` (rating, votes, id) + - `Tomatoes` (viewer, critic, fresh, rotten, production, lastUpdated) + - `Tomatoes.Viewer` (rating, numReviews, meter) + - `Tomatoes.Critic` (rating, numReviews, meter) +- ✅ Lombok annotations for reduced boilerplate +- ✅ Comprehensive JavaDoc documentation + +**2. Theater.java** (103 lines) +- ✅ All fields from TypeScript `Theater` interface +- ✅ Nested classes: + - `Location` (address, geo) + - `Location.Address` (street1, city, state, zipcode) + - `Location.Geo` (type, coordinates in GeoJSON format) +- ✅ GeoJSON format support for geospatial queries + +**3. Comment.java** (56 lines) +- ✅ All fields from TypeScript `Comment` interface +- ✅ Fields: id, name, email, movieId, text, date + +#### DTOs - Data Transfer Objects (3 classes) + +**1. CreateMovieRequest.java** (92 lines) +- ✅ Required field: `title` with `@NotBlank` validation +- ✅ Optional fields: year, plot, fullplot, genres, directors, writers, cast, countries, languages, rated, runtime, poster +- ✅ Matches Express backend `CreateMovieRequest` interface + +**2. UpdateMovieRequest.java** (89 lines) +- ✅ All fields optional (for partial updates) +- ✅ Same fields as `CreateMovieRequest` + +**3. MovieSearchQuery.java** (64 lines) +- ✅ Full-text search: `q` parameter +- ✅ Filters: genre, year, minRating, maxRating +- ✅ Pagination: limit, skip +- ✅ Sorting: sortBy, sortOrder + +#### Response Models (3 classes + 1 interface) + +**1. ApiResponse Interface** (30 lines) +- ✅ Common interface for all API responses +- ✅ Methods: `isSuccess()`, `getTimestamp()` + +**2. SuccessResponse** (88 lines) +- ✅ Generic type parameter for flexible data responses +- ✅ Fields: success (true), message, data, timestamp, pagination +- ✅ Nested class: `Pagination` (page, limit, total, pages) +- ✅ `@JsonInclude(NON_NULL)` to exclude null fields from JSON +- ✅ Auto-generated timestamp using `Instant.now()` + +**3. ErrorResponse** (80 lines) +- ✅ Fields: success (false), message, error, timestamp +- ✅ Nested class: `ErrorDetails` (message, code, details) +- ✅ Matches Express backend error format + +--- + +## 📁 Files Changed + +### Modified Files (8) +- `server/java-spring/pom.xml` - Updated dependencies +- `server/java-spring/.mvn/wrapper/maven-wrapper.properties` - Maven wrapper config +- `server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java` - **Phase 2** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java` - **Phase 3** + +### New Files (4) +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java` - **Phase 3** +- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java` - **Phase 3** + +### Not Staged (Documentation) +- `JAVA-SPRING-IMPLEMENTATION-PLAN.md` - Updated with Phase 2 & 3 completion status + +--- + +## 🔍 What to Review + +### 1. Database Configuration (`config/`) +**Focus Areas:** +- [ ] Connection pool settings are appropriate for production use +- [ ] Timeout values (10s) are reasonable +- [ ] Error handling in `DatabaseVerification` is robust +- [ ] Text index creation matches Express backend behavior +- [ ] Logging messages are helpful and informative + +**Questions for Reviewers:** +- Are the connection pool settings (max 100, min 10) appropriate? +- Should we add more comprehensive index verification (check if index exists before creating)? + +### 2. Domain Models (`model/`) +**Focus Areas:** +- [ ] All fields match the TypeScript interfaces from Express backend +- [ ] Nested class structure is clean and maintainable +- [ ] Lombok annotations are used appropriately +- [ ] JavaDoc documentation is comprehensive +- [ ] Field types are correct (Integer vs int, Date vs Instant, etc.) + +**Questions for Reviewers:** +- Should we use `Instant` instead of `Date` for date fields? +- Are the nested classes appropriately structured, or should some be top-level classes? + +### 3. DTOs (`model/dto/`) +**Focus Areas:** +- [ ] Validation annotations are appropriate (`@NotBlank` on title) +- [ ] All optional fields are correctly marked +- [ ] DTOs match Express backend request structures +- [ ] Field naming conventions are consistent + +**Questions for Reviewers:** +- Should we add more validation annotations (e.g., `@Min`, `@Max` for year/rating)? +- Should `MovieSearchQuery` have default values for limit/skip? + +### 4. Response Models (`model/response/`) +**Focus Areas:** +- [ ] Generic type usage in `SuccessResponse` is correct +- [ ] `@JsonInclude(NON_NULL)` behavior is desired +- [ ] Timestamp format (ISO 8601) matches Express backend +- [ ] Error response structure matches Express backend +- [ ] Builder pattern with defaults works as expected + +**Questions for Reviewers:** +- Should we use a custom timestamp format instead of `Instant.now().toString()`? +- Is the `ErrorDetails` nested class structure appropriate? + +--- + +## 🚧 What's NOT in This PR (Pending Work) + +### Phase 4: Repository Layer (Next PR) +- [ ] `MovieRepository` interface +- [ ] `MovieRepositoryImpl` using `MongoCollection` directly +- [ ] Manual BSON Document ↔ Movie object conversion +- [ ] All CRUD operations: insertOne, insertMany, findById, find, updateOne, updateMany, deleteOne, deleteMany, findOneAndDelete + +### Phase 5: Service Layer (Future PR) +- [ ] `MovieService` with business logic +- [ ] Request validation +- [ ] DTO ↔ Domain model conversion +- [ ] Query building for search/filter operations + +### Phase 6: Controller Layer (Future PR) +- [ ] `MovieController` with REST endpoints +- [ ] Request/response mapping +- [ ] Exception handling + +### Phase 7-9: Testing, Error Handling, Documentation (Future PRs) +- [ ] Unit tests +- [ ] Integration tests +- [ ] Global exception handler +- [ ] API documentation (Swagger/OpenAPI) +- [ ] README updates + +--- + +## ✅ Build Verification + +```bash +$ cd server/java-spring && ./mvnw clean compile +[INFO] BUILD SUCCESS +[INFO] Compiling 20 source files +``` + +All files compile successfully with no errors or warnings. + +--- + +## 🎯 Design Decisions + +### Why Lombok? +- **Reduces boilerplate**: No need to write getters, setters, constructors, toString, equals, hashCode +- **Improves readability**: Focus on business logic, not boilerplate +- **Maintainability**: Changes to fields automatically update generated methods +- **Industry standard**: Widely used in Spring Boot projects + +### Why Custom Repository (Not Spring Data)? +- **Educational value**: Demonstrates direct MongoDB Driver usage +- **Matches Express backend**: Similar to how Node.js driver is used +- **Full control**: Complete control over BSON document structure and queries +- **Explicit operations**: Shows exactly what MongoDB operations are being performed + +### Why Nested Classes? +- **Encapsulation**: Nested classes are only used within their parent context +- **Namespace clarity**: `Movie.Awards` is clearer than a separate `MovieAwards` class +- **Matches MongoDB structure**: Reflects the nested document structure in MongoDB + +--- + +## 📚 References + +- **Implementation Plan**: `JAVA-SPRING-IMPLEMENTATION-PLAN.md` +- **Express Backend**: `server/express/src/` (reference implementation) +- **MongoDB Java Driver Docs**: https://www.mongodb.com/docs/drivers/java/sync/current/ +- **Spring Boot Docs**: https://docs.spring.io/spring-boot/docs/current/reference/html/ + +--- + +## 🤔 Questions for Discussion + +1. **Date Types**: Should we use `java.time.Instant` instead of `java.util.Date` for better Java 8+ compatibility? +2. **Validation**: Should we add more comprehensive validation annotations on DTOs? +3. **Index Management**: Should we check if indexes exist before creating them, or rely on MongoDB's idempotent behavior? +4. **Error Messages**: Are the error messages and logging levels appropriate? +5. **Pagination Defaults**: Should `MovieSearchQuery` have default values (e.g., limit=20, skip=0)? + +--- + +## 📝 Reviewer Checklist + +- [ ] Code compiles without errors +- [ ] All models match Express backend TypeScript interfaces +- [ ] Lombok annotations are used appropriately +- [ ] JavaDoc documentation is comprehensive +- [ ] Connection pool settings are production-ready +- [ ] Database verification logic is sound +- [ ] Response models match Express backend format +- [ ] Validation annotations are appropriate +- [ ] No security vulnerabilities introduced +- [ ] Code follows Java/Spring Boot best practices + +--- + +**Ready for Review** ✅ + +This PR lays the foundation for the Java Spring backend by implementing database configuration and all model classes. The next PR will implement the Repository layer with direct MongoDB Driver usage. + diff --git a/server/java-spring/.mvn/wrapper/maven-wrapper.properties b/server/java-spring/.mvn/wrapper/maven-wrapper.properties index 7fb0190..a57d0ee 100644 --- a/server/java-spring/.mvn/wrapper/maven-wrapper.properties +++ b/server/java-spring/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,13 @@ +# Maven Wrapper Configuration +# Use ./mvnw instead of mvn to automatically download and use the specified Maven version. + +# Maven Wrapper version (not the Maven version itself) wrapperVersion=3.3.4 + +# Download only Maven binaries (recommended for most projects) distributionType=only-script + +# Maven version to download - pins everyone to Maven 3.8.6 for consistent builds distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip + +# Usage: ./mvnw clean install (Unix/macOS) or mvnw.cmd clean install (Windows) diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index 3dc71af..e5dcafc 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -50,12 +50,23 @@ lombok true - + + + org.apache.commons + commons-lang3 + 3.19.0 + org.springdoc springdoc-openapi-starter-webmvc-ui ${springdoc.version} + + + org.apache.commons + commons-lang3 + + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index ee6a58e..e6d294a 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -38,17 +38,17 @@ public class MongoConfig { /** * Creates and configures the MongoDB client with connection pooling and timeout settings. - * + *

* Connection Pool Settings: * - Max pool size: 100 connections (handles high concurrent load) * - Min pool size: 10 connections (maintains ready connections) * - Max connection idle time: 60 seconds (releases idle connections) * - Max wait time: 10 seconds (time to wait for available connection) - * + *

* Socket Settings: * - Connect timeout: 10 seconds (time to establish connection) * - Read timeout: 10 seconds (time to wait for server response) - * + *

* The MongoClient is thread-safe and should be shared across the application. * Spring will automatically close this client when the application shuts down. * @@ -94,10 +94,10 @@ public MongoClient mongoClient() { /** * Creates a reference to the MongoDB database. - * + *

* This bean provides access to the sample_mflix database and can be injected * into repositories and services throughout the application. - * + *

* The database name is configured in application.properties and defaults to "sample_mflix". * * @param mongoClient the MongoDB client (injected by Spring) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java new file mode 100644 index 0000000..2702f6b --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java @@ -0,0 +1,58 @@ +package com.mongodb.samplemflix.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; + +import java.util.Date; + +/** + * Domain model representing a comment document from the MongoDB comments collection. + * + * This class maps to the comments collection in the sample_mflix database. + * Comments are user reviews/comments associated with movies. + * + * The structure matches the TypeScript Comment interface from the Express backend + * to ensure API compatibility. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Comment { + + /** + * MongoDB document ID. + * Maps to the _id field in MongoDB. + */ + private ObjectId id; + + /** + * Name of the commenter. + */ + private String name; + + /** + * Email address of the commenter. + */ + private String email; + + /** + * ID of the movie this comment is associated with. + * References a document in the movies collection. + */ + private ObjectId movieId; + + /** + * Comment text content. + */ + private String text; + + /** + * Date when the comment was posted. + */ + private Date date; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 52fc785..e0c6acb 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -1,38 +1,270 @@ package com.mongodb.samplemflix.model; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; + +import java.util.Date; +import java.util.List; + /** * Domain model representing a movie document from the MongoDB movies collection. - * + * * This class maps to the movies collection in the sample_mflix database. * It includes all fields from the movie documents including nested objects * for awards, IMDB ratings, and Tomatoes ratings. - * - * TODO: Phase 3 - Implement full Movie class with all fields - * TODO: Phase 3 - Add BSON annotations for MongoDB mapping - * TODO: Phase 3 - Implement nested classes (Awards, IMDB, Tomatoes) + * + * The structure matches the TypeScript Movie interface from the Express backend + * to ensure API compatibility. + * + * Note: We use Lombok annotations to reduce boilerplate code: + * - @Data: Generates getters, setters, toString, equals, and hashCode + * - @Builder: Provides a fluent builder pattern for object construction + * - @NoArgsConstructor: Generates a no-argument constructor (required by MongoDB driver) + * - @AllArgsConstructor: Generates a constructor with all fields */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Movie { - - // TODO: Phase 3 - Add fields and annotations - // Fields to include: - // - ObjectId id - // - String title - // - Integer year - // - String plot - // - String fullplot - // - List genres - // - List directors - // - List writers - // - List cast - // - List countries - // - List languages - // - String rated - // - Integer runtime - // - String poster - // - Awards awards - // - IMDB imdb - // - Tomatoes tomatoes - // - Integer metacritic - // - String type -} + /** + * MongoDB document ID. + * Maps to the _id field in MongoDB. + * Can be null for new documents (MongoDB will generate it). + */ + private ObjectId id; + + /** + * Movie title (required field). + */ + private String title; + + /** + * Release year. + */ + private Integer year; + + /** + * Short plot summary. + */ + private String plot; + + /** + * Full plot description. + */ + private String fullplot; + + /** + * Release date. + */ + private Date released; + + /** + * Runtime in minutes. + */ + private Integer runtime; + + /** + * Poster image URL. + */ + private String poster; + + /** + * List of genres (e.g., "Action", "Drama", "Comedy"). + */ + private List genres; + + /** + * List of directors. + */ + private List directors; + + /** + * List of writers. + */ + private List writers; + + /** + * List of cast members. + */ + private List cast; + + /** + * List of countries where the movie was produced. + */ + private List countries; + + /** + * List of languages in the movie. + */ + private List languages; + + /** + * Movie rating (e.g., "PG", "PG-13", "R"). + */ + private String rated; + + /** + * Awards information (wins, nominations, text). + */ + private Awards awards; + + /** + * IMDB rating information. + */ + private Imdb imdb; + + /** + * Rotten Tomatoes rating information. + */ + private Tomatoes tomatoes; + + /** + * Metacritic score. + */ + private Integer metacritic; + + /** + * Type of content (e.g., "movie", "series"). + */ + private String type; + + /** + * Nested class representing awards information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Awards { + /** + * Number of awards won. + */ + private Integer wins; + + /** + * Number of nominations. + */ + private Integer nominations; + + /** + * Text description of awards. + */ + private String text; + } + + /** + * Nested class representing IMDB rating information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Imdb { + /** + * IMDB rating (0.0 to 10.0). + */ + private Double rating; + + /** + * Number of votes. + */ + private Integer votes; + + /** + * IMDB ID number. + */ + private Integer id; + } + + /** + * Nested class representing Rotten Tomatoes rating information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Tomatoes { + /** + * Viewer ratings information. + */ + private Viewer viewer; + + /** + * Critic ratings information. + */ + private Critic critic; + + /** + * Number of fresh reviews. + */ + private Integer fresh; + + /** + * Number of rotten reviews. + */ + private Integer rotten; + + /** + * Production company. + */ + private String production; + + /** + * Last updated date. + */ + private Date lastUpdated; + + /** + * Nested class for viewer ratings. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Viewer { + /** + * Viewer rating (0.0 to 5.0). + */ + private Double rating; + + /** + * Number of viewer reviews. + */ + private Integer numReviews; + + /** + * Viewer meter percentage (0-100). + */ + private Integer meter; + } + + /** + * Nested class for critic ratings. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Critic { + /** + * Critic rating (0.0 to 5.0). + */ + private Double rating; + + /** + * Number of critic reviews. + */ + private Integer numReviews; + + /** + * Critic meter percentage (0-100). + */ + private Integer meter; + } + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java new file mode 100644 index 0000000..e636226 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java @@ -0,0 +1,109 @@ +package com.mongodb.samplemflix.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; + +/** + * Domain model representing a theater document from the MongoDB theaters collection. + * + * This class maps to the theaters collection in the sample_mflix database. + * It includes location information with address and geospatial coordinates. + * + * The structure matches the TypeScript Theater interface from the Express backend + * to ensure API compatibility. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Theater { + + /** + * MongoDB document ID. + * Maps to the _id field in MongoDB. + */ + private ObjectId id; + + /** + * Theater ID number. + */ + private Integer theaterId; + + /** + * Location information including address and geospatial coordinates. + */ + private Location location; + + /** + * Nested class representing location information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Location { + /** + * Address information. + */ + private Address address; + + /** + * Geospatial coordinates. + */ + private Geo geo; + + /** + * Nested class for address information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Address { + /** + * Street address line 1. + */ + private String street1; + + /** + * City name. + */ + private String city; + + /** + * State or province. + */ + private String state; + + /** + * ZIP or postal code. + */ + private String zipcode; + } + + /** + * Nested class for geospatial coordinates. + * Uses GeoJSON format for MongoDB geospatial queries. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Geo { + /** + * GeoJSON type (always "Point" for theater locations). + */ + private String type; + + /** + * Coordinates array: [longitude, latitude]. + * Note: GeoJSON uses longitude first, then latitude. + */ + private double[] coordinates; + } + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java index 383d212..179bb03 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java @@ -1,16 +1,92 @@ package com.mongodb.samplemflix.model.dto; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + /** * Data Transfer Object for creating a new movie. - * + * * This DTO is used for POST /api/movies requests. * It includes validation annotations to ensure required fields are present. - * - * TODO: Phase 3 - Implement with validation annotations - * TODO: Phase 3 - Add @NotNull, @NotBlank annotations as needed + * + * The structure matches the TypeScript CreateMovieRequest interface from the Express backend. + * Only the title field is required; all other fields are optional. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class CreateMovieRequest { - - // TODO: Phase 3 - Add fields and validation annotations -} + /** + * Movie title (required). + * Must not be blank. + */ + @NotBlank(message = "Title is required") + private String title; + + /** + * Release year (optional). + */ + private Integer year; + + /** + * Short plot summary (optional). + */ + private String plot; + + /** + * Full plot description (optional). + */ + private String fullplot; + + /** + * List of genres (optional). + */ + private List genres; + + /** + * List of directors (optional). + */ + private List directors; + + /** + * List of writers (optional). + */ + private List writers; + + /** + * List of cast members (optional). + */ + private List cast; + + /** + * List of countries (optional). + */ + private List countries; + + /** + * List of languages (optional). + */ + private List languages; + + /** + * Movie rating (optional). + */ + private String rated; + + /** + * Runtime in minutes (optional). + */ + private Integer runtime; + + /** + * Poster image URL (optional). + */ + private String poster; +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java new file mode 100644 index 0000000..b9ad823 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java @@ -0,0 +1,70 @@ +package com.mongodb.samplemflix.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Data Transfer Object for movie search query parameters. + * + * This DTO is used to parse and validate query parameters for GET /api/movies requests. + * It supports full-text search, filtering by genre/year/rating, sorting, and pagination. + * + * The structure matches the search functionality from the Express backend. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MovieSearchQuery { + + /** + * Full-text search query. + * Searches across plot, title, and fullplot fields using MongoDB text index. + */ + private String q; + + /** + * Filter by genre (case-insensitive partial match). + */ + private String genre; + + /** + * Filter by exact year. + */ + private Integer year; + + /** + * Minimum IMDB rating (inclusive). + */ + private Double minRating; + + /** + * Maximum IMDB rating (inclusive). + */ + private Double maxRating; + + /** + * Number of results to return (default: 20, max: 100). + */ + private Integer limit; + + /** + * Number of results to skip for pagination (default: 0). + */ + private Integer skip; + + /** + * Field to sort by (e.g., "title", "year", "imdb.rating"). + * Default: "title" + */ + private String sortBy; + + /** + * Sort order: "asc" or "desc". + * Default: "asc" + */ + private String sortOrder; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java index 9551482..50423cb 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -1,15 +1,89 @@ package com.mongodb.samplemflix.model.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + /** * Data Transfer Object for updating an existing movie. - * + * * This DTO is used for PUT /api/movies/{id} requests. * All fields are optional since partial updates are allowed. - * - * TODO: Phase 3 - Implement with optional fields + * + * The structure matches the TypeScript UpdateMovieRequest interface from the Express backend. + * Any field that is null will not be updated in the database. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class UpdateMovieRequest { - - // TODO: Phase 3 - Add fields (all optional) -} + /** + * Movie title (optional). + */ + private String title; + + /** + * Release year (optional). + */ + private Integer year; + + /** + * Short plot summary (optional). + */ + private String plot; + + /** + * Full plot description (optional). + */ + private String fullplot; + + /** + * List of genres (optional). + */ + private List genres; + + /** + * List of directors (optional). + */ + private List directors; + + /** + * List of writers (optional). + */ + private List writers; + + /** + * List of cast members (optional). + */ + private List cast; + + /** + * List of countries (optional). + */ + private List countries; + + /** + * List of languages (optional). + */ + private List languages; + + /** + * Movie rating (optional). + */ + private String rated; + + /** + * Runtime in minutes (optional). + */ + private Integer runtime; + + /** + * Poster image URL (optional). + */ + private String poster; +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java index 60acdf5..fb06da9 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java @@ -2,14 +2,29 @@ /** * Generic API response interface. - * + * * This interface is implemented by both SuccessResponse and ErrorResponse - * to provide a consistent response structure. - * - * TODO: Phase 3 - Define interface methods + * to provide a consistent response structure across all API endpoints. + * + * All API responses include: + * - success: boolean indicating if the request was successful + * - timestamp: ISO 8601 timestamp of when the response was generated + * + * This matches the TypeScript ApiResponse type from the Express backend. */ public interface ApiResponse { - - // TODO: Phase 3 - Add common response methods -} + /** + * Indicates whether the request was successful. + * + * @return true for successful responses, false for error responses + */ + boolean isSuccess(); + + /** + * Gets the timestamp when the response was generated. + * + * @return ISO 8601 formatted timestamp string + */ + String getTimestamp(); +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java index e328a56..f81ded1 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java @@ -1,15 +1,80 @@ package com.mongodb.samplemflix.model.response; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + /** * Error response wrapper for API error responses. - * + * * This class wraps error responses with error codes, messages, and metadata. - * - * TODO: Phase 3 - Implement error response structure - * TODO: Phase 3 - Add error code, message, timestamp, path fields + * + * The structure matches the TypeScript ErrorResponse type from the Express backend: + * { + * success: false, + * message: string, + * error: { + * message: string, + * code?: string, + * details?: any + * }, + * timestamp: string + * } */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class ErrorResponse implements ApiResponse { - - // TODO: Phase 3 - Add fields: error, message, timestamp, path, etc. -} + /** + * Always false for error responses. + */ + @Builder.Default + private boolean success = false; + + /** + * High-level error message. + */ + private String message; + + /** + * Detailed error information. + */ + private ErrorDetails error; + + /** + * ISO 8601 timestamp when the error occurred. + */ + @Builder.Default + private String timestamp = Instant.now().toString(); + + /** + * Nested class for detailed error information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ErrorDetails { + /** + * Detailed error message. + */ + private String message; + + /** + * Error code (e.g., "VALIDATION_ERROR", "NOT_FOUND"). + */ + private String code; + + /** + * Additional error details (optional). + */ + private Object details; + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java index 6bb47de..36c0e41 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java @@ -1,15 +1,88 @@ package com.mongodb.samplemflix.model.response; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + /** * Success response wrapper for API responses. - * - * This class wraps successful API responses with metadata like timestamp. - * - * TODO: Phase 3 - Implement with generic type parameter - * TODO: Phase 3 - Add timestamp and metadata fields + * + * This class wraps successful API responses with metadata like timestamp and pagination. + * It uses a generic type parameter T to hold the response data. + * + * The structure matches the TypeScript SuccessResponse type from the Express backend: + * { + * success: true, + * message?: string, + * data: T, + * timestamp: string, + * pagination?: { page, limit, total, pages } + * } */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class SuccessResponse implements ApiResponse { - - // TODO: Phase 3 - Add fields: data, timestamp, etc. -} + /** + * Always true for success responses. + */ + @Builder.Default + private boolean success = true; + + /** + * Optional success message. + */ + private String message; + + /** + * The response data (generic type). + */ + private T data; + + /** + * ISO 8601 timestamp when the response was generated. + */ + @Builder.Default + private String timestamp = Instant.now().toString(); + + /** + * Optional pagination metadata (for list responses). + */ + private Pagination pagination; + + /** + * Nested class for pagination metadata. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Pagination { + /** + * Current page number (1-based). + */ + private int page; + + /** + * Number of items per page. + */ + private int limit; + + /** + * Total number of items. + */ + private long total; + + /** + * Total number of pages. + */ + private int pages; + } +} From 7e25be694b74d6dd644f058df9d1c282403b298d Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 15:20:16 -0400 Subject: [PATCH 011/110] feat: add CRUD endpoints --- JAVA-SPRING-IMPLEMENTATION-PLAN.md | 679 ------------------ server/express/.gitignore | 30 +- server/express/src/utils/errorHandler.ts | 4 +- .../samplemflix/SampleMflixApplication.java | 23 +- .../samplemflix/config/CorsConfig.java | 5 +- .../config/DatabaseVerification.java | 10 +- .../samplemflix/config/MongoConfig.java | 6 +- .../controller/MovieController.java | 31 - .../controller/MovieControllerImpl.java | 248 +++++++ .../exception/GlobalExceptionHandler.java | 110 ++- .../mongodb/samplemflix/model/Comment.java | 6 +- .../com/mongodb/samplemflix/model/Movie.java | 7 +- .../mongodb/samplemflix/model/Theater.java | 6 +- .../model/dto/CreateMovieRequest.java | 4 +- .../model/dto/MovieSearchQuery.java | 5 +- .../model/dto/UpdateMovieRequest.java | 4 +- .../model/response/ApiResponse.java | 6 +- .../model/response/ErrorResponse.java | 5 +- .../model/response/SuccessResponse.java | 5 +- .../repository/MovieRepository.java | 118 ++- .../repository/MovieRepositoryImpl.java | 353 +++++++++ .../samplemflix/service/MovieService.java | 47 +- .../samplemflix/service/MovieServiceImpl.java | 309 ++++++++ .../src/main/resources/application.properties | 5 +- 24 files changed, 1189 insertions(+), 837 deletions(-) delete mode 100644 JAVA-SPRING-IMPLEMENTATION-PLAN.md delete mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java diff --git a/JAVA-SPRING-IMPLEMENTATION-PLAN.md b/JAVA-SPRING-IMPLEMENTATION-PLAN.md deleted file mode 100644 index 206e08e..0000000 --- a/JAVA-SPRING-IMPLEMENTATION-PLAN.md +++ /dev/null @@ -1,679 +0,0 @@ -# Java Spring Backend Implementation Plan - -## Executive Summary - -This document outlines the comprehensive implementation plan for translating the Express.js/TypeScript backend into a Java Spring Boot application. The implementation will maintain feature parity with the existing Express backend while following Spring Boot best practices and the MongoDB Java Driver conventions. - ---- - -## 1. Project Overview - -### 1.1 Current State Analysis - -**Existing Express Backend Features:** -- ✅ Basic CRUD operations (insertOne, insertMany, findOne, find, updateOne, updateMany, deleteOne, deleteMany, findOneAndDelete) -- ✅ MongoDB connection management with pre-flight verification -- ✅ Text search index creation and verification -- ✅ Comprehensive error handling -- ✅ Request validation -- ✅ CORS configuration -- ✅ Environment variable configuration -- ✅ Unit tests with Jest - -**Python Backend Status:** -- Partially implemented (only GET and batch POST endpoints) -- Uses FastAPI with PyMongo driver -- Async/await pattern - -### 1.2 Target Architecture - -**Java Spring Boot Stack:** -- **Framework:** Spring Boot 3.x -- **MongoDB Driver:** MongoDB Java Driver (latest stable version) -- **Build Tool:** Maven or Gradle -- **Java Version:** Java 17 or 21 (LTS) -- **Testing:** JUnit 5 + Mockito + Spring Boot Test -- **Documentation:** SpringDoc OpenAPI (Swagger) - ---- - -## 2. Project Structure - -### 2.1 Directory Layout - -``` -server/java-spring/ -├── src/ -│ ├── main/ -│ │ ├── java/ -│ │ │ └── com/ -│ │ │ └── mongodb/ -│ │ │ └── samplemflix/ -│ │ │ ├── SampleMflixApplication.java -│ │ │ ├── config/ -│ │ │ │ ├── MongoConfig.java -│ │ │ │ ├── CorsConfig.java -│ │ │ │ └── DatabaseVerification.java -│ │ │ ├── controller/ -│ │ │ │ └── MovieController.java -│ │ │ ├── service/ -│ │ │ │ └── MovieService.java -│ │ │ ├── repository/ -│ │ │ │ └── MovieRepository.java -│ │ │ ├── model/ -│ │ │ │ ├── Movie.java -│ │ │ │ ├── Theater.java -│ │ │ │ ├── Comment.java -│ │ │ │ ├── dto/ -│ │ │ │ │ ├── CreateMovieRequest.java -│ │ │ │ │ ├── UpdateMovieRequest.java -│ │ │ │ │ └── MovieSearchQuery.java -│ │ │ │ └── response/ -│ │ │ │ ├── ApiResponse.java -│ │ │ │ ├── SuccessResponse.java -│ │ │ │ └── ErrorResponse.java -│ │ │ ├── exception/ -│ │ │ │ ├── GlobalExceptionHandler.java -│ │ │ │ ├── ValidationException.java -│ │ │ │ └── ResourceNotFoundException.java -│ │ │ └── util/ -│ │ │ └── ValidationUtils.java -│ │ └── resources/ -│ │ ├── application.properties -│ │ └── application-dev.properties -│ └── test/ -│ └── java/ -│ └── com/ -│ └── mongodb/ -│ └── samplemflix/ -│ ├── controller/ -│ │ └── MovieControllerTest.java -│ ├── service/ -│ │ └── MovieServiceTest.java -│ └── integration/ -│ └── MovieIntegrationTest.java -├── pom.xml (or build.gradle) -├── README.md -└── .env.example -``` - ---- - -## 3. Implementation Phases - -### Phase 1: Project Setup and Configuration (Days 1-2) - -#### 1.1 Initialize Spring Boot Project -- [ ] Create Spring Boot project using Spring Initializr -- [ ] Configure Maven/Gradle with required dependencies -- [ ] Set up project structure following Spring conventions -- [ ] Configure `.gitignore` for Java/Spring projects - -#### 1.2 Dependencies Configuration - -**Required Dependencies:** -```xml - - - org.springframework.boot - spring-boot-starter-web - - - - - org.mongodb - mongodb-driver-sync - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.projectlombok - lombok - true - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - -``` - -#### 1.3 Environment Configuration -- [ ] Create `application.properties` with MongoDB connection settings -- [ ] Create `.env.example` file documenting required environment variables -- [ ] Implement environment variable loading (Spring Boot handles this natively) -- [ ] Configure CORS settings - -**application.properties:** -```properties -# MongoDB Configuration -spring.data.mongodb.uri=${MONGODB_URI} -spring.data.mongodb.database=sample_mflix - -# Server Configuration -server.port=${PORT:3001} - -# CORS Configuration -cors.allowed.origins=${CORS_ORIGIN:http://localhost:3000} - -# Application Info -spring.application.name=MongoDB Sample MFlix API -``` - ---- - -### Phase 2: Database Configuration and Connection (Days 2-3) ✅ COMPLETE - -#### 2.1 MongoDB Configuration Class ✅ -- [x] Create `MongoConfig.java` to configure MongoDB client -- [x] Implement connection pooling and timeout settings -- [x] Add connection lifecycle management - -**Key Features:** -- ✅ Singleton MongoClient instance -- ✅ Connection string validation -- ✅ Database reference management -- ✅ Graceful shutdown handling (managed by Spring) -- ✅ Connection pool settings (max 100, min 10 connections) -- ✅ Socket timeouts (10s connect, 10s read) -- ✅ Server selection timeout (10s) - -#### 2.2 Database Verification Service ✅ -- [x] Create `DatabaseVerification.java` as a Spring component -- [x] Implement `@PostConstruct` method for startup verification -- [x] Check for movies collection and document count -- [x] Create text search indexes if missing -- [x] Log verification results - -**Verification Checklist:** -- ✅ Movies collection exists -- ✅ Movies collection has documents (with warning if empty) -- ✅ Text search index on plot, title, fullplot fields -- ✅ Log warnings if data is missing -- ✅ Non-blocking verification (app starts even if verification fails) - ---- - -### Phase 3: Model Layer Implementation (Days 3-4) ✅ COMPLETE - -#### 3.1 Domain Models ✅ -- [x] Create `Movie.java` entity class -- [x] Create `Theater.java` entity class -- [x] Create `Comment.java` entity class -- [x] Add BSON annotations for MongoDB mapping (using Lombok) -- [x] Implement nested objects (Awards, IMDB, Tomatoes) - -**Movie.java Structure:** ✅ -- ✅ All fields from TypeScript Movie interface -- ✅ Nested class: Awards (wins, nominations, text) -- ✅ Nested class: Imdb (rating, votes, id) -- ✅ Nested class: Tomatoes (viewer, critic, fresh, rotten, production, lastUpdated) -- ✅ Nested classes: Tomatoes.Viewer and Tomatoes.Critic -- ✅ Lombok annotations (@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor) -- ✅ Comprehensive JavaDoc documentation - -**Theater.java Structure:** ✅ -- ✅ All fields from TypeScript Theater interface -- ✅ Nested class: Location (address, geo) -- ✅ Nested class: Location.Address (street1, city, state, zipcode) -- ✅ Nested class: Location.Geo (type, coordinates) -- ✅ GeoJSON format support for geospatial queries - -**Comment.java Structure:** ✅ -- ✅ All fields from TypeScript Comment interface -- ✅ Fields: id, name, email, movieId, text, date - -#### 3.2 DTOs (Data Transfer Objects) ✅ -- [x] Create `CreateMovieRequest.java` -- [x] Create `UpdateMovieRequest.java` -- [x] Create `MovieSearchQuery.java` -- [x] Add validation annotations (@NotBlank for required fields) - -**CreateMovieRequest.java:** ✅ -- ✅ Required field: title (with @NotBlank validation) -- ✅ Optional fields: year, plot, fullplot, genres, directors, writers, cast, countries, languages, rated, runtime, poster - -**UpdateMovieRequest.java:** ✅ -- ✅ All fields optional (for partial updates) -- ✅ Same fields as CreateMovieRequest - -**MovieSearchQuery.java:** ✅ -- ✅ Full-text search: q -- ✅ Filters: genre, year, minRating, maxRating -- ✅ Pagination: limit, skip -- ✅ Sorting: sortBy, sortOrder - -#### 3.3 Response Models ✅ -- [x] Create generic `ApiResponse` interface -- [x] Create `SuccessResponse` class -- [x] Create `ErrorResponse` class -- [x] Add timestamp and metadata fields - -**ApiResponse Interface:** ✅ -- ✅ Methods: isSuccess(), getTimestamp() - -**SuccessResponse:** ✅ -- ✅ Generic type parameter for data -- ✅ Fields: success (true), message, data, timestamp, pagination -- ✅ Nested class: Pagination (page, limit, total, pages) -- ✅ @JsonInclude(NON_NULL) to exclude null fields - -**ErrorResponse:** ✅ -- ✅ Fields: success (false), message, error, timestamp -- ✅ Nested class: ErrorDetails (message, code, details) -- ✅ Matches Express backend error format - ---- - -### Phase 4: Repository Layer (Days 4-5) - -#### 4.1 Custom Repository Implementation -- [ ] Create `MovieRepository.java` interface -- [ ] Implement custom repository using MongoTemplate or MongoCollection -- [ ] Add methods for all CRUD operations - -**Repository Methods:** -```java -public interface MovieRepository { - // Basic CRUD - Movie insertOne(Movie movie); - List insertMany(List movies); - Optional findById(ObjectId id); - List find(Document filter, Document sort, int skip, int limit); - UpdateResult updateOne(ObjectId id, Document update); - UpdateResult updateMany(Document filter, Document update); - DeleteResult deleteOne(ObjectId id); - DeleteResult deleteMany(Document filter); - Optional findOneAndDelete(ObjectId id); - - // Utility - long countDocuments(); -} -``` - ---- - -### Phase 5: Service Layer (Days 5-7) - -#### 5.1 Movie Service Implementation -- [ ] Create `MovieService.java` with business logic -- [ ] Implement all CRUD operations -- [ ] Add query building logic for filtering -- [ ] Implement pagination and sorting -- [ ] Add validation logic - -**Service Responsibilities:** -- Business logic and validation -- Query construction (filters, sorts, pagination) -- Data transformation between DTOs and entities -- Error handling and exception throwing - ---- - -### Phase 6: Controller Layer (Days 7-9) - -#### 6.1 REST API Endpoints -- [ ] Create `MovieController.java` with REST endpoints -- [ ] Implement all endpoints matching Express routes -- [ ] Add request validation -- [ ] Add API documentation annotations - -**Endpoint Mapping:** -``` -GET /api/movies -> getAllMovies() -GET /api/movies/{id} -> getMovieById() -POST /api/movies -> createMovie() -POST /api/movies/batch -> createMoviesBatch() -PUT /api/movies/{id} -> updateMovie() -PATCH /api/movies -> updateMoviesBatch() -DELETE /api/movies/{id} -> deleteMovie() -DELETE /api/movies -> deleteMoviesBatch() -DELETE /api/movies/{id}/find-and-delete -> findAndDeleteMovie() -``` - -#### 6.2 Request/Response Handling -- [ ] Implement consistent response wrapping -- [ ] Add query parameter parsing -- [ ] Implement request body validation -- [ ] Add proper HTTP status codes - ---- - -### Phase 7: Error Handling (Days 9-10) - -#### 7.1 Exception Hierarchy -- [ ] Create custom exception classes -- [ ] Implement `GlobalExceptionHandler` with @ControllerAdvice -- [ ] Handle MongoDB-specific exceptions -- [ ] Handle validation exceptions - -**Exception Types:** -- `ResourceNotFoundException` (404) -- `ValidationException` (400) -- `DuplicateKeyException` (409) -- `DatabaseException` (500) - -#### 7.2 Error Response Format -- [ ] Standardize error response structure -- [ ] Include error codes and messages -- [ ] Add timestamp and request path -- [ ] Log errors appropriately - ---- - -### Phase 8: Testing (Days 10-12) - -#### 8.1 Unit Tests -- [ ] Test MovieService methods -- [ ] Test MovieController endpoints -- [ ] Mock repository layer -- [ ] Achieve >80% code coverage - -#### 8.2 Integration Tests -- [ ] Test with embedded MongoDB or Testcontainers -- [ ] Test full request/response cycle -- [ ] Test error scenarios -- [ ] Test database operations - -#### 8.3 Test Configuration -- [ ] Create test application.properties -- [ ] Set up test fixtures and data -- [ ] Configure test MongoDB instance - ---- - -### Phase 9: Documentation and Polish (Days 12-13) - -#### 9.1 Code Documentation -- [ ] Add comprehensive JavaDoc comments -- [ ] Document MongoDB operations clearly -- [ ] Add inline comments explaining driver usage -- [ ] Note deviations from standard practices - -#### 9.2 API Documentation -- [ ] Configure Swagger/OpenAPI -- [ ] Add endpoint descriptions -- [ ] Document request/response schemas -- [ ] Add example requests - -#### 9.3 README and Setup Guide -- [ ] Create comprehensive README.md -- [ ] Document prerequisites (Java version, Maven/Gradle) -- [ ] Provide setup instructions -- [ ] Document environment variables -- [ ] Add troubleshooting section - ---- - -## 4. Technical Implementation Details - -### 4.1 MongoDB Driver Usage Patterns - -**Connection Management:** -```java -@Configuration -public class MongoConfig { - @Bean - public MongoClient mongoClient(@Value("${spring.data.mongodb.uri}") String uri) { - return MongoClients.create(uri); - } - - @Bean - public MongoDatabase mongoDatabase(MongoClient client) { - return client.getDatabase("sample_mflix"); - } -} -``` - -**Collection Access:** -```java -@Repository -public class MovieRepositoryImpl implements MovieRepository { - private final MongoCollection collection; - - public MovieRepositoryImpl(MongoDatabase database) { - this.collection = database.getCollection("movies"); - } -} -``` - -### 4.2 Query Building Examples - -**Text Search:** -```java -Document filter = new Document("$text", new Document("$search", searchQuery)); -``` - -**Range Queries:** -```java -Document ratingFilter = new Document() - .append("$gte", minRating) - .append("$lte", maxRating); -filter.append("imdb.rating", ratingFilter); -``` - -**Sorting:** -```java -Document sort = new Document(sortBy, sortOrder.equals("desc") ? -1 : 1); -``` - -### 4.3 Error Handling Pattern - -```java -@ControllerAdvice -public class GlobalExceptionHandler { - @ExceptionHandler(MongoException.class) - public ResponseEntity handleMongoException(MongoException ex) { - // Parse MongoDB error codes - // Return appropriate HTTP status and error response - } -} -``` - ---- - -## 5. Migration Mapping - -### 5.1 Express to Spring Equivalents - -| Express Concept | Spring Boot Equivalent | -|----------------|------------------------| -| `app.ts` | `SampleMflixApplication.java` (main class) | -| Routes (`movies.ts`) | `@RestController` in `MovieController.java` | -| Controllers | `@Service` in `MovieService.java` | -| `database.ts` | `MongoConfig.java` + `DatabaseVerification.java` | -| `errorHandler.ts` | `GlobalExceptionHandler.java` | -| Types/Interfaces | Java classes with proper annotations | -| `asyncHandler` | Spring handles async automatically | -| `dotenv` | `application.properties` + environment variables | - -### 5.2 Code Pattern Translations - -**Express Pattern:** -```typescript -router.get("/", asyncHandler(movieController.getAllMovies)); -``` - -**Spring Pattern:** -```java -@GetMapping -public ResponseEntity>> getAllMovies( - @RequestParam(required = false) String q, - // ... other params -) { - // Implementation -} -``` - ---- - -## 6. Quality Assurance Checklist - -### 6.1 Functional Requirements -- [ ] All CRUD operations work correctly -- [ ] Text search functionality works -- [ ] Filtering and pagination work -- [ ] Sorting works correctly -- [ ] Batch operations work -- [ ] Error handling is comprehensive - -### 6.2 Code Quality -- [ ] Code follows Spring Boot best practices -- [ ] Comprehensive comments explaining MongoDB operations -- [ ] Proper separation of concerns (Controller/Service/Repository) -- [ ] No hardcoded values -- [ ] Proper exception handling - -### 6.3 Testing -- [ ] Unit tests pass -- [ ] Integration tests pass -- [ ] Code coverage >80% -- [ ] Edge cases tested - -### 6.4 Documentation -- [ ] README is comprehensive -- [ ] API documentation is complete -- [ ] Code comments are thorough -- [ ] Setup instructions are clear - ---- - -## 7. Timeline and Milestones - -| Phase | Duration | Deliverable | -|-------|----------|-------------| -| Phase 1: Setup | 2 days | Project structure, dependencies configured | -| Phase 2: Database | 1 day | MongoDB connection and verification working | -| Phase 3: Models | 1 day | All model classes implemented | -| Phase 4: Repository | 1 day | Repository layer complete | -| Phase 5: Service | 2 days | Business logic implemented | -| Phase 6: Controller | 2 days | All REST endpoints working | -| Phase 7: Error Handling | 1 day | Comprehensive error handling | -| Phase 8: Testing | 2 days | Tests written and passing | -| Phase 9: Documentation | 1 day | Documentation complete | -| **Total** | **13 days** | **Production-ready Java Spring backend** | - ---- - -## 8. Post-Implementation Tasks - -### 8.1 Integration with Frontend -- [ ] Test with existing Next.js frontend -- [ ] Verify CORS configuration -- [ ] Test all API endpoints from frontend -- [ ] Verify response format compatibility - -### 8.2 Performance Optimization -- [ ] Add connection pooling configuration -- [ ] Optimize query performance -- [ ] Add caching if needed -- [ ] Profile and optimize hot paths - -### 8.3 Deployment Preparation -- [ ] Create Docker configuration -- [ ] Document deployment process -- [ ] Create production configuration -- [ ] Add health check endpoints - ---- - -## 9. Success Criteria - -The Java Spring implementation will be considered complete when: - -1. ✅ All CRUD operations match Express functionality -2. ✅ All tests pass with >80% coverage -3. ✅ API responses match Express format exactly -4. ✅ Frontend works without modifications -5. ✅ Documentation is comprehensive -6. ✅ Code follows Spring Boot best practices -7. ✅ MongoDB operations are well-documented -8. ✅ Error handling is robust -9. ✅ Pre-flight verification works -10. ✅ README provides clear setup instructions - ---- - -## 10. Notes and Considerations - -### 10.1 Design Decisions - -**Why not Spring Data MongoDB?** -- The scope requires direct MongoDB Driver usage to demonstrate driver operations -- Spring Data MongoDB abstracts away driver details -- Educational value is in showing raw driver usage - -**Lombok Usage:** -- Optional but recommended for reducing boilerplate -- Helps maintain readability -- Can be removed if team prefers explicit code - -**Testing Strategy:** -- Unit tests for service layer -- Integration tests for full stack -- Consider Testcontainers for realistic MongoDB testing - -### 10.2 Future Enhancements (Post-MVP) - -- [ ] Aggregation pipeline endpoints -- [ ] Full-text search with Atlas Search -- [ ] Vector search implementation -- [ ] Geospatial queries for theaters -- [ ] Caching layer -- [ ] Rate limiting -- [ ] Metrics and monitoring - ---- - -## Appendix A: Key Files Reference - -### Express Backend Files to Reference -- `server/express/src/app.ts` - Main application setup -- `server/express/src/config/database.ts` - Database configuration -- `server/express/src/controllers/movieController.ts` - Business logic -- `server/express/src/routes/movies.ts` - Route definitions -- `server/express/src/types/index.ts` - Type definitions -- `server/express/src/utils/errorHandler.ts` - Error handling - -### Python Backend Files (for comparison) -- `server/python/main.py` - FastAPI application -- `server/python/src/routers/movies.py` - Route handlers -- `server/python/src/database/mongo_client.py` - Database connection - ---- - -## Appendix B: Environment Variables - -Required environment variables for Java Spring backend: - -```properties -# MongoDB Connection -MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority - -# Server Configuration -PORT=3001 - -# CORS Configuration -CORS_ORIGIN=http://localhost:3000 -``` - ---- - -**Document Version:** 1.0 -**Last Updated:** 2025-10-23 -**Author:** Implementation Plan Generator -**Status:** Ready for Implementation diff --git a/server/express/.gitignore b/server/express/.gitignore index 26db928..8fa0429 100644 --- a/server/express/.gitignore +++ b/server/express/.gitignore @@ -1,26 +1,16 @@ -# Dependencies -node_modules/ -npm-debug.log* -package-lock.json +# ============================================================================= +# Express.js Backend - Specific Ignores +# ============================================================================= +# Common patterns (node_modules, .env, .DS_Store, etc.) are in root .gitignore +# This file only contains Express/TypeScript-specific patterns +# ============================================================================= -# Environment variables -.env - -# Build output +# Build Output dist/ +build/ # TypeScript *.tsbuildinfo -# Logs -logs -*.log - -# Test coverage -coverage/ - -# Optional npm cache directory -.npm - -# macOS -.DS_Store \ No newline at end of file +# Package Lock (optional - remove this line if you want to commit package-lock.json) +# package-lock.json diff --git a/server/express/src/utils/errorHandler.ts b/server/express/src/utils/errorHandler.ts index 929cf29..83a2b54 100644 --- a/server/express/src/utils/errorHandler.ts +++ b/server/express/src/utils/errorHandler.ts @@ -28,13 +28,13 @@ export class ValidationError extends Error { * @param err - The error that was thrown * @param req - Express request object * @param res - Express response object - * @param next - Express next function + * @param _next - Express next function */ export function errorHandler( err: Error, req: Request, res: Response, - next: NextFunction + _next: NextFunction ): void { // Log the error for debugging purposes // In production, we recommend using a logging service diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java index 52f367b..86ea28f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -2,22 +2,39 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; /** * Main Spring Boot application class for the MongoDB Sample MFlix API. - * + * * This application demonstrates MongoDB CRUD operations using the MongoDB Java Driver * in a Spring Boot environment. It provides a REST API for managing movie data from * the sample_mflix database. - * + * * @author MongoDB Documentation Team * @version 1.0 */ @SpringBootApplication +@RestController public class SampleMflixApplication { public static void main(String[] args) { SpringApplication.run(SampleMflixApplication.class, args); } -} + /** + * Root endpoint providing basic information about the API. + */ + @GetMapping("/") + public Map root() { + return Map.of( + "name", "MongoDB Sample MFlix API", + "version", "1.0.0", + "description", "Java Spring Boot backend demonstrating MongoDB operations with the sample_mflix dataset", + "endpoints", Map.of("movies", "/api/movies") + ); + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java index f1a9ae6..178cdc3 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java @@ -11,10 +11,10 @@ /** * CORS (Cross-Origin Resource Sharing) configuration for the Sample MFlix API. - * + *

* This configuration allows the frontend application (typically running on a different port * during development) to make requests to this backend API. - * + *

* The allowed origins are configured via the CORS_ORIGIN environment variable. */ @Configuration @@ -50,4 +50,3 @@ public CorsFilter corsFilter() { return new CorsFilter(source); } } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java index 38cb728..6063bb6 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java @@ -13,16 +13,16 @@ /** * Database verification component that runs on application startup. - * + *

* This component performs pre-flight checks to ensure the MongoDB database * is properly configured and contains the expected data and indexes. - * + *

* Verification steps: * 1. Check if the movies collection exists * 2. Verify the collection contains documents * 3. Check for text search indexes on plot, title, and fullplot fields * 4. Create text search index if missing - * + *

* This matches the behavior of the Express.js backend's verifyRequirements() function. * The verification is non-blocking - the application will start even if verification fails, * but warnings will be logged to help developers identify configuration issues. @@ -43,10 +43,10 @@ public DatabaseVerification(MongoDatabase database) { /** * Runs database verification checks after the bean is constructed. - * + *

* This method is called automatically by Spring after dependency injection * is complete. It performs all verification steps and logs the results. - * + *

* The method catches all exceptions to prevent application startup failure, * but logs errors to help developers identify issues. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index e6d294a..3afa3a4 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -13,17 +13,17 @@ /** * MongoDB configuration class for the Sample MFlix application. - * + *

* This class configures the MongoDB client connection using the MongoDB Java Driver. * It creates singleton beans for MongoClient and MongoDatabase that will be injected * throughout the application. - * + *

* Key features: * - Connection pooling with configurable settings (max 100 connections, min 10) * - Connection timeout configuration (10 seconds for connect and read) * - Graceful shutdown handling (managed by Spring's bean lifecycle) * - Connection string validation - * + *

* The MongoClient is thread-safe and designed to be shared across the application. * Spring automatically manages the lifecycle and closes the client on shutdown. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java deleted file mode 100644 index 463e27c..0000000 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.mongodb.samplemflix.controller; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * REST controller for movie-related endpoints. - * - * This controller handles all HTTP requests for movie operations including: - * - GET /api/movies - Get all movies with filtering, sorting, and pagination - * - GET /api/movies/{id} - Get a single movie by ID - * - POST /api/movies - Create a new movie - * - POST /api/movies/batch - Create multiple movies - * - PUT /api/movies/{id} - Update a movie - * - PATCH /api/movies - Update multiple movies - * - DELETE /api/movies/{id} - Delete a movie - * - DELETE /api/movies - Delete multiple movies - * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie - * - * TODO: Phase 6 - Implement all REST endpoints - * TODO: Phase 6 - Add request validation - * TODO: Phase 6 - Add API documentation annotations - */ -@RestController -@RequestMapping("/api/movies") -public class MovieController { - - // TODO: Phase 6 - Inject MovieService - // TODO: Phase 6 - Implement endpoint methods -} - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java new file mode 100644 index 0000000..643b48e --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -0,0 +1,248 @@ +package com.mongodb.samplemflix.controller; + +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import com.mongodb.samplemflix.model.response.SuccessResponse; +import com.mongodb.samplemflix.service.MovieService; +import jakarta.validation.Valid; +import org.bson.Document; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * REST controller for movie-related endpoints. + *

+ * This controller handles all HTTP requests for movie operations including: + * - GET /api/movies - Get all movies with filtering, sorting, and pagination + * - GET /api/movies/{id} - Get a single movie by ID + * - POST /api/movies - Create a new movie + * - POST /api/movies/batch - Create multiple movies + * - PUT /api/movies/{id} - Update a movie + * - PATCH /api/movies - Update multiple movies + * - DELETE /api/movies/{id} - Delete a movie + * - DELETE /api/movies - Delete multiple movies + * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie + */ +@RestController +@RequestMapping("/api/movies") +public class MovieControllerImpl { + + private final MovieService movieService; + + public MovieControllerImpl(MovieService movieService) { + this.movieService = movieService; + } + + /** + * GET /api/movies + *

+ * Retrieves multiple movies with optional filtering, sorting, and pagination. + */ + @GetMapping + public ResponseEntity>> getAllMovies( + @RequestParam(required = false) String q, + @RequestParam(required = false) String genre, + @RequestParam(required = false) Integer year, + @RequestParam(required = false) Double minRating, + @RequestParam(required = false) Double maxRating, + @RequestParam(defaultValue = "20") Integer limit, + @RequestParam(defaultValue = "0") Integer skip, + @RequestParam(defaultValue = "title") String sortBy, + @RequestParam(defaultValue = "asc") String sortOrder) { + + MovieSearchQuery query = MovieSearchQuery.builder() + .q(q) + .genre(genre) + .year(year) + .minRating(minRating) + .maxRating(maxRating) + .limit(limit) + .skip(skip) + .sortBy(sortBy) + .sortOrder(sortOrder) + .build(); + + List movies = movieService.getAllMovies(query); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message("Found " + movies.size() + " movies") + .data(movies) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * GET /api/movies/{id} + *

+ * Retrieves a single movie by its ObjectId. + */ + @GetMapping("/{id}") + public ResponseEntity> getMovieById(@PathVariable String id) { + Movie movie = movieService.getMovieById(id); + + SuccessResponse response = SuccessResponse.builder() + .success(true) + .message("Movie retrieved successfully") + .data(movie) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * POST /api/movies + *

+ * Creates a single new movie document. + */ + @PostMapping + public ResponseEntity> createMovie(@Valid @RequestBody CreateMovieRequest request) { + Movie movie = movieService.createMovie(request); + + SuccessResponse response = SuccessResponse.builder() + .success(true) + .message("Movie '" + request.getTitle() + "' created successfully") + .data(movie) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + /** + * POST /api/movies/batch + *

+ * Creates multiple movie documents in a single operation. + */ + @PostMapping("/batch") + public ResponseEntity>> createMoviesBatch( + @RequestBody List requests) { + Map result = movieService.createMoviesBatch(requests); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message("Successfully created " + result.get("insertedCount") + " movies") + .data(result) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + /** + * PUT /api/movies/{id} + *

+ * Updates a single movie document. + */ + @PutMapping("/{id}") + public ResponseEntity> updateMovie( + @PathVariable String id, + @RequestBody UpdateMovieRequest request) { + Movie movie = movieService.updateMovie(id, request); + + SuccessResponse response = SuccessResponse.builder() + .success(true) + .message("Movie updated successfully") + .data(movie) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * PATCH /api/movies + *

+ * Updates multiple movies based on a filter. + */ + @SuppressWarnings("unchecked") + @PatchMapping + public ResponseEntity>> updateMoviesBatch( + @RequestBody Map body) { + Document filter = new Document((Map) body.get("filter")); + Document update = new Document((Map) body.get("update")); + + Map result = movieService.updateMoviesBatch(filter, update); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message("Update operation completed. Matched " + result.get("matchedCount") + + " documents, modified " + result.get("modifiedCount") + " documents.") + .data(result) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * DELETE /api/movies/{id}/find-and-delete + *

+ * Finds and deletes a movie in a single atomic operation. + */ + @DeleteMapping("/{id}/find-and-delete") + public ResponseEntity> findAndDeleteMovie(@PathVariable String id) { + Movie movie = movieService.findAndDeleteMovie(id); + + SuccessResponse response = SuccessResponse.builder() + .success(true) + .message("Movie found and deleted successfully") + .data(movie) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * DELETE /api/movies/{id} + *

+ * Deletes a single movie document. + */ + @DeleteMapping("/{id}") + public ResponseEntity>> deleteMovie(@PathVariable String id) { + Map result = movieService.deleteMovie(id); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message("Movie deleted successfully") + .data(result) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * DELETE /api/movies + *

+ * Deletes multiple movies based on a filter. + */ + @SuppressWarnings("unchecked") + @DeleteMapping + public ResponseEntity>> deleteMoviesBatch( + @RequestBody Map body) { + Document filter = new Document((Map) body.get("filter")); + + Map result = movieService.deleteMoviesBatch(filter); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message("Delete operation completed. Removed " + result.get("deletedCount") + " documents.") + .data(result) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java index 4fbfaf8..fc344ea 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java @@ -1,26 +1,108 @@ package com.mongodb.samplemflix.exception; +import com.mongodb.MongoWriteException; +import com.mongodb.samplemflix.model.response.ErrorResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.time.Instant; /** * Global exception handler for the application. - * + * * This class uses @ControllerAdvice to handle exceptions thrown by controllers * and convert them into appropriate HTTP responses. - * - * Exception types to handle: - * - ResourceNotFoundException (404) - * - ValidationException (400) - * - DuplicateKeyException (409) - * - MongoException (500) - * - General exceptions (500) - * - * TODO: Phase 7 - Implement exception handler methods - * TODO: Phase 7 - Add @ExceptionHandler methods for each exception type */ @ControllerAdvice public class GlobalExceptionHandler { - - // TODO: Phase 7 - Implement exception handlers -} + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFoundException( + ResourceNotFoundException ex, WebRequest request) { + logger.error("Resource not found: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message(ex.getMessage()) + .error(ErrorResponse.ErrorDetails.builder() + .message(ex.getMessage()) + .code("RESOURCE_NOT_FOUND") + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException( + ValidationException ex, WebRequest request) { + logger.error("Validation error: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message("Validation failed") + .error(ErrorResponse.ErrorDetails.builder() + .message(ex.getMessage()) + .code("VALIDATION_ERROR") + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MongoWriteException.class) + public ResponseEntity handleMongoWriteException( + MongoWriteException ex, WebRequest request) { + logger.error("MongoDB write error: {}", ex.getMessage()); + + String message = "Database error"; + String code = "DATABASE_ERROR"; + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + + if (ex.getError().getCode() == 11000) { + message = "Duplicate key error"; + code = "DUPLICATE_KEY"; + status = HttpStatus.CONFLICT; + } + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message(message) + .error(ErrorResponse.ErrorDetails.builder() + .message(message) + .code(code) + .details(ex.getError().getCode()) + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, status); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException( + Exception ex, WebRequest request) { + logger.error("Unexpected error occurred", ex); + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message(ex.getMessage() != null ? ex.getMessage() : "Internal server error") + .error(ErrorResponse.ErrorDetails.builder() + .message(ex.getMessage() != null ? ex.getMessage() : "Internal server error") + .code("INTERNAL_ERROR") + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java index 2702f6b..e7a61f4 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java @@ -10,12 +10,9 @@ /** * Domain model representing a comment document from the MongoDB comments collection. - * + *

* This class maps to the comments collection in the sample_mflix database. * Comments are user reviews/comments associated with movies. - * - * The structure matches the TypeScript Comment interface from the Express backend - * to ensure API compatibility. */ @Data @Builder @@ -55,4 +52,3 @@ public class Comment { */ private Date date; } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index e0c6acb..98235e0 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -11,14 +11,11 @@ /** * Domain model representing a movie document from the MongoDB movies collection. - * + *

* This class maps to the movies collection in the sample_mflix database. * It includes all fields from the movie documents including nested objects * for awards, IMDB ratings, and Tomatoes ratings. - * - * The structure matches the TypeScript Movie interface from the Express backend - * to ensure API compatibility. - * + *

* Note: We use Lombok annotations to reduce boilerplate code: * - @Data: Generates getters, setters, toString, equals, and hashCode * - @Builder: Provides a fluent builder pattern for object construction diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java index e636226..2dc8161 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java @@ -8,12 +8,9 @@ /** * Domain model representing a theater document from the MongoDB theaters collection. - * + *

* This class maps to the theaters collection in the sample_mflix database. * It includes location information with address and geospatial coordinates. - * - * The structure matches the TypeScript Theater interface from the Express backend - * to ensure API compatibility. */ @Data @Builder @@ -106,4 +103,3 @@ public static class Geo { } } } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java index 179bb03..485f65b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java @@ -10,11 +10,9 @@ /** * Data Transfer Object for creating a new movie. - * + *

* This DTO is used for POST /api/movies requests. * It includes validation annotations to ensure required fields are present. - * - * The structure matches the TypeScript CreateMovieRequest interface from the Express backend. * Only the title field is required; all other fields are optional. */ @Data diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java index b9ad823..20d6833 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java @@ -7,11 +7,9 @@ /** * Data Transfer Object for movie search query parameters. - * + *

* This DTO is used to parse and validate query parameters for GET /api/movies requests. * It supports full-text search, filtering by genre/year/rating, sorting, and pagination. - * - * The structure matches the search functionality from the Express backend. */ @Data @Builder @@ -67,4 +65,3 @@ public class MovieSearchQuery { */ private String sortOrder; } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java index 50423cb..99d9ab3 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -9,11 +9,9 @@ /** * Data Transfer Object for updating an existing movie. - * + *

* This DTO is used for PUT /api/movies/{id} requests. * All fields are optional since partial updates are allowed. - * - * The structure matches the TypeScript UpdateMovieRequest interface from the Express backend. * Any field that is null will not be updated in the database. */ @Data diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java index fb06da9..b76fa3d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java @@ -2,15 +2,13 @@ /** * Generic API response interface. - * + *

* This interface is implemented by both SuccessResponse and ErrorResponse * to provide a consistent response structure across all API endpoints. - * + *

* All API responses include: * - success: boolean indicating if the request was successful * - timestamp: ISO 8601 timestamp of when the response was generated - * - * This matches the TypeScript ApiResponse type from the Express backend. */ public interface ApiResponse { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java index f81ded1..457dd52 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java @@ -10,10 +10,9 @@ /** * Error response wrapper for API error responses. - * + *

* This class wraps error responses with error codes, messages, and metadata. - * - * The structure matches the TypeScript ErrorResponse type from the Express backend: + *

* { * success: false, * message: string, diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java index 36c0e41..251c6f3 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java @@ -10,11 +10,10 @@ /** * Success response wrapper for API responses. - * + *

* This class wraps successful API responses with metadata like timestamp and pagination. * It uses a generic type parameter T to hold the response data. - * - * The structure matches the TypeScript SuccessResponse type from the Express backend: + *

* { * success: true, * message?: string, diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java index a4efb4e..b416ba1 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java @@ -1,27 +1,105 @@ package com.mongodb.samplemflix.repository; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertManyResult; +import com.mongodb.client.result.InsertOneResult; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.samplemflix.model.Movie; +import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.List; +import java.util.Optional; + /** * Repository interface for movie data access. - * - * This repository provides methods for all CRUD operations using the MongoDB Java Driver. - * - * Methods to implement: - * - insertOne(Movie movie) - * - insertMany(List movies) - * - findById(ObjectId id) - * - find(Document filter, Document sort, int skip, int limit) - * - updateOne(ObjectId id, Document update) - * - updateMany(Document filter, Document update) - * - deleteOne(ObjectId id) - * - deleteMany(Document filter) - * - findOneAndDelete(ObjectId id) - * - countDocuments() - * - * TODO: Phase 4 - Define repository interface methods - * TODO: Phase 4 - Create implementation class using MongoCollection + * + * This repository provides methods for all CRUD operations using the MongoDB Java Driver directly. + * The implementation uses MongoCollection for direct control over BSON documents. */ public interface MovieRepository { - - // TODO: Phase 4 - Add method signatures -} + /** + * Inserts a single movie document. + * + * @param movie the movie to insert + * @return the result of the insert operation + */ + InsertOneResult insertOne(Movie movie); + + /** + * Inserts multiple movie documents. + * + * @param movies the list of movies to insert + * @return the result of the insert operation + */ + InsertManyResult insertMany(List movies); + + /** + * Finds a single movie by its ID. + * + * @param id the movie ID + * @return Optional containing the movie if found, empty otherwise + */ + Optional findById(ObjectId id); + + /** + * Finds multiple movies with filtering, sorting, and pagination. + * + * @param filter the filter document + * @param sort the sort document + * @param skip number of documents to skip + * @param limit maximum number of documents to return + * @return list of movies matching the criteria + */ + List find(Document filter, Document sort, int skip, int limit); + + /** + * Updates a single movie by ID. + * + * @param id the movie ID + * @param update the update document + * @return the result of the update operation + */ + UpdateResult updateOne(ObjectId id, Document update); + + /** + * Updates multiple movies matching the filter. + * + * @param filter the filter document + * @param update the update document + * @return the result of the update operation + */ + UpdateResult updateMany(Document filter, Document update); + + /** + * Deletes a single movie by ID. + * + * @param id the movie ID + * @return the result of the delete operation + */ + DeleteResult deleteOne(ObjectId id); + + /** + * Deletes multiple movies matching the filter. + * + * @param filter the filter document + * @return the result of the delete operation + */ + DeleteResult deleteMany(Document filter); + + /** + * Finds and deletes a single movie in one atomic operation. + * + * @param id the movie ID + * @return Optional containing the deleted movie if found, empty otherwise + */ + Optional findOneAndDelete(ObjectId id); + + /** + * Counts the total number of documents in the movies collection. + * + * @return the count of documents + */ + long countDocuments(); +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java new file mode 100644 index 0000000..a8ccc16 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java @@ -0,0 +1,353 @@ +package com.mongodb.samplemflix.repository; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertManyResult; +import com.mongodb.client.result.InsertOneResult; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.samplemflix.model.Movie; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implementation of MovieRepository using MongoDB Java Driver directly. + * + * This class demonstrates direct usage of MongoCollection for CRUD operations. + * It manually converts between Movie objects and BSON Documents to provide full control + * over the MongoDB operations. + * + * This approach is used for educational purposes to show how the MongoDB driver works, + * rather than using Spring Data MongoDB which abstracts these details. + */ +@Repository +public class MovieRepositoryImpl implements MovieRepository { + + private final MongoCollection moviesCollection; + + public MovieRepositoryImpl(MongoDatabase mongoDatabase) { + this.moviesCollection = mongoDatabase.getCollection("movies"); + } + + @Override + public InsertOneResult insertOne(Movie movie) { + Document doc = movieToDocument(movie); + return moviesCollection.insertOne(doc); + } + + @Override + public InsertManyResult insertMany(List movies) { + List documents = movies.stream() + .map(this::movieToDocument) + .collect(Collectors.toList()); + return moviesCollection.insertMany(documents); + } + + @Override + public Optional findById(ObjectId id) { + Document doc = moviesCollection.find(Filters.eq("_id", id)).first(); + return Optional.ofNullable(doc).map(this::documentToMovie); + } + + @Override + public List find(Document filter, Document sort, int skip, int limit) { + List movies = new ArrayList<>(); + moviesCollection.find(filter) + .sort(sort) + .skip(skip) + .limit(limit) + .forEach(doc -> movies.add(documentToMovie(doc))); + return movies; + } + + @Override + public UpdateResult updateOne(ObjectId id, Document update) { + return moviesCollection.updateOne(Filters.eq("_id", id), update); + } + + @Override + public UpdateResult updateMany(Document filter, Document update) { + return moviesCollection.updateMany(filter, update); + } + + @Override + public DeleteResult deleteOne(ObjectId id) { + return moviesCollection.deleteOne(Filters.eq("_id", id)); + } + + @Override + public DeleteResult deleteMany(Document filter) { + return moviesCollection.deleteMany(filter); + } + + @Override + public Optional findOneAndDelete(ObjectId id) { + Document doc = moviesCollection.findOneAndDelete(Filters.eq("_id", id)); + return Optional.ofNullable(doc).map(this::documentToMovie); + } + + @Override + public long countDocuments() { + return moviesCollection.countDocuments(); + } + + /** + * Converts a Movie object to a BSON Document. + * + * @param movie the Movie object + * @return the BSON Document + */ + private Document movieToDocument(Movie movie) { + Document doc = new Document(); + + if (movie.getId() != null) { + doc.append("_id", movie.getId()); + } + if (movie.getTitle() != null) { + doc.append("title", movie.getTitle()); + } + if (movie.getYear() != null) { + doc.append("year", movie.getYear()); + } + if (movie.getPlot() != null) { + doc.append("plot", movie.getPlot()); + } + if (movie.getFullplot() != null) { + doc.append("fullplot", movie.getFullplot()); + } + if (movie.getReleased() != null) { + doc.append("released", movie.getReleased()); + } + if (movie.getRuntime() != null) { + doc.append("runtime", movie.getRuntime()); + } + if (movie.getPoster() != null) { + doc.append("poster", movie.getPoster()); + } + if (movie.getGenres() != null) { + doc.append("genres", movie.getGenres()); + } + if (movie.getDirectors() != null) { + doc.append("directors", movie.getDirectors()); + } + if (movie.getWriters() != null) { + doc.append("writers", movie.getWriters()); + } + if (movie.getCast() != null) { + doc.append("cast", movie.getCast()); + } + if (movie.getCountries() != null) { + doc.append("countries", movie.getCountries()); + } + if (movie.getLanguages() != null) { + doc.append("languages", movie.getLanguages()); + } + if (movie.getRated() != null) { + doc.append("rated", movie.getRated()); + } + if (movie.getAwards() != null) { + doc.append("awards", awardsToDocument(movie.getAwards())); + } + if (movie.getImdb() != null) { + doc.append("imdb", imdbToDocument(movie.getImdb())); + } + if (movie.getTomatoes() != null) { + doc.append("tomatoes", tomatoesToDocument(movie.getTomatoes())); + } + if (movie.getMetacritic() != null) { + doc.append("metacritic", movie.getMetacritic()); + } + if (movie.getType() != null) { + doc.append("type", movie.getType()); + } + + return doc; + } + + /** + * Converts a BSON Document to a Movie object. + * + * @param doc the BSON Document + * @return the Movie object + */ + @SuppressWarnings("unchecked") + private Movie documentToMovie(Document doc) { + Movie movie = new Movie(); + + movie.setId(doc.getObjectId("_id")); + movie.setTitle(doc.getString("title")); + movie.setYear(doc.getInteger("year")); + movie.setPlot(doc.getString("plot")); + movie.setFullplot(doc.getString("fullplot")); + movie.setReleased(doc.getDate("released")); + movie.setRuntime(doc.getInteger("runtime")); + movie.setPoster(doc.getString("poster")); + movie.setGenres((List) doc.get("genres")); + movie.setDirectors((List) doc.get("directors")); + movie.setWriters((List) doc.get("writers")); + movie.setCast((List) doc.get("cast")); + movie.setCountries((List) doc.get("countries")); + movie.setLanguages((List) doc.get("languages")); + movie.setRated(doc.getString("rated")); + movie.setMetacritic(doc.getInteger("metacritic")); + movie.setType(doc.getString("type")); + + Document awardsDoc = (Document) doc.get("awards"); + if (awardsDoc != null) { + movie.setAwards(documentToAwards(awardsDoc)); + } + + Document imdbDoc = (Document) doc.get("imdb"); + if (imdbDoc != null) { + movie.setImdb(documentToImdb(imdbDoc)); + } + + Document tomatoesDoc = (Document) doc.get("tomatoes"); + if (tomatoesDoc != null) { + movie.setTomatoes(documentToTomatoes(tomatoesDoc)); + } + + return movie; + } + + private Document awardsToDocument(Movie.Awards awards) { + Document doc = new Document(); + if (awards.getWins() != null) { + doc.append("wins", awards.getWins()); + } + if (awards.getNominations() != null) { + doc.append("nominations", awards.getNominations()); + } + if (awards.getText() != null) { + doc.append("text", awards.getText()); + } + return doc; + } + + private Movie.Awards documentToAwards(Document doc) { + return Movie.Awards.builder() + .wins(doc.getInteger("wins")) + .nominations(doc.getInteger("nominations")) + .text(doc.getString("text")) + .build(); + } + + private Document imdbToDocument(Movie.Imdb imdb) { + Document doc = new Document(); + if (imdb.getRating() != null) { + doc.append("rating", imdb.getRating()); + } + if (imdb.getVotes() != null) { + doc.append("votes", imdb.getVotes()); + } + if (imdb.getId() != null) { + doc.append("id", imdb.getId()); + } + return doc; + } + + private Movie.Imdb documentToImdb(Document doc) { + return Movie.Imdb.builder() + .rating(doc.getDouble("rating")) + .votes(doc.getInteger("votes")) + .id(doc.getInteger("id")) + .build(); + } + + private Document tomatoesToDocument(Movie.Tomatoes tomatoes) { + Document doc = new Document(); + if (tomatoes.getViewer() != null) { + doc.append("viewer", viewerToDocument(tomatoes.getViewer())); + } + if (tomatoes.getCritic() != null) { + doc.append("critic", criticToDocument(tomatoes.getCritic())); + } + if (tomatoes.getFresh() != null) { + doc.append("fresh", tomatoes.getFresh()); + } + if (tomatoes.getRotten() != null) { + doc.append("rotten", tomatoes.getRotten()); + } + if (tomatoes.getProduction() != null) { + doc.append("production", tomatoes.getProduction()); + } + if (tomatoes.getLastUpdated() != null) { + doc.append("lastUpdated", tomatoes.getLastUpdated()); + } + return doc; + } + + private Movie.Tomatoes documentToTomatoes(Document doc) { + Movie.Tomatoes.Viewer viewer = null; + Document viewerDoc = (Document) doc.get("viewer"); + if (viewerDoc != null) { + viewer = documentToViewer(viewerDoc); + } + + Movie.Tomatoes.Critic critic = null; + Document criticDoc = (Document) doc.get("critic"); + if (criticDoc != null) { + critic = documentToCritic(criticDoc); + } + + return Movie.Tomatoes.builder() + .viewer(viewer) + .critic(critic) + .fresh(doc.getInteger("fresh")) + .rotten(doc.getInteger("rotten")) + .production(doc.getString("production")) + .lastUpdated(doc.getDate("lastUpdated")) + .build(); + } + + private Document viewerToDocument(Movie.Tomatoes.Viewer viewer) { + Document doc = new Document(); + if (viewer.getRating() != null) { + doc.append("rating", viewer.getRating()); + } + if (viewer.getNumReviews() != null) { + doc.append("numReviews", viewer.getNumReviews()); + } + if (viewer.getMeter() != null) { + doc.append("meter", viewer.getMeter()); + } + return doc; + } + + private Movie.Tomatoes.Viewer documentToViewer(Document doc) { + return Movie.Tomatoes.Viewer.builder() + .rating(doc.getDouble("rating")) + .numReviews(doc.getInteger("numReviews")) + .meter(doc.getInteger("meter")) + .build(); + } + + private Document criticToDocument(Movie.Tomatoes.Critic critic) { + Document doc = new Document(); + if (critic.getRating() != null) { + doc.append("rating", critic.getRating()); + } + if (critic.getNumReviews() != null) { + doc.append("numReviews", critic.getNumReviews()); + } + if (critic.getMeter() != null) { + doc.append("meter", critic.getMeter()); + } + return doc; + } + + private Movie.Tomatoes.Critic documentToCritic(Document doc) { + return Movie.Tomatoes.Critic.builder() + .rating(doc.getDouble("rating")) + .numReviews(doc.getInteger("numReviews")) + .meter(doc.getInteger("meter")) + .build(); + } +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 529b20d..4e44003 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -1,25 +1,34 @@ package com.mongodb.samplemflix.service; -import org.springframework.stereotype.Service; +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import org.bson.Document; + +import java.util.List; +import java.util.Map; /** - * Service layer for movie business logic. - * - * This service handles: - * - Business logic and validation - * - Query construction (filters, sorts, pagination) - * - Data transformation between DTOs and entities - * - Error handling and exception throwing - * - * TODO: Phase 5 - Implement all CRUD operations - * TODO: Phase 5 - Add query building logic for filtering - * TODO: Phase 5 - Implement pagination and sorting - * TODO: Phase 5 - Add validation logic + * Service interface for movie business logic. */ -@Service -public class MovieService { - - // TODO: Phase 5 - Inject MovieRepository - // TODO: Phase 5 - Implement service methods -} +public interface MovieService { + + List getAllMovies(MovieSearchQuery query); + + Movie getMovieById(String id); + + Movie createMovie(CreateMovieRequest request); + + Map createMoviesBatch(List requests); + Movie updateMovie(String id, UpdateMovieRequest request); + + Map updateMoviesBatch(Document filter, Document update); + + Map deleteMovie(String id); + + Map deleteMoviesBatch(Document filter); + + Movie findAndDeleteMovie(String id); +} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java new file mode 100644 index 0000000..e049cfe --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -0,0 +1,309 @@ +package com.mongodb.samplemflix.service; + +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertManyResult; +import com.mongodb.client.result.InsertOneResult; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.samplemflix.exception.ResourceNotFoundException; +import com.mongodb.samplemflix.exception.ValidationException; +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import com.mongodb.samplemflix.repository.MovieRepository; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Service layer for movie business logic. + * + * This service handles: + * - Business logic and validation + * - Query construction (filters, sorts, pagination) + * - Data transformation between DTOs and entities + * - Error handling and exception throwing + */ +@Service +public class MovieServiceImpl implements MovieService { + + private final MovieRepository movieRepository; + + public MovieServiceImpl(MovieRepository movieRepository) { + this.movieRepository = movieRepository; + } + + @Override + public List getAllMovies(MovieSearchQuery query) { + Document filter = buildFilter(query); + Document sort = buildSort(query.getSortBy(), query.getSortOrder()); + + int limit = Math.min(Math.max(query.getLimit() != null ? query.getLimit() : 20, 1), 100); + int skip = Math.max(query.getSkip() != null ? query.getSkip() : 0, 0); + + return movieRepository.find(filter, sort, skip, limit); + } + + @Override + public Movie getMovieById(String id) { + if (!ObjectId.isValid(id)) { + throw new ValidationException("Invalid movie ID format"); + } + + return movieRepository.findById(new ObjectId(id)) + .orElseThrow(() -> new ResourceNotFoundException("Movie not found")); + } + + @Override + public Movie createMovie(CreateMovieRequest request) { + if (request.getTitle() == null || request.getTitle().trim().isEmpty()) { + throw new ValidationException("Title is required"); + } + + Movie movie = Movie.builder() + .title(request.getTitle()) + .year(request.getYear()) + .plot(request.getPlot()) + .fullplot(request.getFullplot()) + .genres(request.getGenres()) + .directors(request.getDirectors()) + .writers(request.getWriters()) + .cast(request.getCast()) + .countries(request.getCountries()) + .languages(request.getLanguages()) + .rated(request.getRated()) + .runtime(request.getRuntime()) + .poster(request.getPoster()) + .build(); + + InsertOneResult result = movieRepository.insertOne(movie); + + if (!result.wasAcknowledged()) { + throw new RuntimeException("Movie insertion was not acknowledged by the database"); + } + + return movieRepository.findById(result.getInsertedId().asObjectId().getValue()) + .orElseThrow(() -> new RuntimeException("Failed to retrieve created movie")); + } + + @Override + public Map createMoviesBatch(List requests) { + if (requests == null || requests.isEmpty()) { + throw new ValidationException("Request body must be a non-empty array of movie objects"); + } + + for (int i = 0; i < requests.size(); i++) { + CreateMovieRequest request = requests.get(i); + if (request.getTitle() == null || request.getTitle().trim().isEmpty()) { + throw new ValidationException("Movie at index " + i + ": Title is required"); + } + } + + List movies = requests.stream() + .map(request -> Movie.builder() + .title(request.getTitle()) + .year(request.getYear()) + .plot(request.getPlot()) + .fullplot(request.getFullplot()) + .genres(request.getGenres()) + .directors(request.getDirectors()) + .writers(request.getWriters()) + .cast(request.getCast()) + .countries(request.getCountries()) + .languages(request.getLanguages()) + .rated(request.getRated()) + .runtime(request.getRuntime()) + .poster(request.getPoster()) + .build()) + .toList(); + + InsertManyResult result = movieRepository.insertMany(movies); + + if (!result.wasAcknowledged()) { + throw new RuntimeException("Batch movie insertion was not acknowledged by the database"); + } + + return Map.of( + "insertedCount", result.getInsertedIds().size(), + "insertedIds", result.getInsertedIds().values() + ); + } + + @Override + public Movie updateMovie(String id, UpdateMovieRequest request) { + if (!ObjectId.isValid(id)) { + throw new ValidationException("Invalid movie ID format"); + } + + if (request == null || isUpdateRequestEmpty(request)) { + throw new ValidationException("No update data provided"); + } + + Document update = new Document("$set", buildUpdateDocument(request)); + UpdateResult result = movieRepository.updateOne(new ObjectId(id), update); + + if (result.getMatchedCount() == 0) { + throw new ResourceNotFoundException("Movie not found"); + } + + return movieRepository.findById(new ObjectId(id)) + .orElseThrow(() -> new RuntimeException("Failed to retrieve updated movie")); + } + + @Override + public Map updateMoviesBatch(Document filter, Document update) { + if (filter == null || update == null) { + throw new ValidationException("Both filter and update objects are required"); + } + + if (update.isEmpty()) { + throw new ValidationException("Update object cannot be empty"); + } + + Document setUpdate = new Document("$set", update); + UpdateResult result = movieRepository.updateMany(filter, setUpdate); + + return Map.of( + "matchedCount", result.getMatchedCount(), + "modifiedCount", result.getModifiedCount() + ); + } + + @Override + public Map deleteMovie(String id) { + if (!ObjectId.isValid(id)) { + throw new ValidationException("Invalid movie ID format"); + } + + DeleteResult result = movieRepository.deleteOne(new ObjectId(id)); + + if (result.getDeletedCount() == 0) { + throw new ResourceNotFoundException("Movie not found"); + } + + return Map.of("deletedCount", result.getDeletedCount()); + } + + @Override + public Map deleteMoviesBatch(Document filter) { + if (filter == null || filter.isEmpty()) { + throw new ValidationException("Filter object is required and cannot be empty. This prevents accidental deletion of all documents."); + } + + DeleteResult result = movieRepository.deleteMany(filter); + + return Map.of("deletedCount", result.getDeletedCount()); + } + + @Override + public Movie findAndDeleteMovie(String id) { + if (!ObjectId.isValid(id)) { + throw new ValidationException("Invalid movie ID format"); + } + + return movieRepository.findOneAndDelete(new ObjectId(id)) + .orElseThrow(() -> new ResourceNotFoundException("Movie not found")); + } + + private Document buildFilter(MovieSearchQuery query) { + Document filter = new Document(); + + if (query.getQ() != null && !query.getQ().trim().isEmpty()) { + filter.append("$text", new Document("$search", query.getQ())); + } + + if (query.getGenre() != null && !query.getGenre().trim().isEmpty()) { + filter.append("genres", new Document("$regex", Pattern.compile(query.getGenre(), Pattern.CASE_INSENSITIVE))); + } + + if (query.getYear() != null) { + filter.append("year", query.getYear()); + } + + if (query.getMinRating() != null || query.getMaxRating() != null) { + Document ratingFilter = new Document(); + if (query.getMinRating() != null) { + ratingFilter.append("$gte", query.getMinRating()); + } + if (query.getMaxRating() != null) { + ratingFilter.append("$lte", query.getMaxRating()); + } + filter.append("imdb.rating", ratingFilter); + } + + return filter; + } + + private Document buildSort(String sortBy, String sortOrder) { + String field = sortBy != null && !sortBy.trim().isEmpty() ? sortBy : "title"; + int order = "desc".equalsIgnoreCase(sortOrder) ? -1 : 1; + return new Document(field, order); + } + + private boolean isUpdateRequestEmpty(UpdateMovieRequest request) { + return request.getTitle() == null && + request.getYear() == null && + request.getPlot() == null && + request.getFullplot() == null && + request.getGenres() == null && + request.getDirectors() == null && + request.getWriters() == null && + request.getCast() == null && + request.getCountries() == null && + request.getLanguages() == null && + request.getRated() == null && + request.getRuntime() == null && + request.getPoster() == null; + } + + private Document buildUpdateDocument(UpdateMovieRequest request) { + Document doc = new Document(); + + if (request.getTitle() != null) { + doc.append("title", request.getTitle()); + } + if (request.getYear() != null) { + doc.append("year", request.getYear()); + } + if (request.getPlot() != null) { + doc.append("plot", request.getPlot()); + } + if (request.getFullplot() != null) { + doc.append("fullplot", request.getFullplot()); + } + if (request.getGenres() != null) { + doc.append("genres", request.getGenres()); + } + if (request.getDirectors() != null) { + doc.append("directors", request.getDirectors()); + } + if (request.getWriters() != null) { + doc.append("writers", request.getWriters()); + } + if (request.getCast() != null) { + doc.append("cast", request.getCast()); + } + if (request.getCountries() != null) { + doc.append("countries", request.getCountries()); + } + if (request.getLanguages() != null) { + doc.append("languages", request.getLanguages()); + } + if (request.getRated() != null) { + doc.append("rated", request.getRated()); + } + if (request.getRuntime() != null) { + doc.append("runtime", request.getRuntime()); + } + if (request.getPoster() != null) { + doc.append("poster", request.getPoster()); + } + + return doc; + } +} + diff --git a/server/java-spring/src/main/resources/application.properties b/server/java-spring/src/main/resources/application.properties index 276a15b..46705ba 100644 --- a/server/java-spring/src/main/resources/application.properties +++ b/server/java-spring/src/main/resources/application.properties @@ -1,10 +1,10 @@ # MongoDB Configuration -# Connection URI should be provided via MONGODB_URI environment variable +# Connection URI should be provided with the MONGODB_URI environment variable spring.data.mongodb.uri=${MONGODB_URI} spring.data.mongodb.database=sample_mflix # Server Configuration -# Default port is 3001, can be overridden via PORT environment variable +# Default port is 3001, can be overridden with the PORT environment variable server.port=${PORT:3001} # CORS Configuration @@ -26,4 +26,3 @@ spring.jackson.serialization.write-dates-as-timestamps=false springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html springdoc.swagger-ui.operationsSorter=method - From dd8fe3a2c1fdd6b790cc9db93424a4fa277d822e Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 15:52:35 -0400 Subject: [PATCH 012/110] fix: remove unneeded files and update .gitignore and .gitattributes --- .gitattributes | 8 + PR_DESCRIPTION.md | 274 -- client/next-env.d.ts | 6 - client/package-lock.json | 5007 ------------------------------ server/express/.gitignore | 4 +- server/express/package-lock.json | 4748 ---------------------------- 6 files changed, 10 insertions(+), 10037 deletions(-) create mode 100644 .gitattributes delete mode 100644 PR_DESCRIPTION.md delete mode 100644 client/next-env.d.ts delete mode 100644 client/package-lock.json delete mode 100644 server/express/package-lock.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f0749cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Git Attributes for docs-sample-apps +# Marks auto-generated files that are committed to version control +# so they're collapsed in GitHub PR diffs and excluded from code review + +# Maven Wrapper - Auto-generated by Apache Maven (committed to git) +# See: https://maven.apache.org/wrapper/ +server/java-spring/mvnw linguist-generated=true +server/java-spring/mvnw.cmd linguist-generated=true diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md deleted file mode 100644 index 2d46f00..0000000 --- a/PR_DESCRIPTION.md +++ /dev/null @@ -1,274 +0,0 @@ -# Java Spring Backend Implementation - Phases 2 & 3 - -## 📋 Overview - -This PR implements **Phase 2 (Database Configuration)** and **Phase 3 (Model Layer)** of the Java Spring Boot backend for the MongoDB Sample MFlix application. This backend will provide feature parity with the existing Express.js/TypeScript backend while demonstrating direct MongoDB Java Driver usage. - -**Branch:** `java-scaffolding-setup` -**Base:** `main` - ---- - -## ✅ What's Completed - -### Phase 2: Database Configuration and Connection ✅ - -#### MongoDB Configuration (`MongoConfig.java`) -- ✅ **Connection pooling** with production-ready settings: - - Max pool size: 100 connections - - Min pool size: 10 connections - - Max connection idle time: 60 seconds - - Max wait time: 10 seconds -- ✅ **Socket timeouts** to prevent hanging connections: - - Connect timeout: 10 seconds - - Read timeout: 10 seconds -- ✅ **Server selection timeout**: 10 seconds -- ✅ **Connection string validation** before attempting connection -- ✅ **Graceful shutdown** managed by Spring's bean lifecycle -- ✅ Singleton `MongoClient` and `MongoDatabase` beans - -#### Database Verification (`DatabaseVerification.java`) -- ✅ **Startup verification** via `@PostConstruct` -- ✅ **Collection verification**: Checks if movies collection exists and contains data -- ✅ **Text search index creation**: Creates compound text index on `plot`, `title`, and `fullplot` fields -- ✅ **Background index creation** to avoid blocking operations -- ✅ **Non-blocking verification**: Application starts even if verification fails -- ✅ **Comprehensive logging** with helpful error messages and links to MongoDB documentation - -### Phase 3: Model Layer Implementation ✅ - -#### Domain Models (3 classes) - -**1. Movie.java** (270 lines) -- ✅ All fields from TypeScript `Movie` interface -- ✅ Nested classes: - - `Awards` (wins, nominations, text) - - `Imdb` (rating, votes, id) - - `Tomatoes` (viewer, critic, fresh, rotten, production, lastUpdated) - - `Tomatoes.Viewer` (rating, numReviews, meter) - - `Tomatoes.Critic` (rating, numReviews, meter) -- ✅ Lombok annotations for reduced boilerplate -- ✅ Comprehensive JavaDoc documentation - -**2. Theater.java** (103 lines) -- ✅ All fields from TypeScript `Theater` interface -- ✅ Nested classes: - - `Location` (address, geo) - - `Location.Address` (street1, city, state, zipcode) - - `Location.Geo` (type, coordinates in GeoJSON format) -- ✅ GeoJSON format support for geospatial queries - -**3. Comment.java** (56 lines) -- ✅ All fields from TypeScript `Comment` interface -- ✅ Fields: id, name, email, movieId, text, date - -#### DTOs - Data Transfer Objects (3 classes) - -**1. CreateMovieRequest.java** (92 lines) -- ✅ Required field: `title` with `@NotBlank` validation -- ✅ Optional fields: year, plot, fullplot, genres, directors, writers, cast, countries, languages, rated, runtime, poster -- ✅ Matches Express backend `CreateMovieRequest` interface - -**2. UpdateMovieRequest.java** (89 lines) -- ✅ All fields optional (for partial updates) -- ✅ Same fields as `CreateMovieRequest` - -**3. MovieSearchQuery.java** (64 lines) -- ✅ Full-text search: `q` parameter -- ✅ Filters: genre, year, minRating, maxRating -- ✅ Pagination: limit, skip -- ✅ Sorting: sortBy, sortOrder - -#### Response Models (3 classes + 1 interface) - -**1. ApiResponse Interface** (30 lines) -- ✅ Common interface for all API responses -- ✅ Methods: `isSuccess()`, `getTimestamp()` - -**2. SuccessResponse** (88 lines) -- ✅ Generic type parameter for flexible data responses -- ✅ Fields: success (true), message, data, timestamp, pagination -- ✅ Nested class: `Pagination` (page, limit, total, pages) -- ✅ `@JsonInclude(NON_NULL)` to exclude null fields from JSON -- ✅ Auto-generated timestamp using `Instant.now()` - -**3. ErrorResponse** (80 lines) -- ✅ Fields: success (false), message, error, timestamp -- ✅ Nested class: `ErrorDetails` (message, code, details) -- ✅ Matches Express backend error format - ---- - -## 📁 Files Changed - -### Modified Files (8) -- `server/java-spring/pom.xml` - Updated dependencies -- `server/java-spring/.mvn/wrapper/maven-wrapper.properties` - Maven wrapper config -- `server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java` - **Phase 2** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java` - **Phase 3** - -### New Files (4) -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java` - **Phase 3** -- `server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java` - **Phase 3** - -### Not Staged (Documentation) -- `JAVA-SPRING-IMPLEMENTATION-PLAN.md` - Updated with Phase 2 & 3 completion status - ---- - -## 🔍 What to Review - -### 1. Database Configuration (`config/`) -**Focus Areas:** -- [ ] Connection pool settings are appropriate for production use -- [ ] Timeout values (10s) are reasonable -- [ ] Error handling in `DatabaseVerification` is robust -- [ ] Text index creation matches Express backend behavior -- [ ] Logging messages are helpful and informative - -**Questions for Reviewers:** -- Are the connection pool settings (max 100, min 10) appropriate? -- Should we add more comprehensive index verification (check if index exists before creating)? - -### 2. Domain Models (`model/`) -**Focus Areas:** -- [ ] All fields match the TypeScript interfaces from Express backend -- [ ] Nested class structure is clean and maintainable -- [ ] Lombok annotations are used appropriately -- [ ] JavaDoc documentation is comprehensive -- [ ] Field types are correct (Integer vs int, Date vs Instant, etc.) - -**Questions for Reviewers:** -- Should we use `Instant` instead of `Date` for date fields? -- Are the nested classes appropriately structured, or should some be top-level classes? - -### 3. DTOs (`model/dto/`) -**Focus Areas:** -- [ ] Validation annotations are appropriate (`@NotBlank` on title) -- [ ] All optional fields are correctly marked -- [ ] DTOs match Express backend request structures -- [ ] Field naming conventions are consistent - -**Questions for Reviewers:** -- Should we add more validation annotations (e.g., `@Min`, `@Max` for year/rating)? -- Should `MovieSearchQuery` have default values for limit/skip? - -### 4. Response Models (`model/response/`) -**Focus Areas:** -- [ ] Generic type usage in `SuccessResponse` is correct -- [ ] `@JsonInclude(NON_NULL)` behavior is desired -- [ ] Timestamp format (ISO 8601) matches Express backend -- [ ] Error response structure matches Express backend -- [ ] Builder pattern with defaults works as expected - -**Questions for Reviewers:** -- Should we use a custom timestamp format instead of `Instant.now().toString()`? -- Is the `ErrorDetails` nested class structure appropriate? - ---- - -## 🚧 What's NOT in This PR (Pending Work) - -### Phase 4: Repository Layer (Next PR) -- [ ] `MovieRepository` interface -- [ ] `MovieRepositoryImpl` using `MongoCollection` directly -- [ ] Manual BSON Document ↔ Movie object conversion -- [ ] All CRUD operations: insertOne, insertMany, findById, find, updateOne, updateMany, deleteOne, deleteMany, findOneAndDelete - -### Phase 5: Service Layer (Future PR) -- [ ] `MovieService` with business logic -- [ ] Request validation -- [ ] DTO ↔ Domain model conversion -- [ ] Query building for search/filter operations - -### Phase 6: Controller Layer (Future PR) -- [ ] `MovieController` with REST endpoints -- [ ] Request/response mapping -- [ ] Exception handling - -### Phase 7-9: Testing, Error Handling, Documentation (Future PRs) -- [ ] Unit tests -- [ ] Integration tests -- [ ] Global exception handler -- [ ] API documentation (Swagger/OpenAPI) -- [ ] README updates - ---- - -## ✅ Build Verification - -```bash -$ cd server/java-spring && ./mvnw clean compile -[INFO] BUILD SUCCESS -[INFO] Compiling 20 source files -``` - -All files compile successfully with no errors or warnings. - ---- - -## 🎯 Design Decisions - -### Why Lombok? -- **Reduces boilerplate**: No need to write getters, setters, constructors, toString, equals, hashCode -- **Improves readability**: Focus on business logic, not boilerplate -- **Maintainability**: Changes to fields automatically update generated methods -- **Industry standard**: Widely used in Spring Boot projects - -### Why Custom Repository (Not Spring Data)? -- **Educational value**: Demonstrates direct MongoDB Driver usage -- **Matches Express backend**: Similar to how Node.js driver is used -- **Full control**: Complete control over BSON document structure and queries -- **Explicit operations**: Shows exactly what MongoDB operations are being performed - -### Why Nested Classes? -- **Encapsulation**: Nested classes are only used within their parent context -- **Namespace clarity**: `Movie.Awards` is clearer than a separate `MovieAwards` class -- **Matches MongoDB structure**: Reflects the nested document structure in MongoDB - ---- - -## 📚 References - -- **Implementation Plan**: `JAVA-SPRING-IMPLEMENTATION-PLAN.md` -- **Express Backend**: `server/express/src/` (reference implementation) -- **MongoDB Java Driver Docs**: https://www.mongodb.com/docs/drivers/java/sync/current/ -- **Spring Boot Docs**: https://docs.spring.io/spring-boot/docs/current/reference/html/ - ---- - -## 🤔 Questions for Discussion - -1. **Date Types**: Should we use `java.time.Instant` instead of `java.util.Date` for better Java 8+ compatibility? -2. **Validation**: Should we add more comprehensive validation annotations on DTOs? -3. **Index Management**: Should we check if indexes exist before creating them, or rely on MongoDB's idempotent behavior? -4. **Error Messages**: Are the error messages and logging levels appropriate? -5. **Pagination Defaults**: Should `MovieSearchQuery` have default values (e.g., limit=20, skip=0)? - ---- - -## 📝 Reviewer Checklist - -- [ ] Code compiles without errors -- [ ] All models match Express backend TypeScript interfaces -- [ ] Lombok annotations are used appropriately -- [ ] JavaDoc documentation is comprehensive -- [ ] Connection pool settings are production-ready -- [ ] Database verification logic is sound -- [ ] Response models match Express backend format -- [ ] Validation annotations are appropriate -- [ ] No security vulnerabilities introduced -- [ ] Code follows Java/Spring Boot best practices - ---- - -**Ready for Review** ✅ - -This PR lays the foundation for the Java Spring backend by implementing database configuration and all model classes. The next PR will implement the Repository layer with direct MongoDB Driver usage. - diff --git a/client/next-env.d.ts b/client/next-env.d.ts deleted file mode 100644 index 830fb59..0000000 --- a/client/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/client/package-lock.json b/client/package-lock.json deleted file mode 100644 index 498a11c..0000000 --- a/client/package-lock.json +++ /dev/null @@ -1,5007 +0,0 @@ -{ - "name": "sample-mflix-front-end", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sample-mflix-front-end", - "version": "0.1.0", - "dependencies": { - "next": "15.5.5", - "react": "19.1.0", - "react-dom": "19.1.0" - }, - "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.5.5", - "typescript": "^5" - } - }, - "node_modules/@emnapi/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", - "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.16.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.5.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@next/env": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz", - "integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.5.tgz", - "integrity": "sha512-FMzm412l9oFB8zdRD+K6HQ1VzlS+sNNsdg0MfvTg0i8lfCyTgP/RFxiu/pGJqZ/IQnzn9xSiLkjOVI7Iv4nbdQ==", - "dev": true, - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz", - "integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz", - "integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz", - "integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz", - "integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz", - "integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz", - "integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz", - "integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz", - "integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz", - "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==", - "dev": true - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.19.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", - "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", - "dev": true, - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/type-utils": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.46.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.5.tgz", - "integrity": "sha512-f8lRSSelp6cqrYjxEMjJ5En3WV913gTu/w9goYShnIujwDSQlKt4x9MwSDiduE9R5mmFETK44+qlQDxeSA0rUA==", - "dev": true, - "dependencies": { - "@next/eslint-plugin-next": "15.5.5", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/next": { - "version": "15.5.5", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz", - "integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==", - "dependencies": { - "@next/env": "15.5.5", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.5", - "@next/swc-darwin-x64": "15.5.5", - "@next/swc-linux-arm64-gnu": "15.5.5", - "@next/swc-linux-arm64-musl": "15.5.5", - "@next/swc-linux-x64-gnu": "15.5.5", - "@next/swc-linux-x64-musl": "15.5.5", - "@next/swc-win32-arm64-msvc": "15.5.5", - "@next/swc-win32-x64-msvc": "15.5.5", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "devOptional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/server/express/.gitignore b/server/express/.gitignore index 8fa0429..d0ebe3f 100644 --- a/server/express/.gitignore +++ b/server/express/.gitignore @@ -12,5 +12,5 @@ build/ # TypeScript *.tsbuildinfo -# Package Lock (optional - remove this line if you want to commit package-lock.json) -# package-lock.json +# Package Lock +package-lock.json diff --git a/server/express/package-lock.json b/server/express/package-lock.json deleted file mode 100644 index c9b2554..0000000 --- a/server/express/package-lock.json +++ /dev/null @@ -1,4748 +0,0 @@ -{ - "name": "sample-mflix-express-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sample-mflix-express-backend", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "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", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "typescript": "^5.3.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", - "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.19.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz", - "integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", - "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", - "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", - "dev": true, - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.235", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", - "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", - "dev": true - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ] - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-jest": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", - "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", - "dev": true, - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.3", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} From 7ea6b1ed74d6bf0b408b975a3f6d5a7eef8f0270 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 16:57:12 -0400 Subject: [PATCH 013/110] fix: resolve module access issue with driver v5.6 --- server/java-spring/README.md | 2 ++ server/java-spring/pom.xml | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 20c2f87..c0db54c 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -69,6 +69,8 @@ PORT=3001 CORS_ORIGIN=http://localhost:3000 ``` +> **Note**: This project uses [spring-dotenv](https://github.com/paulschwarz/spring-dotenv) to automatically load `.env` files, similar to Node.js applications. The `.env` file will be loaded automatically when you run the application. + ### 3. Load Sample Data If you haven't already, load the `sample_mflix` database into your MongoDB instance: diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index e5dcafc..25bdd6e 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -20,8 +20,9 @@ 17 - 5.6.1 + 5.6.0 2.8.13 + 4.0.0 @@ -43,14 +44,33 @@ mongodb-driver-sync ${mongodb.driver.version} - + + + + org.mongodb + bson + ${mongodb.driver.version} + + + org.mongodb + mongodb-driver-core + ${mongodb.driver.version} + + + + + me.paulschwarz + spring-dotenv + ${dotenv.version} + + org.projectlombok lombok true - + org.apache.commons commons-lang3 From 462d71fcdb2be0ece7f2bdf23d61dacacbd671f2 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:11:51 -0400 Subject: [PATCH 014/110] refactor(RuntimeException): Add new DatabaseOperationException.java custom exception --- server/java-spring/pom.xml | 2 +- .../exception/DatabaseOperationException.java | 20 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 18 +++++++++++++++++ .../samplemflix/service/MovieServiceImpl.java | 18 ++++++++--------- 4 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/exception/DatabaseOperationException.java diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index 25bdd6e..ed33e67 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -70,7 +70,7 @@ lombok true - + org.apache.commons commons-lang3 diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/DatabaseOperationException.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/DatabaseOperationException.java new file mode 100644 index 0000000..44cfd52 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/DatabaseOperationException.java @@ -0,0 +1,20 @@ +package com.mongodb.samplemflix.exception; + +/** + * Exception thrown when a database operation fails unexpectedly. + * + * This exception results in a 500 Internal Server Error response. + * Used for cases where database operations are not acknowledged or + * fail to complete as expected. + */ +public class DatabaseOperationException extends RuntimeException { + + public DatabaseOperationException(String message) { + super(message); + } + + public DatabaseOperationException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java index fc344ea..c128981 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java @@ -59,6 +59,24 @@ public ResponseEntity handleValidationException( return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(DatabaseOperationException.class) + public ResponseEntity handleDatabaseOperationException( + DatabaseOperationException ex, WebRequest request) { + logger.error("Database operation error: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message("Database operation failed") + .error(ErrorResponse.ErrorDetails.builder() + .message(ex.getMessage()) + .code("DATABASE_OPERATION_ERROR") + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + @ExceptionHandler(MongoWriteException.class) public ResponseEntity handleMongoWriteException( MongoWriteException ex, WebRequest request) { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index e049cfe..9ac2d17 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -4,6 +4,7 @@ import com.mongodb.client.result.InsertManyResult; import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; +import com.mongodb.samplemflix.exception.DatabaseOperationException; import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; @@ -81,13 +82,13 @@ public Movie createMovie(CreateMovieRequest request) { .build(); InsertOneResult result = movieRepository.insertOne(movie); - + if (!result.wasAcknowledged()) { - throw new RuntimeException("Movie insertion was not acknowledged by the database"); + throw new DatabaseOperationException("Movie insertion was not acknowledged by the database"); } - + return movieRepository.findById(result.getInsertedId().asObjectId().getValue()) - .orElseThrow(() -> new RuntimeException("Failed to retrieve created movie")); + .orElseThrow(() -> new DatabaseOperationException("Failed to retrieve created movie")); } @Override @@ -122,9 +123,9 @@ public Map createMoviesBatch(List requests) .toList(); InsertManyResult result = movieRepository.insertMany(movies); - + if (!result.wasAcknowledged()) { - throw new RuntimeException("Batch movie insertion was not acknowledged by the database"); + throw new DatabaseOperationException("Batch movie insertion was not acknowledged by the database"); } return Map.of( @@ -149,9 +150,9 @@ public Movie updateMovie(String id, UpdateMovieRequest request) { if (result.getMatchedCount() == 0) { throw new ResourceNotFoundException("Movie not found"); } - + return movieRepository.findById(new ObjectId(id)) - .orElseThrow(() -> new RuntimeException("Failed to retrieve updated movie")); + .orElseThrow(() -> new DatabaseOperationException("Failed to retrieve updated movie")); } @Override @@ -306,4 +307,3 @@ private Document buildUpdateDocument(UpdateMovieRequest request) { return doc; } } - From 170c3c54549594d4f06fd2af29ec3024386bf54e Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:17:55 -0400 Subject: [PATCH 015/110] refactor(pojo): apply patch and use POJO codec --- .../samplemflix/config/MongoConfig.java | 18 ++ .../repository/MovieRepositoryImpl.java | 289 +----------------- 2 files changed, 34 insertions(+), 273 deletions(-) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index 3afa3a4..ba0c6d6 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -5,12 +5,17 @@ import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.PojoCodecProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; + /** * MongoDB configuration class for the Sample MFlix application. *

@@ -64,12 +69,25 @@ public MongoClient mongoClient() { ); } + // Create the POJO codec provider, enabling automatic POJO discovery + CodecRegistry pojoCodecRegistry = fromProviders( + PojoCodecProvider.builder().automatic(true).build() + ); + + // Combine the default registry with your new POJO registry + // IMPORTANT: The default registry must come FIRST. + CodecRegistry registry = fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), + pojoCodecRegistry + ); + // Parse and validate the connection string ConnectionString connectionString = new ConnectionString(mongoUri); // Build client settings with connection pooling and timeouts // These settings optimize for both performance and resource management MongoClientSettings settings = MongoClientSettings.builder() + .codecRegistry(registry) .applyConnectionString(connectionString) // Configure connection pool for optimal performance .applyToConnectionPoolSettings(builder -> diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java index a8ccc16..6f0c438 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java @@ -15,45 +15,39 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; /** * Implementation of MovieRepository using MongoDB Java Driver directly. - * - * This class demonstrates direct usage of MongoCollection for CRUD operations. - * It manually converts between Movie objects and BSON Documents to provide full control - * over the MongoDB operations. - * + * + * This class demonstrates direct usage of MongoCollection for CRUD operations. + * It uses MongoDB's POJO codec to automatically convert between Movie objects and BSON Documents. + * * This approach is used for educational purposes to show how the MongoDB driver works, * rather than using Spring Data MongoDB which abstracts these details. */ @Repository public class MovieRepositoryImpl implements MovieRepository { - - private final MongoCollection moviesCollection; - + + private final MongoCollection moviesCollection; + public MovieRepositoryImpl(MongoDatabase mongoDatabase) { - this.moviesCollection = mongoDatabase.getCollection("movies"); + this.moviesCollection = mongoDatabase.getCollection("movies", Movie.class); } @Override public InsertOneResult insertOne(Movie movie) { - Document doc = movieToDocument(movie); - return moviesCollection.insertOne(doc); + return moviesCollection.insertOne(movie); } @Override public InsertManyResult insertMany(List movies) { - List documents = movies.stream() - .map(this::movieToDocument) - .collect(Collectors.toList()); - return moviesCollection.insertMany(documents); + return moviesCollection.insertMany(movies); } @Override public Optional findById(ObjectId id) { - Document doc = moviesCollection.find(Filters.eq("_id", id)).first(); - return Optional.ofNullable(doc).map(this::documentToMovie); + Movie doc = moviesCollection.find(Filters.eq("_id", id)).first(); + return Optional.ofNullable(doc); } @Override @@ -63,7 +57,8 @@ public List find(Document filter, Document sort, int skip, int limit) { .sort(sort) .skip(skip) .limit(limit) - .forEach(doc -> movies.add(documentToMovie(doc))); + .into(movies); + return movies; } @@ -89,265 +84,13 @@ public DeleteResult deleteMany(Document filter) { @Override public Optional findOneAndDelete(ObjectId id) { - Document doc = moviesCollection.findOneAndDelete(Filters.eq("_id", id)); - return Optional.ofNullable(doc).map(this::documentToMovie); + Movie doc = moviesCollection.findOneAndDelete(Filters.eq("_id", id)); + return Optional.ofNullable(doc); } @Override public long countDocuments() { return moviesCollection.countDocuments(); } - - /** - * Converts a Movie object to a BSON Document. - * - * @param movie the Movie object - * @return the BSON Document - */ - private Document movieToDocument(Movie movie) { - Document doc = new Document(); - - if (movie.getId() != null) { - doc.append("_id", movie.getId()); - } - if (movie.getTitle() != null) { - doc.append("title", movie.getTitle()); - } - if (movie.getYear() != null) { - doc.append("year", movie.getYear()); - } - if (movie.getPlot() != null) { - doc.append("plot", movie.getPlot()); - } - if (movie.getFullplot() != null) { - doc.append("fullplot", movie.getFullplot()); - } - if (movie.getReleased() != null) { - doc.append("released", movie.getReleased()); - } - if (movie.getRuntime() != null) { - doc.append("runtime", movie.getRuntime()); - } - if (movie.getPoster() != null) { - doc.append("poster", movie.getPoster()); - } - if (movie.getGenres() != null) { - doc.append("genres", movie.getGenres()); - } - if (movie.getDirectors() != null) { - doc.append("directors", movie.getDirectors()); - } - if (movie.getWriters() != null) { - doc.append("writers", movie.getWriters()); - } - if (movie.getCast() != null) { - doc.append("cast", movie.getCast()); - } - if (movie.getCountries() != null) { - doc.append("countries", movie.getCountries()); - } - if (movie.getLanguages() != null) { - doc.append("languages", movie.getLanguages()); - } - if (movie.getRated() != null) { - doc.append("rated", movie.getRated()); - } - if (movie.getAwards() != null) { - doc.append("awards", awardsToDocument(movie.getAwards())); - } - if (movie.getImdb() != null) { - doc.append("imdb", imdbToDocument(movie.getImdb())); - } - if (movie.getTomatoes() != null) { - doc.append("tomatoes", tomatoesToDocument(movie.getTomatoes())); - } - if (movie.getMetacritic() != null) { - doc.append("metacritic", movie.getMetacritic()); - } - if (movie.getType() != null) { - doc.append("type", movie.getType()); - } - - return doc; - } - - /** - * Converts a BSON Document to a Movie object. - * - * @param doc the BSON Document - * @return the Movie object - */ - @SuppressWarnings("unchecked") - private Movie documentToMovie(Document doc) { - Movie movie = new Movie(); - - movie.setId(doc.getObjectId("_id")); - movie.setTitle(doc.getString("title")); - movie.setYear(doc.getInteger("year")); - movie.setPlot(doc.getString("plot")); - movie.setFullplot(doc.getString("fullplot")); - movie.setReleased(doc.getDate("released")); - movie.setRuntime(doc.getInteger("runtime")); - movie.setPoster(doc.getString("poster")); - movie.setGenres((List) doc.get("genres")); - movie.setDirectors((List) doc.get("directors")); - movie.setWriters((List) doc.get("writers")); - movie.setCast((List) doc.get("cast")); - movie.setCountries((List) doc.get("countries")); - movie.setLanguages((List) doc.get("languages")); - movie.setRated(doc.getString("rated")); - movie.setMetacritic(doc.getInteger("metacritic")); - movie.setType(doc.getString("type")); - - Document awardsDoc = (Document) doc.get("awards"); - if (awardsDoc != null) { - movie.setAwards(documentToAwards(awardsDoc)); - } - - Document imdbDoc = (Document) doc.get("imdb"); - if (imdbDoc != null) { - movie.setImdb(documentToImdb(imdbDoc)); - } - - Document tomatoesDoc = (Document) doc.get("tomatoes"); - if (tomatoesDoc != null) { - movie.setTomatoes(documentToTomatoes(tomatoesDoc)); - } - - return movie; - } - - private Document awardsToDocument(Movie.Awards awards) { - Document doc = new Document(); - if (awards.getWins() != null) { - doc.append("wins", awards.getWins()); - } - if (awards.getNominations() != null) { - doc.append("nominations", awards.getNominations()); - } - if (awards.getText() != null) { - doc.append("text", awards.getText()); - } - return doc; - } - - private Movie.Awards documentToAwards(Document doc) { - return Movie.Awards.builder() - .wins(doc.getInteger("wins")) - .nominations(doc.getInteger("nominations")) - .text(doc.getString("text")) - .build(); - } - - private Document imdbToDocument(Movie.Imdb imdb) { - Document doc = new Document(); - if (imdb.getRating() != null) { - doc.append("rating", imdb.getRating()); - } - if (imdb.getVotes() != null) { - doc.append("votes", imdb.getVotes()); - } - if (imdb.getId() != null) { - doc.append("id", imdb.getId()); - } - return doc; - } - - private Movie.Imdb documentToImdb(Document doc) { - return Movie.Imdb.builder() - .rating(doc.getDouble("rating")) - .votes(doc.getInteger("votes")) - .id(doc.getInteger("id")) - .build(); - } - - private Document tomatoesToDocument(Movie.Tomatoes tomatoes) { - Document doc = new Document(); - if (tomatoes.getViewer() != null) { - doc.append("viewer", viewerToDocument(tomatoes.getViewer())); - } - if (tomatoes.getCritic() != null) { - doc.append("critic", criticToDocument(tomatoes.getCritic())); - } - if (tomatoes.getFresh() != null) { - doc.append("fresh", tomatoes.getFresh()); - } - if (tomatoes.getRotten() != null) { - doc.append("rotten", tomatoes.getRotten()); - } - if (tomatoes.getProduction() != null) { - doc.append("production", tomatoes.getProduction()); - } - if (tomatoes.getLastUpdated() != null) { - doc.append("lastUpdated", tomatoes.getLastUpdated()); - } - return doc; - } - - private Movie.Tomatoes documentToTomatoes(Document doc) { - Movie.Tomatoes.Viewer viewer = null; - Document viewerDoc = (Document) doc.get("viewer"); - if (viewerDoc != null) { - viewer = documentToViewer(viewerDoc); - } - - Movie.Tomatoes.Critic critic = null; - Document criticDoc = (Document) doc.get("critic"); - if (criticDoc != null) { - critic = documentToCritic(criticDoc); - } - - return Movie.Tomatoes.builder() - .viewer(viewer) - .critic(critic) - .fresh(doc.getInteger("fresh")) - .rotten(doc.getInteger("rotten")) - .production(doc.getString("production")) - .lastUpdated(doc.getDate("lastUpdated")) - .build(); - } - private Document viewerToDocument(Movie.Tomatoes.Viewer viewer) { - Document doc = new Document(); - if (viewer.getRating() != null) { - doc.append("rating", viewer.getRating()); - } - if (viewer.getNumReviews() != null) { - doc.append("numReviews", viewer.getNumReviews()); - } - if (viewer.getMeter() != null) { - doc.append("meter", viewer.getMeter()); - } - return doc; - } - - private Movie.Tomatoes.Viewer documentToViewer(Document doc) { - return Movie.Tomatoes.Viewer.builder() - .rating(doc.getDouble("rating")) - .numReviews(doc.getInteger("numReviews")) - .meter(doc.getInteger("meter")) - .build(); - } - - private Document criticToDocument(Movie.Tomatoes.Critic critic) { - Document doc = new Document(); - if (critic.getRating() != null) { - doc.append("rating", critic.getRating()); - } - if (critic.getNumReviews() != null) { - doc.append("numReviews", critic.getNumReviews()); - } - if (critic.getMeter() != null) { - doc.append("meter", critic.getMeter()); - } - return doc; - } - - private Movie.Tomatoes.Critic documentToCritic(Document doc) { - return Movie.Tomatoes.Critic.builder() - .rating(doc.getDouble("rating")) - .numReviews(doc.getInteger("numReviews")) - .meter(doc.getInteger("meter")) - .build(); - } } From 2ea3ff0948d3e97033006470963590197fac9140 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:21:38 -0400 Subject: [PATCH 016/110] refactor(serializer): Apply patch and use ObjectMapper --- .../config/ObjectIdSerializer.java | 25 +++++++ .../config/ObjectMapperConfig.java | 39 ++++++++++ .../samplemflix/service/MovieServiceImpl.java | 72 ++++--------------- 3 files changed, 77 insertions(+), 59 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java new file mode 100644 index 0000000..56c2649 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java @@ -0,0 +1,25 @@ +package com.mongodb.samplemflix.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import org.bson.types.ObjectId; + +// ObjectId is a MognoDB GUID Class, an efficient 12 byte UUID starting with a +// timestamp for efficnent indexing - we need to teach Jackson how to convert it +// to JSON nicely + +public class ObjectIdSerializer extends StdSerializer { + + public ObjectIdSerializer() { + super(ObjectId.class); + } + + @Override + public void serialize(ObjectId value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeString(value.toHexString()); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java new file mode 100644 index 0000000..ba55e71 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java @@ -0,0 +1,39 @@ +package com.mongodb.samplemflix.config; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.bson.types.ObjectId; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; + +@Configuration +public class ObjectMapperConfig { + + @Bean + public ObjectMapper objectMapper(JsonFactory jsonFactory) { + ObjectMapper mapper = + new ObjectMapper(jsonFactory) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .registerModule(new JavaTimeModule()); + SimpleModule module = new SimpleModule(); + module.addSerializer(ObjectId.class, new ObjectIdSerializer()); + mapper.registerModule(module); + return mapper; + } + + @Bean + public JsonFactory jsonFactory() { + return new JsonFactory(); + } + + @Bean + public DataBufferFactory dataBufferFactory() { + return new DefaultDataBufferFactory(); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 9ac2d17..88a724d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -3,6 +3,7 @@ import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.InsertManyResult; import com.mongodb.client.result.InsertOneResult; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mongodb.client.result.UpdateResult; import com.mongodb.samplemflix.exception.DatabaseOperationException; import com.mongodb.samplemflix.exception.ResourceNotFoundException; @@ -31,11 +32,13 @@ */ @Service public class MovieServiceImpl implements MovieService { - + private final MovieRepository movieRepository; - - public MovieServiceImpl(MovieRepository movieRepository) { + private final ObjectMapper objectMapper; + + public MovieServiceImpl(MovieRepository movieRepository, ObjectMapper objectMapper) { this.movieRepository = movieRepository; + this.objectMapper = objectMapper; } @Override @@ -246,64 +249,15 @@ private Document buildSort(String sortBy, String sortOrder) { } private boolean isUpdateRequestEmpty(UpdateMovieRequest request) { - return request.getTitle() == null && - request.getYear() == null && - request.getPlot() == null && - request.getFullplot() == null && - request.getGenres() == null && - request.getDirectors() == null && - request.getWriters() == null && - request.getCast() == null && - request.getCountries() == null && - request.getLanguages() == null && - request.getRated() == null && - request.getRuntime() == null && - request.getPoster() == null; + @SuppressWarnings("unchecked") + Map requestMap = objectMapper.convertValue(request, Map.class); + return requestMap.values().stream().allMatch(java.util.Objects::isNull); } private Document buildUpdateDocument(UpdateMovieRequest request) { - Document doc = new Document(); - - if (request.getTitle() != null) { - doc.append("title", request.getTitle()); - } - if (request.getYear() != null) { - doc.append("year", request.getYear()); - } - if (request.getPlot() != null) { - doc.append("plot", request.getPlot()); - } - if (request.getFullplot() != null) { - doc.append("fullplot", request.getFullplot()); - } - if (request.getGenres() != null) { - doc.append("genres", request.getGenres()); - } - if (request.getDirectors() != null) { - doc.append("directors", request.getDirectors()); - } - if (request.getWriters() != null) { - doc.append("writers", request.getWriters()); - } - if (request.getCast() != null) { - doc.append("cast", request.getCast()); - } - if (request.getCountries() != null) { - doc.append("countries", request.getCountries()); - } - if (request.getLanguages() != null) { - doc.append("languages", request.getLanguages()); - } - if (request.getRated() != null) { - doc.append("rated", request.getRated()); - } - if (request.getRuntime() != null) { - doc.append("runtime", request.getRuntime()); - } - if (request.getPoster() != null) { - doc.append("poster", request.getPoster()); - } - - return doc; + @SuppressWarnings("unchecked") + Map requestMap = objectMapper.convertValue(request, Map.class); + requestMap.values().removeIf(java.util.Objects::isNull); + return new Document(requestMap); } } From f454cc8892b65cdc4d96d59b33e06b2cb5a042e0 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:26:58 -0400 Subject: [PATCH 017/110] refactor(field names): implement nested fields consistently --- .../config/DatabaseVerification.java | 11 +++-- .../com/mongodb/samplemflix/model/Movie.java | 41 +++++++++++++++++++ .../repository/MovieRepositoryImpl.java | 8 ++-- .../samplemflix/service/MovieServiceImpl.java | 18 ++++---- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java index 6063bb6..c11a4b6 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java @@ -4,6 +4,7 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; +import com.mongodb.samplemflix.model.Movie; import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -122,13 +123,15 @@ private void createTextSearchIndex(MongoCollection moviesCollection) { .name(TEXT_INDEX_NAME) .background(true); - // Create the text index + // Create the text index using field name constants from Movie.Fields + // This makes the coupling between Movie class and index creation explicit + // and allows IDE "Find Usages" to track dependencies // MongoDB will automatically ignore this if the index already exists moviesCollection.createIndex( Indexes.compoundIndex( - Indexes.text("plot"), - Indexes.text("title"), - Indexes.text("fullplot") + Indexes.text(Movie.Fields.PLOT), + Indexes.text(Movie.Fields.TITLE), + Indexes.text(Movie.Fields.FULLPLOT) ), indexOptions ); diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 98235e0..4efe341 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -28,6 +28,47 @@ @AllArgsConstructor public class Movie { + /** + * Field name constants for MongoDB operations. + *

+ * These constants should be used when referencing field names in queries, filters, + * indexes, and other MongoDB operations to ensure type safety and enable IDE + * "Find Usages" functionality. + *

+ * Example usage: + *

+     * filter.append(Movie.Fields.TITLE, "The Matrix");
+     * Indexes.text(Movie.Fields.PLOT);
+     * 
+ */ + public static class Fields { + public static final String ID = "_id"; + public static final String TITLE = "title"; + public static final String YEAR = "year"; + public static final String PLOT = "plot"; + public static final String FULLPLOT = "fullplot"; + public static final String RELEASED = "released"; + public static final String RUNTIME = "runtime"; + public static final String POSTER = "poster"; + public static final String GENRES = "genres"; + public static final String DIRECTORS = "directors"; + public static final String WRITERS = "writers"; + public static final String CAST = "cast"; + public static final String COUNTRIES = "countries"; + public static final String LANGUAGES = "languages"; + public static final String RATED = "rated"; + public static final String AWARDS = "awards"; + public static final String IMDB = "imdb"; + public static final String IMDB_RATING = "imdb.rating"; + public static final String TOMATOES = "tomatoes"; + public static final String METACRITIC = "metacritic"; + public static final String TYPE = "type"; + + private Fields() { + // Private constructor to prevent instantiation + } + } + /** * MongoDB document ID. * Maps to the _id field in MongoDB. diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java index 6f0c438..95a87c6 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java @@ -46,7 +46,7 @@ public InsertManyResult insertMany(List movies) { @Override public Optional findById(ObjectId id) { - Movie doc = moviesCollection.find(Filters.eq("_id", id)).first(); + Movie doc = moviesCollection.find(Filters.eq(Movie.Fields.ID, id)).first(); return Optional.ofNullable(doc); } @@ -64,7 +64,7 @@ public List find(Document filter, Document sort, int skip, int limit) { @Override public UpdateResult updateOne(ObjectId id, Document update) { - return moviesCollection.updateOne(Filters.eq("_id", id), update); + return moviesCollection.updateOne(Filters.eq(Movie.Fields.ID, id), update); } @Override @@ -74,7 +74,7 @@ public UpdateResult updateMany(Document filter, Document update) { @Override public DeleteResult deleteOne(ObjectId id) { - return moviesCollection.deleteOne(Filters.eq("_id", id)); + return moviesCollection.deleteOne(Filters.eq(Movie.Fields.ID, id)); } @Override @@ -84,7 +84,7 @@ public DeleteResult deleteMany(Document filter) { @Override public Optional findOneAndDelete(ObjectId id) { - Movie doc = moviesCollection.findOneAndDelete(Filters.eq("_id", id)); + Movie doc = moviesCollection.findOneAndDelete(Filters.eq(Movie.Fields.ID, id)); return Optional.ofNullable(doc); } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 88a724d..e406ffe 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -215,19 +215,19 @@ public Movie findAndDeleteMovie(String id) { private Document buildFilter(MovieSearchQuery query) { Document filter = new Document(); - + if (query.getQ() != null && !query.getQ().trim().isEmpty()) { filter.append("$text", new Document("$search", query.getQ())); } - + if (query.getGenre() != null && !query.getGenre().trim().isEmpty()) { - filter.append("genres", new Document("$regex", Pattern.compile(query.getGenre(), Pattern.CASE_INSENSITIVE))); + filter.append(Movie.Fields.GENRES, new Document("$regex", Pattern.compile(query.getGenre(), Pattern.CASE_INSENSITIVE))); } - + if (query.getYear() != null) { - filter.append("year", query.getYear()); + filter.append(Movie.Fields.YEAR, query.getYear()); } - + if (query.getMinRating() != null || query.getMaxRating() != null) { Document ratingFilter = new Document(); if (query.getMinRating() != null) { @@ -236,14 +236,14 @@ private Document buildFilter(MovieSearchQuery query) { if (query.getMaxRating() != null) { ratingFilter.append("$lte", query.getMaxRating()); } - filter.append("imdb.rating", ratingFilter); + filter.append(Movie.Fields.IMDB_RATING, ratingFilter); } - + return filter; } private Document buildSort(String sortBy, String sortOrder) { - String field = sortBy != null && !sortBy.trim().isEmpty() ? sortBy : "title"; + String field = sortBy != null && !sortBy.trim().isEmpty() ? sortBy : Movie.Fields.TITLE; int order = "desc".equalsIgnoreCase(sortOrder) ? -1 : 1; return new Document(field, order); } From c6eeb46843a8cca597ea99f84f5ac59af3543cd9 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:36:15 -0400 Subject: [PATCH 018/110] test: add new tests based on refactors --- .../samplemflix/service/MovieServiceImpl.java | 2 +- .../controller/MovieControllerTest.java | 330 +++++++++++- .../samplemflix/service/MovieServiceTest.java | 483 +++++++++++++++++- 3 files changed, 796 insertions(+), 19 deletions(-) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index e406ffe..88c35cd 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -23,7 +23,7 @@ /** * Service layer for movie business logic. - * + *

* This service handles: * - Business logic and validation * - Query construction (filters, sorts, pagination) diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index f8c6da0..cf8dedd 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -1,14 +1,326 @@ package com.mongodb.samplemflix.controller; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.samplemflix.exception.ResourceNotFoundException; +import com.mongodb.samplemflix.exception.ValidationException; +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import com.mongodb.samplemflix.service.MovieService; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + /** - * Unit tests for MovieController. - * - * TODO: Phase 8 - Implement controller unit tests - * TODO: Phase 8 - Mock service layer - * TODO: Phase 8 - Test all endpoints + * Unit tests for MovieControllerImpl. + * + * These tests verify the REST API endpoints by mocking the service layer. + * Uses Spring's MockMvc for testing HTTP requests and responses. */ -public class MovieControllerTest { - - // TODO: Phase 8 - Add test methods -} +@WebMvcTest(MovieControllerImpl.class) +@DisplayName("MovieController Unit Tests") +class MovieControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MovieService movieService; + + private ObjectId testId; + private Movie testMovie; + private CreateMovieRequest createRequest; + private UpdateMovieRequest updateRequest; + + @BeforeEach + void setUp() { + testId = new ObjectId(); + + testMovie = Movie.builder() + .id(testId) + .title("Test Movie") + .year(2024) + .plot("A test plot") + .genres(Arrays.asList("Action", "Drama")) + .build(); + + createRequest = CreateMovieRequest.builder() + .title("New Movie") + .year(2024) + .plot("A new movie plot") + .build(); + + updateRequest = UpdateMovieRequest.builder() + .title("Updated Title") + .year(2025) + .build(); + } + + // ==================== GET ALL MOVIES TESTS ==================== + + @Test + @DisplayName("GET /api/movies - Should return list of movies") + void testGetAllMovies_Success() throws Exception { + // Arrange + List movies = Arrays.asList(testMovie); + when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); + + // Act & Assert + mockMvc.perform(get("/api/movies")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(1))) + .andExpect(jsonPath("$.data[0].title").value("Test Movie")) + .andExpect(jsonPath("$.data[0].year").value(2024)); + } + + @Test + @DisplayName("GET /api/movies - Should handle query parameters") + void testGetAllMovies_WithQueryParams() throws Exception { + // Arrange + List movies = Arrays.asList(testMovie); + when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); + + // Act & Assert + mockMvc.perform(get("/api/movies") + .param("q", "test") + .param("genre", "Action") + .param("year", "2024") + .param("limit", "10") + .param("skip", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } + + // ==================== GET MOVIE BY ID TESTS ==================== + + @Test + @DisplayName("GET /api/movies/{id} - Should return movie by ID") + void testGetMovieById_Success() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.getMovieById(movieId)).thenReturn(testMovie); + + // Act & Assert + mockMvc.perform(get("/api/movies/{id}", movieId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.title").value("Test Movie")) + .andExpect(jsonPath("$.data.year").value(2024)); + } + + @Test + @DisplayName("GET /api/movies/{id} - Should return 404 when movie not found") + void testGetMovieById_NotFound() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.getMovieById(movieId)) + .thenThrow(new ResourceNotFoundException("Movie not found")); + + // Act & Assert + mockMvc.perform(get("/api/movies/{id}", movieId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("RESOURCE_NOT_FOUND")) + .andExpect(jsonPath("$.error.message").value("Movie not found")); + } + + @Test + @DisplayName("GET /api/movies/{id} - Should return 400 for invalid ID") + void testGetMovieById_InvalidId() throws Exception { + // Arrange + String invalidId = "invalid-id"; + when(movieService.getMovieById(invalidId)) + .thenThrow(new ValidationException("Invalid movie ID format")); + + // Act & Assert + mockMvc.perform(get("/api/movies/{id}", invalidId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } + // ==================== CREATE MOVIE TESTS ==================== + + @Test + @DisplayName("POST /api/movies - Should create movie successfully") + void testCreateMovie_Success() throws Exception { + // Arrange + when(movieService.createMovie(any(CreateMovieRequest.class))).thenReturn(testMovie); + + // Act & Assert + mockMvc.perform(post("/api/movies") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createRequest))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.title").value("Test Movie")); + } + + @Test + @DisplayName("POST /api/movies - Should return 400 for validation error") + void testCreateMovie_ValidationError() throws Exception { + // Arrange + when(movieService.createMovie(any(CreateMovieRequest.class))) + .thenThrow(new ValidationException("Title is required")); + + // Act & Assert + mockMvc.perform(post("/api/movies") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } + + @Test + @DisplayName("POST /api/movies/batch - Should create movies batch successfully") + void testCreateMoviesBatch_Success() throws Exception { + // Arrange + List requests = Arrays.asList(createRequest, createRequest); + Map response = new HashMap<>(); + response.put("success", true); + response.put("insertedCount", 2); + + when(movieService.createMoviesBatch(anyList())).thenReturn(response); + + // Act & Assert + mockMvc.perform(post("/api/movies/batch") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requests))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.insertedCount").value(2)); + } + + // ==================== UPDATE MOVIE TESTS ==================== + + @Test + @DisplayName("PUT /api/movies/{id} - Should update movie successfully") + void testUpdateMovie_Success() throws Exception { + // Arrange + String movieId = testId.toHexString(); + Movie updatedMovie = Movie.builder() + .id(testId) + .title("Updated Title") + .year(2025) + .build(); + + when(movieService.updateMovie(eq(movieId), any(UpdateMovieRequest.class))) + .thenReturn(updatedMovie); + + // Act & Assert + mockMvc.perform(put("/api/movies/{id}", movieId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.title").value("Updated Title")) + .andExpect(jsonPath("$.data.year").value(2025)); + } + + @Test + @DisplayName("PUT /api/movies/{id} - Should return 404 when movie not found") + void testUpdateMovie_NotFound() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.updateMovie(eq(movieId), any(UpdateMovieRequest.class))) + .thenThrow(new ResourceNotFoundException("Movie not found")); + + // Act & Assert + mockMvc.perform(put("/api/movies/{id}", movieId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("RESOURCE_NOT_FOUND")); + } + + // ==================== DELETE MOVIE TESTS ==================== + + @Test + @DisplayName("DELETE /api/movies/{id} - Should delete movie successfully") + void testDeleteMovie_Success() throws Exception { + // Arrange + String movieId = testId.toHexString(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("deletedCount", 1L); + + when(movieService.deleteMovie(movieId)).thenReturn(response); + + // Act & Assert + mockMvc.perform(delete("/api/movies/{id}", movieId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.deletedCount").value(1)); + } + + @Test + @DisplayName("DELETE /api/movies/{id} - Should return 404 when movie not found") + void testDeleteMovie_NotFound() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.deleteMovie(movieId)) + .thenThrow(new ResourceNotFoundException("Movie not found")); + + // Act & Assert + mockMvc.perform(delete("/api/movies/{id}", movieId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("RESOURCE_NOT_FOUND")); + } + + @Test + @DisplayName("DELETE /api/movies/{id}/find-and-delete - Should find and delete movie successfully") + void testFindAndDeleteMovie_Success() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.findAndDeleteMovie(movieId)).thenReturn(testMovie); + + // Act & Assert + mockMvc.perform(delete("/api/movies/{id}/find-and-delete", movieId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.title").value("Test Movie")); + } + + @Test + @DisplayName("DELETE /api/movies/{id}/find-and-delete - Should return 404 when movie not found") + void testFindAndDeleteMovie_NotFound() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.findAndDeleteMovie(movieId)) + .thenThrow(new ResourceNotFoundException("Movie not found")); + + // Act & Assert + mockMvc.perform(delete("/api/movies/{id}/find-and-delete", movieId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("RESOURCE_NOT_FOUND")); + } +} diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java index bf8dabe..0a45520 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -1,14 +1,479 @@ package com.mongodb.samplemflix.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertManyResult; +import com.mongodb.client.result.InsertOneResult; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.samplemflix.exception.DatabaseOperationException; +import com.mongodb.samplemflix.exception.ResourceNotFoundException; +import com.mongodb.samplemflix.exception.ValidationException; +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import com.mongodb.samplemflix.repository.MovieRepository; +import org.bson.BsonObjectId; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + /** - * Unit tests for MovieService. - * - * TODO: Phase 8 - Implement service unit tests - * TODO: Phase 8 - Mock repository layer - * TODO: Phase 8 - Test business logic + * Unit tests for MovieServiceImpl. + * + * These tests verify the business logic of the service layer + * by mocking the repository layer dependencies. */ -public class MovieServiceTest { - - // TODO: Phase 8 - Add test methods -} +@ExtendWith(MockitoExtension.class) +@DisplayName("MovieService Unit Tests") +class MovieServiceTest { + + @Mock + private MovieRepository movieRepository; + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private MovieServiceImpl movieService; + + private ObjectId testId; + private Movie testMovie; + private CreateMovieRequest createRequest; + private UpdateMovieRequest updateRequest; + + @BeforeEach + void setUp() { + testId = new ObjectId(); + + testMovie = Movie.builder() + .id(testId) + .title("Test Movie") + .year(2024) + .plot("A test plot") + .genres(Arrays.asList("Action", "Drama")) + .build(); + + createRequest = CreateMovieRequest.builder() + .title("New Movie") + .year(2024) + .plot("A new movie plot") + .build(); + + updateRequest = UpdateMovieRequest.builder() + .title("Updated Title") + .year(2025) + .build(); + } + + // ==================== GET ALL MOVIES TESTS ==================== + + @Test + @DisplayName("Should get all movies with default pagination") + void testGetAllMovies_WithDefaults() { + // Arrange + MovieSearchQuery query = MovieSearchQuery.builder().build(); + List expectedMovies = Arrays.asList(testMovie); + + when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(20))) + .thenReturn(expectedMovies); + + // Act + List result = movieService.getAllMovies(query); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(testMovie.getTitle(), result.get(0).getTitle()); + verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(20)); + } + + @Test + @DisplayName("Should get all movies with custom pagination") + void testGetAllMovies_WithCustomPagination() { + // Arrange + MovieSearchQuery query = MovieSearchQuery.builder() + .limit(50) + .skip(10) + .build(); + List expectedMovies = Arrays.asList(testMovie); + + when(movieRepository.find(any(Document.class), any(Document.class), eq(10), eq(50))) + .thenReturn(expectedMovies); + + // Act + List result = movieService.getAllMovies(query); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + verify(movieRepository).find(any(Document.class), any(Document.class), eq(10), eq(50)); + } + + @Test + @DisplayName("Should enforce maximum limit of 100") + void testGetAllMovies_EnforcesMaxLimit() { + // Arrange + MovieSearchQuery query = MovieSearchQuery.builder() + .limit(200) + .build(); + + when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(100))) + .thenReturn(Collections.emptyList()); + + // Act + movieService.getAllMovies(query); + + // Assert + verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(100)); + } + + @Test + @DisplayName("Should enforce minimum limit of 1") + void testGetAllMovies_EnforcesMinLimit() { + // Arrange + MovieSearchQuery query = MovieSearchQuery.builder() + .limit(0) + .build(); + + when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(1))) + .thenReturn(Collections.emptyList()); + + // Act + movieService.getAllMovies(query); + + // Assert + verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(1)); + } + + // ==================== GET MOVIE BY ID TESTS ==================== + + @Test + @DisplayName("Should get movie by valid ID") + void testGetMovieById_ValidId() { + // Arrange + String validId = testId.toHexString(); + when(movieRepository.findById(testId)).thenReturn(Optional.of(testMovie)); + + // Act + Movie result = movieService.getMovieById(validId); + + // Assert + assertNotNull(result); + assertEquals(testMovie.getTitle(), result.getTitle()); + verify(movieRepository).findById(testId); + } + + @Test + @DisplayName("Should throw ValidationException for invalid ID format") + void testGetMovieById_InvalidIdFormat() { + // Arrange + String invalidId = "invalid-id"; + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.getMovieById(invalidId)); + verify(movieRepository, never()).findById(any()); + } + + @Test + @DisplayName("Should throw ResourceNotFoundException when movie not found") + void testGetMovieById_NotFound() { + // Arrange + String validId = testId.toHexString(); + when(movieRepository.findById(testId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> movieService.getMovieById(validId)); + verify(movieRepository).findById(testId); + } + + // ==================== CREATE MOVIE TESTS ==================== + + @Test + @DisplayName("Should create movie successfully") + void testCreateMovie_Success() { + // Arrange + InsertOneResult insertResult = mock(InsertOneResult.class); + when(insertResult.wasAcknowledged()).thenReturn(true); + when(insertResult.getInsertedId()).thenReturn(new BsonObjectId(testId)); + when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); + when(movieRepository.findById(testId)).thenReturn(Optional.of(testMovie)); + + // Act + Movie result = movieService.createMovie(createRequest); + + // Assert + assertNotNull(result); + verify(movieRepository).insertOne(any(Movie.class)); + verify(movieRepository).findById(testId); + } + + @Test + @DisplayName("Should throw ValidationException when title is null") + void testCreateMovie_NullTitle() { + // Arrange + CreateMovieRequest invalidRequest = CreateMovieRequest.builder() + .title(null) + .year(2024) + .build(); + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.createMovie(invalidRequest)); + verify(movieRepository, never()).insertOne(any()); + } + + @Test + @DisplayName("Should throw ValidationException when title is empty") + void testCreateMovie_EmptyTitle() { + // Arrange + CreateMovieRequest invalidRequest = CreateMovieRequest.builder() + .title(" ") + .year(2024) + .build(); + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.createMovie(invalidRequest)); + verify(movieRepository, never()).insertOne(any()); + } + + @Test + @DisplayName("Should throw DatabaseOperationException when insert not acknowledged") + void testCreateMovie_NotAcknowledged() { + // Arrange + InsertOneResult insertResult = mock(InsertOneResult.class); + when(insertResult.wasAcknowledged()).thenReturn(false); + when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); + + // Act & Assert + assertThrows(DatabaseOperationException.class, () -> movieService.createMovie(createRequest)); + verify(movieRepository).insertOne(any(Movie.class)); + verify(movieRepository, never()).findById(any()); + } + + @Test + @DisplayName("Should throw DatabaseOperationException when created movie not found") + void testCreateMovie_CreatedMovieNotFound() { + // Arrange + InsertOneResult insertResult = mock(InsertOneResult.class); + when(insertResult.wasAcknowledged()).thenReturn(true); + when(insertResult.getInsertedId()).thenReturn(new BsonObjectId(testId)); + when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); + when(movieRepository.findById(testId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(DatabaseOperationException.class, () -> movieService.createMovie(createRequest)); + verify(movieRepository).insertOne(any(Movie.class)); + verify(movieRepository).findById(testId); + } + + // ==================== CREATE MOVIES BATCH TESTS ==================== + + @Test + @DisplayName("Should create movies batch successfully") + void testCreateMoviesBatch_Success() { + // Arrange + List requests = Arrays.asList(createRequest, createRequest); + InsertManyResult insertResult = mock(InsertManyResult.class); + Map insertedIds = new HashMap<>(); + insertedIds.put(0, new BsonObjectId(new ObjectId())); + insertedIds.put(1, new BsonObjectId(new ObjectId())); + + when(insertResult.wasAcknowledged()).thenReturn(true); + when(insertResult.getInsertedIds()).thenReturn(insertedIds); + when(movieRepository.insertMany(anyList())).thenReturn(insertResult); + + // Act + Map result = movieService.createMoviesBatch(requests); + + // Assert + assertNotNull(result); + assertEquals(2, result.get("insertedCount")); + assertNotNull(result.get("insertedIds")); + verify(movieRepository).insertMany(anyList()); + } + + @Test + @DisplayName("Should throw DatabaseOperationException when batch insert not acknowledged") + void testCreateMoviesBatch_NotAcknowledged() { + // Arrange + List requests = Arrays.asList(createRequest); + InsertManyResult insertResult = mock(InsertManyResult.class); + when(insertResult.wasAcknowledged()).thenReturn(false); + when(movieRepository.insertMany(anyList())).thenReturn(insertResult); + + // Act & Assert + assertThrows(DatabaseOperationException.class, () -> movieService.createMoviesBatch(requests)); + verify(movieRepository).insertMany(anyList()); + } + + // ==================== UPDATE MOVIE TESTS ==================== + + @Test + @DisplayName("Should update movie successfully") + void testUpdateMovie_Success() { + // Arrange + String validId = testId.toHexString(); + Map requestMap = new HashMap<>(); + requestMap.put("title", "Updated Title"); + requestMap.put("year", 2025); + + when(objectMapper.convertValue(updateRequest, Map.class)).thenReturn(requestMap); + + UpdateResult updateResult = mock(UpdateResult.class); + when(updateResult.getMatchedCount()).thenReturn(1L); + when(movieRepository.updateOne(eq(testId), any(Document.class))).thenReturn(updateResult); + when(movieRepository.findById(testId)).thenReturn(Optional.of(testMovie)); + + // Act + Movie result = movieService.updateMovie(validId, updateRequest); + + // Assert + assertNotNull(result); + verify(movieRepository).updateOne(eq(testId), any(Document.class)); + verify(movieRepository).findById(testId); + } + + @Test + @DisplayName("Should throw ValidationException for invalid ID in update") + void testUpdateMovie_InvalidId() { + // Arrange + String invalidId = "invalid-id"; + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.updateMovie(invalidId, updateRequest)); + verify(movieRepository, never()).updateOne(any(), any()); + } + + @Test + @DisplayName("Should throw ValidationException when update request is empty") + void testUpdateMovie_EmptyRequest() { + // Arrange + String validId = testId.toHexString(); + UpdateMovieRequest emptyRequest = UpdateMovieRequest.builder().build(); + Map emptyMap = new HashMap<>(); + + when(objectMapper.convertValue(emptyRequest, Map.class)).thenReturn(emptyMap); + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.updateMovie(validId, emptyRequest)); + verify(movieRepository, never()).updateOne(any(), any()); + } + + @Test + @DisplayName("Should throw ResourceNotFoundException when movie to update not found") + void testUpdateMovie_NotFound() { + // Arrange + String validId = testId.toHexString(); + Map requestMap = new HashMap<>(); + requestMap.put("title", "Updated Title"); + + when(objectMapper.convertValue(updateRequest, Map.class)).thenReturn(requestMap); + + UpdateResult updateResult = mock(UpdateResult.class); + when(updateResult.getMatchedCount()).thenReturn(0L); + when(movieRepository.updateOne(eq(testId), any(Document.class))).thenReturn(updateResult); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> movieService.updateMovie(validId, updateRequest)); + verify(movieRepository).updateOne(eq(testId), any(Document.class)); + verify(movieRepository, never()).findById(any()); + } + + // ==================== DELETE MOVIE TESTS ==================== + + @Test + @DisplayName("Should delete movie successfully") + void testDeleteMovie_Success() { + // Arrange + String validId = testId.toHexString(); + DeleteResult deleteResult = mock(DeleteResult.class); + when(deleteResult.getDeletedCount()).thenReturn(1L); + when(movieRepository.deleteOne(testId)).thenReturn(deleteResult); + + // Act + Map result = movieService.deleteMovie(validId); + + // Assert + assertNotNull(result); + assertEquals(1L, result.get("deletedCount")); + verify(movieRepository).deleteOne(testId); + } + + @Test + @DisplayName("Should throw ValidationException for invalid ID in delete") + void testDeleteMovie_InvalidId() { + // Arrange + String invalidId = "invalid-id"; + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.deleteMovie(invalidId)); + verify(movieRepository, never()).deleteOne(any()); + } + + @Test + @DisplayName("Should throw ResourceNotFoundException when movie to delete not found") + void testDeleteMovie_NotFound() { + // Arrange + String validId = testId.toHexString(); + DeleteResult deleteResult = mock(DeleteResult.class); + when(deleteResult.getDeletedCount()).thenReturn(0L); + when(movieRepository.deleteOne(testId)).thenReturn(deleteResult); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> movieService.deleteMovie(validId)); + verify(movieRepository).deleteOne(testId); + } + + // ==================== FIND AND DELETE MOVIE TESTS ==================== + + @Test + @DisplayName("Should find and delete movie successfully") + void testFindAndDeleteMovie_Success() { + // Arrange + String validId = testId.toHexString(); + when(movieRepository.findOneAndDelete(testId)).thenReturn(Optional.of(testMovie)); + + // Act + Movie result = movieService.findAndDeleteMovie(validId); + + // Assert + assertNotNull(result); + assertEquals(testMovie.getTitle(), result.getTitle()); + verify(movieRepository).findOneAndDelete(testId); + } + + @Test + @DisplayName("Should throw ValidationException for invalid ID in find and delete") + void testFindAndDeleteMovie_InvalidId() { + // Arrange + String invalidId = "invalid-id"; + + // Act & Assert + assertThrows(ValidationException.class, () -> movieService.findAndDeleteMovie(invalidId)); + verify(movieRepository, never()).findOneAndDelete(any()); + } + + @Test + @DisplayName("Should throw ResourceNotFoundException when movie to find and delete not found") + void testFindAndDeleteMovie_NotFound() { + // Arrange + String validId = testId.toHexString(); + when(movieRepository.findOneAndDelete(testId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> movieService.findAndDeleteMovie(validId)); + verify(movieRepository).findOneAndDelete(testId); + } +} From 2d5087bb79c7d100fdf707e695d22cc6231b43c9 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 15:43:31 -0400 Subject: [PATCH 019/110] refactor(DTOs): create missing return types --- .../controller/MovieControllerImpl.java | 43 ++++++++-------- .../model/dto/BatchInsertResponse.java | 20 ++++++++ .../model/dto/BatchUpdateResponse.java | 17 +++++++ .../samplemflix/model/dto/DeleteResponse.java | 16 ++++++ .../samplemflix/service/MovieService.java | 12 +++-- .../samplemflix/service/MovieServiceImpl.java | 51 ++++++++++--------- .../controller/MovieControllerTest.java | 14 ++--- .../samplemflix/service/MovieServiceTest.java | 12 +++-- 8 files changed, 125 insertions(+), 60 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchUpdateResponse.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DeleteResponse.java diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 643b48e..4a139c0 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -1,7 +1,10 @@ package com.mongodb.samplemflix.controller; import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.BatchInsertResponse; +import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.model.response.SuccessResponse; @@ -125,17 +128,17 @@ public ResponseEntity> createMovie(@Valid @RequestBody Cr * Creates multiple movie documents in a single operation. */ @PostMapping("/batch") - public ResponseEntity>> createMoviesBatch( + public ResponseEntity> createMoviesBatch( @RequestBody List requests) { - Map result = movieService.createMoviesBatch(requests); - - SuccessResponse> response = SuccessResponse.>builder() + BatchInsertResponse result = movieService.createMoviesBatch(requests); + + SuccessResponse response = SuccessResponse.builder() .success(true) - .message("Successfully created " + result.get("insertedCount") + " movies") + .message("Successfully created " + result.getInsertedCount() + " movies") .data(result) .timestamp(Instant.now().toString()) .build(); - + return ResponseEntity.status(HttpStatus.CREATED).body(response); } @@ -167,17 +170,17 @@ public ResponseEntity> updateMovie( */ @SuppressWarnings("unchecked") @PatchMapping - public ResponseEntity>> updateMoviesBatch( + public ResponseEntity> updateMoviesBatch( @RequestBody Map body) { Document filter = new Document((Map) body.get("filter")); Document update = new Document((Map) body.get("update")); - Map result = movieService.updateMoviesBatch(filter, update); + BatchUpdateResponse result = movieService.updateMoviesBatch(filter, update); - SuccessResponse> response = SuccessResponse.>builder() + SuccessResponse response = SuccessResponse.builder() .success(true) - .message("Update operation completed. Matched " + result.get("matchedCount") + - " documents, modified " + result.get("modifiedCount") + " documents.") + .message("Update operation completed. Matched " + result.getMatchedCount() + + " documents, modified " + result.getModifiedCount() + " documents.") .data(result) .timestamp(Instant.now().toString()) .build(); @@ -210,16 +213,16 @@ public ResponseEntity> findAndDeleteMovie(@PathVariable S * Deletes a single movie document. */ @DeleteMapping("/{id}") - public ResponseEntity>> deleteMovie(@PathVariable String id) { - Map result = movieService.deleteMovie(id); - - SuccessResponse> response = SuccessResponse.>builder() + public ResponseEntity> deleteMovie(@PathVariable String id) { + DeleteResponse result = movieService.deleteMovie(id); + + SuccessResponse response = SuccessResponse.builder() .success(true) .message("Movie deleted successfully") .data(result) .timestamp(Instant.now().toString()) .build(); - + return ResponseEntity.ok(response); } @@ -230,15 +233,15 @@ public ResponseEntity>> deleteMovie(@PathVar */ @SuppressWarnings("unchecked") @DeleteMapping - public ResponseEntity>> deleteMoviesBatch( + public ResponseEntity> deleteMoviesBatch( @RequestBody Map body) { Document filter = new Document((Map) body.get("filter")); - Map result = movieService.deleteMoviesBatch(filter); + DeleteResponse result = movieService.deleteMoviesBatch(filter); - SuccessResponse> response = SuccessResponse.>builder() + SuccessResponse response = SuccessResponse.builder() .success(true) - .message("Delete operation completed. Removed " + result.get("deletedCount") + " documents.") + .message("Delete operation completed. Removed " + result.getDeletedCount() + " documents.") .data(result) .timestamp(Instant.now().toString()) .build(); diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java new file mode 100644 index 0000000..2196533 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java @@ -0,0 +1,20 @@ +package com.mongodb.samplemflix.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.BsonValue; + +import java.util.Collection; + +/** + * Response DTO for batch insert operations. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BatchInsertResponse { + private int insertedCount; + private Collection insertedIds; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchUpdateResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchUpdateResponse.java new file mode 100644 index 0000000..354bdbc --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchUpdateResponse.java @@ -0,0 +1,17 @@ +package com.mongodb.samplemflix.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response DTO for batch update operations. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BatchUpdateResponse { + private long matchedCount; + private long modifiedCount; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DeleteResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DeleteResponse.java new file mode 100644 index 0000000..b7ddc85 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DeleteResponse.java @@ -0,0 +1,16 @@ +package com.mongodb.samplemflix.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response DTO for delete operations. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeleteResponse { + private long deletedCount; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 4e44003..7841e65 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -1,13 +1,15 @@ package com.mongodb.samplemflix.service; import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.BatchInsertResponse; +import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import org.bson.Document; import java.util.List; -import java.util.Map; /** * Service interface for movie business logic. @@ -20,15 +22,15 @@ public interface MovieService { Movie createMovie(CreateMovieRequest request); - Map createMoviesBatch(List requests); + BatchInsertResponse createMoviesBatch(List requests); Movie updateMovie(String id, UpdateMovieRequest request); - Map updateMoviesBatch(Document filter, Document update); + BatchUpdateResponse updateMoviesBatch(Document filter, Document update); - Map deleteMovie(String id); + DeleteResponse deleteMovie(String id); - Map deleteMoviesBatch(Document filter); + DeleteResponse deleteMoviesBatch(Document filter); Movie findAndDeleteMovie(String id); } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 88c35cd..c31db1b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -9,7 +9,10 @@ import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.BatchInsertResponse; +import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; @@ -95,18 +98,18 @@ public Movie createMovie(CreateMovieRequest request) { } @Override - public Map createMoviesBatch(List requests) { + public BatchInsertResponse createMoviesBatch(List requests) { if (requests == null || requests.isEmpty()) { throw new ValidationException("Request body must be a non-empty array of movie objects"); } - + for (int i = 0; i < requests.size(); i++) { CreateMovieRequest request = requests.get(i); if (request.getTitle() == null || request.getTitle().trim().isEmpty()) { throw new ValidationException("Movie at index " + i + ": Title is required"); } } - + List movies = requests.stream() .map(request -> Movie.builder() .title(request.getTitle()) @@ -124,16 +127,16 @@ public Map createMoviesBatch(List requests) .poster(request.getPoster()) .build()) .toList(); - + InsertManyResult result = movieRepository.insertMany(movies); if (!result.wasAcknowledged()) { throw new DatabaseOperationException("Batch movie insertion was not acknowledged by the database"); } - - return Map.of( - "insertedCount", result.getInsertedIds().size(), - "insertedIds", result.getInsertedIds().values() + + return new BatchInsertResponse( + result.getInsertedIds().size(), + result.getInsertedIds().values() ); } @@ -159,48 +162,48 @@ public Movie updateMovie(String id, UpdateMovieRequest request) { } @Override - public Map updateMoviesBatch(Document filter, Document update) { + public BatchUpdateResponse updateMoviesBatch(Document filter, Document update) { if (filter == null || update == null) { throw new ValidationException("Both filter and update objects are required"); } - + if (update.isEmpty()) { throw new ValidationException("Update object cannot be empty"); } - + Document setUpdate = new Document("$set", update); UpdateResult result = movieRepository.updateMany(filter, setUpdate); - - return Map.of( - "matchedCount", result.getMatchedCount(), - "modifiedCount", result.getModifiedCount() + + return new BatchUpdateResponse( + result.getMatchedCount(), + result.getModifiedCount() ); } @Override - public Map deleteMovie(String id) { + public DeleteResponse deleteMovie(String id) { if (!ObjectId.isValid(id)) { throw new ValidationException("Invalid movie ID format"); } - + DeleteResult result = movieRepository.deleteOne(new ObjectId(id)); - + if (result.getDeletedCount() == 0) { throw new ResourceNotFoundException("Movie not found"); } - - return Map.of("deletedCount", result.getDeletedCount()); + + return new DeleteResponse(result.getDeletedCount()); } @Override - public Map deleteMoviesBatch(Document filter) { + public DeleteResponse deleteMoviesBatch(Document filter) { if (filter == null || filter.isEmpty()) { throw new ValidationException("Filter object is required and cannot be empty. This prevents accidental deletion of all documents."); } - + DeleteResult result = movieRepository.deleteMany(filter); - - return Map.of("deletedCount", result.getDeletedCount()); + + return new DeleteResponse(result.getDeletedCount()); } @Override diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index cf8dedd..a180d51 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -4,10 +4,13 @@ import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.BatchInsertResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.service.MovieService; +import org.bson.BsonObjectId; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -202,9 +205,10 @@ void testCreateMovie_ValidationError() throws Exception { void testCreateMoviesBatch_Success() throws Exception { // Arrange List requests = Arrays.asList(createRequest, createRequest); - Map response = new HashMap<>(); - response.put("success", true); - response.put("insertedCount", 2); + Map insertedIds = new HashMap<>(); + insertedIds.put(0, new BsonObjectId(new ObjectId())); + insertedIds.put(1, new BsonObjectId(new ObjectId())); + BatchInsertResponse response = new BatchInsertResponse(2, insertedIds.values()); when(movieService.createMoviesBatch(anyList())).thenReturn(response); @@ -267,9 +271,7 @@ void testUpdateMovie_NotFound() throws Exception { void testDeleteMovie_Success() throws Exception { // Arrange String movieId = testId.toHexString(); - Map response = new HashMap<>(); - response.put("success", true); - response.put("deletedCount", 1L); + DeleteResponse response = new DeleteResponse(1L); when(movieService.deleteMovie(movieId)).thenReturn(response); diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java index 0a45520..df8a858 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -9,7 +9,9 @@ import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.model.dto.BatchInsertResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; +import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; @@ -295,12 +297,12 @@ void testCreateMoviesBatch_Success() { when(movieRepository.insertMany(anyList())).thenReturn(insertResult); // Act - Map result = movieService.createMoviesBatch(requests); + BatchInsertResponse result = movieService.createMoviesBatch(requests); // Assert assertNotNull(result); - assertEquals(2, result.get("insertedCount")); - assertNotNull(result.get("insertedIds")); + assertEquals(2, result.getInsertedCount()); + assertNotNull(result.getInsertedIds()); verify(movieRepository).insertMany(anyList()); } @@ -403,11 +405,11 @@ void testDeleteMovie_Success() { when(movieRepository.deleteOne(testId)).thenReturn(deleteResult); // Act - Map result = movieService.deleteMovie(validId); + DeleteResponse result = movieService.deleteMovie(validId); // Assert assertNotNull(result); - assertEquals(1L, result.get("deletedCount")); + assertEquals(1L, result.getDeletedCount()); verify(movieRepository).deleteOne(testId); } From 7b9029c2ff142ae138c384e79db206b0e18454f6 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 16:01:08 -0400 Subject: [PATCH 020/110] refactor(spring data): implement spring data fully --- server/java-spring/README.md | 12 +- server/java-spring/pom.xml | 26 +-- .../samplemflix/config/MongoConfig.java | 121 ++++-------- .../repository/MovieRepository.java | 124 +++--------- .../repository/MovieRepositoryImpl.java | 96 --------- .../samplemflix/service/MovieServiceImpl.java | 185 ++++++++++++------ .../samplemflix/service/MovieServiceTest.java | 135 ++++--------- 7 files changed, 244 insertions(+), 455 deletions(-) delete mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java diff --git a/server/java-spring/README.md b/server/java-spring/README.md index c0db54c..54f4498 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -1,17 +1,18 @@ # MongoDB Sample MFlix - Java Spring Boot Backend [DRAFT] -A Spring Boot REST API demonstrating MongoDB CRUD operations using the MongoDB Java Driver with the sample_mflix database. +A Spring Boot REST API demonstrating MongoDB CRUD operations using Spring Data MongoDB with the sample_mflix database. ## Overview This application provides a REST API for managing movie data from MongoDB's sample_mflix database. It demonstrates: -- Direct usage of the MongoDB Java Driver (not Spring Data MongoDB) +- Spring Data MongoDB for simplified data access - CRUD operations (Create, Read, Update, Delete) - Text search functionality - Filtering, sorting, and pagination - Comprehensive error handling - API documentation with Swagger/OpenAPI +- MongoTemplate for complex queries ## Prerequisites @@ -152,9 +153,9 @@ See `JAVA-SPRING-IMPLEMENTATION-PLAN.md` in the repository root for the complete ## Technology Stack -- **Framework**: Spring Boot 3.2.0 +- **Framework**: Spring Boot 3.5.7 - **Java Version**: 17 -- **MongoDB Driver**: MongoDB Java Driver 5.1.4 (Sync) +- **MongoDB**: Spring Data MongoDB 4.5.5 - **Build Tool**: Maven - **API Documentation**: SpringDoc OpenAPI 2.3.0 - **Testing**: JUnit 5, Mockito, Spring Boot Test @@ -163,11 +164,12 @@ See `JAVA-SPRING-IMPLEMENTATION-PLAN.md` in the repository root for the complete This application is designed as an educational sample to demonstrate: -1. How to use the MongoDB Java Driver directly (without Spring Data MongoDB) +1. How to use Spring Data MongoDB for simplified data access 2. Best practices for Spring Boot REST API development 3. Proper separation of concerns (Controller → Service → Repository) 4. MongoDB CRUD operations and query patterns 5. Error handling and validation in Spring Boot +6. Using MongoTemplate for complex queries alongside Spring Data repositories ## Troubleshooting diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index ed33e67..bd35bdf 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -16,45 +16,31 @@ sample-mflix-spring 1.0.0 MongoDB Sample MFlix Spring API - Spring Boot backend for MongoDB sample mflix application demonstrating CRUD operations using MongoDB Java Driver + Spring Boot backend for MongoDB sample mflix application demonstrating CRUD operations using Spring Data MongoDB 17 - 5.6.0 2.8.13 4.0.0 - + org.springframework.boot spring-boot-starter-web - + org.springframework.boot spring-boot-starter-validation - - - - org.mongodb - mongodb-driver-sync - ${mongodb.driver.version} - - + - org.mongodb - bson - ${mongodb.driver.version} - - - org.mongodb - mongodb-driver-core - ${mongodb.driver.version} + org.springframework.boot + spring-boot-starter-data-mongodb diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index ba0c6d6..765989c 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -2,38 +2,35 @@ import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoDatabase; -import org.bson.codecs.configuration.CodecRegistry; -import org.bson.codecs.pojo.PojoCodecProvider; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import java.util.concurrent.TimeUnit; -import static org.bson.codecs.configuration.CodecRegistries.fromProviders; -import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; - /** - * MongoDB configuration class for the Sample MFlix application. + * MongoDB configuration class for the Sample MFlix application using Spring Data MongoDB. *

- * This class configures the MongoDB client connection using the MongoDB Java Driver. - * It creates singleton beans for MongoClient and MongoDatabase that will be injected - * throughout the application. + * This class extends AbstractMongoClientConfiguration to customize MongoDB client settings + * while leveraging Spring Data MongoDB's auto-configuration for repositories and templates. *

* Key features: * - Connection pooling with configurable settings (max 100 connections, min 10) * - Connection timeout configuration (10 seconds for connect and read) - * - Graceful shutdown handling (managed by Spring's bean lifecycle) - * - Connection string validation + * - Automatic POJO mapping (no manual codec configuration needed) + * - Repository scanning and auto-configuration + * - MongoTemplate bean creation for complex queries *

- * The MongoClient is thread-safe and designed to be shared across the application. - * Spring automatically manages the lifecycle and closes the client on shutdown. + * Spring Data MongoDB automatically: + * - Creates MongoClient and MongoTemplate beans + * - Handles POJO to BSON conversion + * - Manages connection lifecycle + * - Provides repository implementations */ @Configuration -public class MongoConfig { +@EnableMongoRepositories(basePackages = "com.mongodb.samplemflix.repository") +public class MongoConfig extends AbstractMongoClientConfiguration { @Value("${spring.data.mongodb.uri}") private String mongoUri; @@ -41,27 +38,13 @@ public class MongoConfig { @Value("${spring.data.mongodb.database}") private String databaseName; - /** - * Creates and configures the MongoDB client with connection pooling and timeout settings. - *

- * Connection Pool Settings: - * - Max pool size: 100 connections (handles high concurrent load) - * - Min pool size: 10 connections (maintains ready connections) - * - Max connection idle time: 60 seconds (releases idle connections) - * - Max wait time: 10 seconds (time to wait for available connection) - *

- * Socket Settings: - * - Connect timeout: 10 seconds (time to establish connection) - * - Read timeout: 10 seconds (time to wait for server response) - *

- * The MongoClient is thread-safe and should be shared across the application. - * Spring will automatically close this client when the application shuts down. - * - * @return configured MongoClient instance - * @throws IllegalArgumentException if connection string is invalid - */ - @Bean - public MongoClient mongoClient() { + @Override + protected String getDatabaseName() { + return databaseName; + } + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { // Validate connection string is not empty if (mongoUri == null || mongoUri.trim().isEmpty()) { throw new IllegalArgumentException( @@ -69,60 +52,26 @@ public MongoClient mongoClient() { ); } - // Create the POJO codec provider, enabling automatic POJO discovery - CodecRegistry pojoCodecRegistry = fromProviders( - PojoCodecProvider.builder().automatic(true).build() - ); - - // Combine the default registry with your new POJO registry - // IMPORTANT: The default registry must come FIRST. - CodecRegistry registry = fromRegistries( - MongoClientSettings.getDefaultCodecRegistry(), - pojoCodecRegistry - ); - // Parse and validate the connection string ConnectionString connectionString = new ConnectionString(mongoUri); - // Build client settings with connection pooling and timeouts - // These settings optimize for both performance and resource management - MongoClientSettings settings = MongoClientSettings.builder() - .codecRegistry(registry) - .applyConnectionString(connectionString) + // Apply connection string and custom settings + builder.applyConnectionString(connectionString) // Configure connection pool for optimal performance - .applyToConnectionPoolSettings(builder -> - builder.maxSize(100) // Maximum connections in pool - .minSize(10) // Minimum connections to maintain - .maxConnectionIdleTime(60000, TimeUnit.MILLISECONDS) // Release idle connections after 60s - .maxWaitTime(10000, TimeUnit.MILLISECONDS) // Wait up to 10s for available connection + .applyToConnectionPoolSettings(poolBuilder -> + poolBuilder.maxSize(100) // Maximum connections in pool + .minSize(10) // Minimum connections to maintain + .maxConnectionIdleTime(60000, TimeUnit.MILLISECONDS) // Release idle connections after 60s + .maxWaitTime(10000, TimeUnit.MILLISECONDS) // Wait up to 10s for available connection ) // Configure socket timeouts to prevent hanging connections - .applyToSocketSettings(builder -> - builder.connectTimeout(10000, TimeUnit.MILLISECONDS) // 10s to establish connection - .readTimeout(10000, TimeUnit.MILLISECONDS) // 10s to wait for server response + .applyToSocketSettings(socketBuilder -> + socketBuilder.connectTimeout(10000, TimeUnit.MILLISECONDS) // 10s to establish connection + .readTimeout(10000, TimeUnit.MILLISECONDS) // 10s to wait for server response ) // Configure server selection timeout - .applyToClusterSettings(builder -> - builder.serverSelectionTimeout(10000, TimeUnit.MILLISECONDS) // 10s to select server - ) - .build(); - - return MongoClients.create(settings); - } - - /** - * Creates a reference to the MongoDB database. - *

- * This bean provides access to the sample_mflix database and can be injected - * into repositories and services throughout the application. - *

- * The database name is configured in application.properties and defaults to "sample_mflix". - * - * @param mongoClient the MongoDB client (injected by Spring) - * @return MongoDatabase instance for the configured database - */ - @Bean - public MongoDatabase mongoDatabase(MongoClient mongoClient) { - return mongoClient.getDatabase(databaseName); + .applyToClusterSettings(clusterBuilder -> + clusterBuilder.serverSelectionTimeout(10000, TimeUnit.MILLISECONDS) // 10s to select server + ); } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java index b416ba1..77cb013 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java @@ -1,105 +1,37 @@ package com.mongodb.samplemflix.repository; -import com.mongodb.client.result.DeleteResult; -import com.mongodb.client.result.InsertManyResult; -import com.mongodb.client.result.InsertOneResult; -import com.mongodb.client.result.UpdateResult; import com.mongodb.samplemflix.model.Movie; -import org.bson.Document; import org.bson.types.ObjectId; - -import java.util.List; -import java.util.Optional; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; /** - * Repository interface for movie data access. + * Spring Data MongoDB repository for movie data access. + * + * This repository extends MongoRepository which provides: + * - Basic CRUD operations (save, findById, findAll, delete, etc.) + * - Pagination and sorting support + * - Query derivation from method names + * - Custom query support via @Query annotation * - * This repository provides methods for all CRUD operations using the MongoDB Java Driver directly. - * The implementation uses MongoCollection for direct control over BSON documents. + * For complex queries not supported by Spring Data, you can inject MongoTemplate + * in the service layer. */ -public interface MovieRepository { - - /** - * Inserts a single movie document. - * - * @param movie the movie to insert - * @return the result of the insert operation - */ - InsertOneResult insertOne(Movie movie); - - /** - * Inserts multiple movie documents. - * - * @param movies the list of movies to insert - * @return the result of the insert operation - */ - InsertManyResult insertMany(List movies); - - /** - * Finds a single movie by its ID. - * - * @param id the movie ID - * @return Optional containing the movie if found, empty otherwise - */ - Optional findById(ObjectId id); - - /** - * Finds multiple movies with filtering, sorting, and pagination. - * - * @param filter the filter document - * @param sort the sort document - * @param skip number of documents to skip - * @param limit maximum number of documents to return - * @return list of movies matching the criteria - */ - List find(Document filter, Document sort, int skip, int limit); - - /** - * Updates a single movie by ID. - * - * @param id the movie ID - * @param update the update document - * @return the result of the update operation - */ - UpdateResult updateOne(ObjectId id, Document update); - - /** - * Updates multiple movies matching the filter. - * - * @param filter the filter document - * @param update the update document - * @return the result of the update operation - */ - UpdateResult updateMany(Document filter, Document update); - - /** - * Deletes a single movie by ID. - * - * @param id the movie ID - * @return the result of the delete operation - */ - DeleteResult deleteOne(ObjectId id); - - /** - * Deletes multiple movies matching the filter. - * - * @param filter the filter document - * @return the result of the delete operation - */ - DeleteResult deleteMany(Document filter); - - /** - * Finds and deletes a single movie in one atomic operation. - * - * @param id the movie ID - * @return Optional containing the deleted movie if found, empty otherwise - */ - Optional findOneAndDelete(ObjectId id); - - /** - * Counts the total number of documents in the movies collection. - * - * @return the count of documents - */ - long countDocuments(); +@Repository +public interface MovieRepository extends MongoRepository { + + // Spring Data MongoDB provides these methods automatically: + // - save(Movie movie) - insert or update + // - saveAll(Iterable movies) - batch insert/update + // - findById(ObjectId id) - find by ID + // - findAll() - find all documents + // - findAll(Pageable pageable) - find with pagination + // - deleteById(ObjectId id) - delete by ID + // - delete(Movie movie) - delete entity + // - count() - count all documents + // - existsById(ObjectId id) - check if exists + + // Custom query methods can be added here using method name conventions: + // Example: List findByGenresContaining(String genre); + // Example: List findByYearBetween(Integer startYear, Integer endYear); } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java deleted file mode 100644 index 95a87c6..0000000 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepositoryImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.mongodb.samplemflix.repository; - -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.Filters; -import com.mongodb.client.result.DeleteResult; -import com.mongodb.client.result.InsertManyResult; -import com.mongodb.client.result.InsertOneResult; -import com.mongodb.client.result.UpdateResult; -import com.mongodb.samplemflix.model.Movie; -import org.bson.Document; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Implementation of MovieRepository using MongoDB Java Driver directly. - * - * This class demonstrates direct usage of MongoCollection for CRUD operations. - * It uses MongoDB's POJO codec to automatically convert between Movie objects and BSON Documents. - * - * This approach is used for educational purposes to show how the MongoDB driver works, - * rather than using Spring Data MongoDB which abstracts these details. - */ -@Repository -public class MovieRepositoryImpl implements MovieRepository { - - private final MongoCollection moviesCollection; - - public MovieRepositoryImpl(MongoDatabase mongoDatabase) { - this.moviesCollection = mongoDatabase.getCollection("movies", Movie.class); - } - - @Override - public InsertOneResult insertOne(Movie movie) { - return moviesCollection.insertOne(movie); - } - - @Override - public InsertManyResult insertMany(List movies) { - return moviesCollection.insertMany(movies); - } - - @Override - public Optional findById(ObjectId id) { - Movie doc = moviesCollection.find(Filters.eq(Movie.Fields.ID, id)).first(); - return Optional.ofNullable(doc); - } - - @Override - public List find(Document filter, Document sort, int skip, int limit) { - List movies = new ArrayList<>(); - moviesCollection.find(filter) - .sort(sort) - .skip(skip) - .limit(limit) - .into(movies); - - return movies; - } - - @Override - public UpdateResult updateOne(ObjectId id, Document update) { - return moviesCollection.updateOne(Filters.eq(Movie.Fields.ID, id), update); - } - - @Override - public UpdateResult updateMany(Document filter, Document update) { - return moviesCollection.updateMany(filter, update); - } - - @Override - public DeleteResult deleteOne(ObjectId id) { - return moviesCollection.deleteOne(Filters.eq(Movie.Fields.ID, id)); - } - - @Override - public DeleteResult deleteMany(Document filter) { - return moviesCollection.deleteMany(filter); - } - - @Override - public Optional findOneAndDelete(ObjectId id) { - Movie doc = moviesCollection.findOneAndDelete(Filters.eq(Movie.Fields.ID, id)); - return Optional.ofNullable(doc); - } - - @Override - public long countDocuments() { - return moviesCollection.countDocuments(); - } - -} diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index c31db1b..a2fe24d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -1,9 +1,7 @@ package com.mongodb.samplemflix.service; -import com.mongodb.client.result.DeleteResult; -import com.mongodb.client.result.InsertManyResult; -import com.mongodb.client.result.InsertOneResult; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.samplemflix.exception.DatabaseOperationException; import com.mongodb.samplemflix.exception.ResourceNotFoundException; @@ -16,43 +14,63 @@ import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; +import org.bson.BsonValue; import org.bson.Document; import org.bson.types.ObjectId; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** - * Service layer for movie business logic. + * Service layer for movie business logic using Spring Data MongoDB. *

* This service handles: * - Business logic and validation - * - Query construction (filters, sorts, pagination) + * - Query construction using Spring Data MongoDB Query API * - Data transformation between DTOs and entities * - Error handling and exception throwing + *

+ * Uses both: + * - MovieRepository (Spring Data) for simple CRUD operations + * - MongoTemplate for complex queries and batch operations */ @Service public class MovieServiceImpl implements MovieService { private final MovieRepository movieRepository; + private final MongoTemplate mongoTemplate; private final ObjectMapper objectMapper; - public MovieServiceImpl(MovieRepository movieRepository, ObjectMapper objectMapper) { + public MovieServiceImpl(MovieRepository movieRepository, MongoTemplate mongoTemplate, ObjectMapper objectMapper) { this.movieRepository = movieRepository; + this.mongoTemplate = mongoTemplate; this.objectMapper = objectMapper; } @Override public List getAllMovies(MovieSearchQuery query) { - Document filter = buildFilter(query); - Document sort = buildSort(query.getSortBy(), query.getSortOrder()); - + Query mongoQuery = buildQuery(query); + int limit = Math.min(Math.max(query.getLimit() != null ? query.getLimit() : 20, 1), 100); int skip = Math.max(query.getSkip() != null ? query.getSkip() : 0, 0); - - return movieRepository.find(filter, sort, skip, limit); + + mongoQuery.skip(skip).limit(limit); + mongoQuery.with(buildSort(query.getSortBy(), query.getSortOrder())); + + return mongoTemplate.find(mongoQuery, Movie.class); } @Override @@ -70,7 +88,7 @@ public Movie createMovie(CreateMovieRequest request) { if (request.getTitle() == null || request.getTitle().trim().isEmpty()) { throw new ValidationException("Title is required"); } - + Movie movie = Movie.builder() .title(request.getTitle()) .year(request.getYear()) @@ -86,15 +104,9 @@ public Movie createMovie(CreateMovieRequest request) { .runtime(request.getRuntime()) .poster(request.getPoster()) .build(); - - InsertOneResult result = movieRepository.insertOne(movie); - if (!result.wasAcknowledged()) { - throw new DatabaseOperationException("Movie insertion was not acknowledged by the database"); - } - - return movieRepository.findById(result.getInsertedId().asObjectId().getValue()) - .orElseThrow(() -> new DatabaseOperationException("Failed to retrieve created movie")); + // Spring Data MongoDB's save() method inserts or updates + return movieRepository.save(movie); } @Override @@ -128,15 +140,17 @@ public BatchInsertResponse createMoviesBatch(List requests) .build()) .toList(); - InsertManyResult result = movieRepository.insertMany(movies); + // Spring Data MongoDB's saveAll() method for batch insert + List savedMovies = movieRepository.saveAll(movies); - if (!result.wasAcknowledged()) { - throw new DatabaseOperationException("Batch movie insertion was not acknowledged by the database"); - } + // Extract IDs from saved movies + Collection insertedIds = savedMovies.stream() + .map(movie -> new org.bson.BsonObjectId(movie.getId())) + .collect(Collectors.toList()); return new BatchInsertResponse( - result.getInsertedIds().size(), - result.getInsertedIds().values() + savedMovies.size(), + insertedIds ); } @@ -145,19 +159,25 @@ public Movie updateMovie(String id, UpdateMovieRequest request) { if (!ObjectId.isValid(id)) { throw new ValidationException("Invalid movie ID format"); } - + if (request == null || isUpdateRequestEmpty(request)) { throw new ValidationException("No update data provided"); } - - Document update = new Document("$set", buildUpdateDocument(request)); - UpdateResult result = movieRepository.updateOne(new ObjectId(id), update); - + + ObjectId objectId = new ObjectId(id); + + // Build Spring Data MongoDB Update object + Update update = buildUpdate(request); + + // Use MongoTemplate for update operation + Query query = new Query(Criteria.where("_id").is(objectId)); + UpdateResult result = mongoTemplate.updateFirst(query, update, Movie.class); + if (result.getMatchedCount() == 0) { throw new ResourceNotFoundException("Movie not found"); } - return movieRepository.findById(new ObjectId(id)) + return movieRepository.findById(objectId) .orElseThrow(() -> new DatabaseOperationException("Failed to retrieve updated movie")); } @@ -171,8 +191,15 @@ public BatchUpdateResponse updateMoviesBatch(Document filter, Document update) { throw new ValidationException("Update object cannot be empty"); } - Document setUpdate = new Document("$set", update); - UpdateResult result = movieRepository.updateMany(filter, setUpdate); + // Convert Document filter to Spring Data Query + Query query = new Query(); + filter.forEach((key, value) -> query.addCriteria(Criteria.where(key).is(value))); + + // Convert Document update to Spring Data Update + Update mongoUpdate = new Update(); + update.forEach(mongoUpdate::set); + + UpdateResult result = mongoTemplate.updateMulti(query, mongoUpdate, Movie.class); return new BatchUpdateResponse( result.getMatchedCount(), @@ -186,13 +213,16 @@ public DeleteResponse deleteMovie(String id) { throw new ValidationException("Invalid movie ID format"); } - DeleteResult result = movieRepository.deleteOne(new ObjectId(id)); + ObjectId objectId = new ObjectId(id); - if (result.getDeletedCount() == 0) { + // Check if movie exists before deleting + if (!movieRepository.existsById(objectId)) { throw new ResourceNotFoundException("Movie not found"); } - return new DeleteResponse(result.getDeletedCount()); + movieRepository.deleteById(objectId); + + return new DeleteResponse(1L); } @Override @@ -201,7 +231,11 @@ public DeleteResponse deleteMoviesBatch(Document filter) { throw new ValidationException("Filter object is required and cannot be empty. This prevents accidental deletion of all documents."); } - DeleteResult result = movieRepository.deleteMany(filter); + // Convert Document filter to Spring Data Query + Query query = new Query(); + filter.forEach((key, value) -> query.addCriteria(Criteria.where(key).is(value))); + + DeleteResult result = mongoTemplate.remove(query, Movie.class); return new DeleteResponse(result.getDeletedCount()); } @@ -211,56 +245,89 @@ public Movie findAndDeleteMovie(String id) { if (!ObjectId.isValid(id)) { throw new ValidationException("Invalid movie ID format"); } - - return movieRepository.findOneAndDelete(new ObjectId(id)) - .orElseThrow(() -> new ResourceNotFoundException("Movie not found")); + + ObjectId objectId = new ObjectId(id); + Query query = new Query(Criteria.where("_id").is(objectId)); + + Movie movie = mongoTemplate.findAndRemove(query, Movie.class); + + if (movie == null) { + throw new ResourceNotFoundException("Movie not found"); + } + + return movie; } - private Document buildFilter(MovieSearchQuery query) { - Document filter = new Document(); + /** + * Builds a Spring Data MongoDB Query from the search parameters. + */ + private Query buildQuery(MovieSearchQuery query) { + Query mongoQuery = new Query(); + // Text search if (query.getQ() != null && !query.getQ().trim().isEmpty()) { - filter.append("$text", new Document("$search", query.getQ())); + TextCriteria textCriteria = TextCriteria.forDefaultLanguage().matching(query.getQ()); + mongoQuery.addCriteria(textCriteria); } + // Genre filter (case-insensitive regex) if (query.getGenre() != null && !query.getGenre().trim().isEmpty()) { - filter.append(Movie.Fields.GENRES, new Document("$regex", Pattern.compile(query.getGenre(), Pattern.CASE_INSENSITIVE))); + mongoQuery.addCriteria(Criteria.where(Movie.Fields.GENRES) + .regex(Pattern.compile(query.getGenre(), Pattern.CASE_INSENSITIVE))); } + // Year filter if (query.getYear() != null) { - filter.append(Movie.Fields.YEAR, query.getYear()); + mongoQuery.addCriteria(Criteria.where(Movie.Fields.YEAR).is(query.getYear())); } + // Rating range filter if (query.getMinRating() != null || query.getMaxRating() != null) { - Document ratingFilter = new Document(); + Criteria ratingCriteria = Criteria.where(Movie.Fields.IMDB_RATING); if (query.getMinRating() != null) { - ratingFilter.append("$gte", query.getMinRating()); + ratingCriteria = ratingCriteria.gte(query.getMinRating()); } if (query.getMaxRating() != null) { - ratingFilter.append("$lte", query.getMaxRating()); + ratingCriteria = ratingCriteria.lte(query.getMaxRating()); } - filter.append(Movie.Fields.IMDB_RATING, ratingFilter); + mongoQuery.addCriteria(ratingCriteria); } - return filter; + return mongoQuery; } - - private Document buildSort(String sortBy, String sortOrder) { + + /** + * Builds a Spring Data Sort object from sort parameters. + */ + private Sort buildSort(String sortBy, String sortOrder) { String field = sortBy != null && !sortBy.trim().isEmpty() ? sortBy : Movie.Fields.TITLE; - int order = "desc".equalsIgnoreCase(sortOrder) ? -1 : 1; - return new Document(field, order); + Sort.Direction direction = "desc".equalsIgnoreCase(sortOrder) ? Sort.Direction.DESC : Sort.Direction.ASC; + return Sort.by(direction, field); } + /** + * Checks if the update request has any non-null fields. + */ private boolean isUpdateRequestEmpty(UpdateMovieRequest request) { @SuppressWarnings("unchecked") Map requestMap = objectMapper.convertValue(request, Map.class); return requestMap.values().stream().allMatch(java.util.Objects::isNull); } - - private Document buildUpdateDocument(UpdateMovieRequest request) { + + /** + * Builds a Spring Data MongoDB Update object from the update request. + */ + private Update buildUpdate(UpdateMovieRequest request) { @SuppressWarnings("unchecked") Map requestMap = objectMapper.convertValue(request, Map.class); - requestMap.values().removeIf(java.util.Objects::isNull); - return new Document(requestMap); + + Update update = new Update(); + requestMap.forEach((key, value) -> { + if (value != null) { + update.set(key, value); + } + }); + + return update; } } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java index df8a858..cefee5d 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.mongodb.client.result.DeleteResult; -import com.mongodb.client.result.InsertManyResult; -import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.samplemflix.exception.DatabaseOperationException; import com.mongodb.samplemflix.exception.ResourceNotFoundException; @@ -25,6 +23,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; import java.util.*; @@ -33,10 +33,10 @@ import static org.mockito.Mockito.*; /** - * Unit tests for MovieServiceImpl. + * Unit tests for MovieServiceImpl using Spring Data MongoDB. * * These tests verify the business logic of the service layer - * by mocking the repository layer dependencies. + * by mocking the repository and MongoTemplate dependencies. */ @ExtendWith(MockitoExtension.class) @DisplayName("MovieService Unit Tests") @@ -45,6 +45,9 @@ class MovieServiceTest { @Mock private MovieRepository movieRepository; + @Mock + private MongoTemplate mongoTemplate; + @Mock private ObjectMapper objectMapper; @@ -89,7 +92,7 @@ void testGetAllMovies_WithDefaults() { MovieSearchQuery query = MovieSearchQuery.builder().build(); List expectedMovies = Arrays.asList(testMovie); - when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(20))) + when(mongoTemplate.find(any(Query.class), eq(Movie.class))) .thenReturn(expectedMovies); // Act @@ -99,7 +102,7 @@ void testGetAllMovies_WithDefaults() { assertNotNull(result); assertEquals(1, result.size()); assertEquals(testMovie.getTitle(), result.get(0).getTitle()); - verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(20)); + verify(mongoTemplate).find(any(Query.class), eq(Movie.class)); } @Test @@ -112,7 +115,7 @@ void testGetAllMovies_WithCustomPagination() { .build(); List expectedMovies = Arrays.asList(testMovie); - when(movieRepository.find(any(Document.class), any(Document.class), eq(10), eq(50))) + when(mongoTemplate.find(any(Query.class), eq(Movie.class))) .thenReturn(expectedMovies); // Act @@ -121,7 +124,7 @@ void testGetAllMovies_WithCustomPagination() { // Assert assertNotNull(result); assertEquals(1, result.size()); - verify(movieRepository).find(any(Document.class), any(Document.class), eq(10), eq(50)); + verify(mongoTemplate).find(any(Query.class), eq(Movie.class)); } @Test @@ -132,14 +135,14 @@ void testGetAllMovies_EnforcesMaxLimit() { .limit(200) .build(); - when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(100))) + when(mongoTemplate.find(any(Query.class), eq(Movie.class))) .thenReturn(Collections.emptyList()); // Act movieService.getAllMovies(query); // Assert - verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(100)); + verify(mongoTemplate).find(any(Query.class), eq(Movie.class)); } @Test @@ -150,14 +153,14 @@ void testGetAllMovies_EnforcesMinLimit() { .limit(0) .build(); - when(movieRepository.find(any(Document.class), any(Document.class), eq(0), eq(1))) + when(mongoTemplate.find(any(Query.class), eq(Movie.class))) .thenReturn(Collections.emptyList()); // Act movieService.getAllMovies(query); // Assert - verify(movieRepository).find(any(Document.class), any(Document.class), eq(0), eq(1)); + verify(mongoTemplate).find(any(Query.class), eq(Movie.class)); } // ==================== GET MOVIE BY ID TESTS ==================== @@ -207,19 +210,14 @@ void testGetMovieById_NotFound() { @DisplayName("Should create movie successfully") void testCreateMovie_Success() { // Arrange - InsertOneResult insertResult = mock(InsertOneResult.class); - when(insertResult.wasAcknowledged()).thenReturn(true); - when(insertResult.getInsertedId()).thenReturn(new BsonObjectId(testId)); - when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); - when(movieRepository.findById(testId)).thenReturn(Optional.of(testMovie)); + when(movieRepository.save(any(Movie.class))).thenReturn(testMovie); // Act Movie result = movieService.createMovie(createRequest); // Assert assertNotNull(result); - verify(movieRepository).insertOne(any(Movie.class)); - verify(movieRepository).findById(testId); + verify(movieRepository).save(any(Movie.class)); } @Test @@ -233,7 +231,7 @@ void testCreateMovie_NullTitle() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.createMovie(invalidRequest)); - verify(movieRepository, never()).insertOne(any()); + verify(movieRepository, never()).save(any()); } @Test @@ -247,37 +245,7 @@ void testCreateMovie_EmptyTitle() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.createMovie(invalidRequest)); - verify(movieRepository, never()).insertOne(any()); - } - - @Test - @DisplayName("Should throw DatabaseOperationException when insert not acknowledged") - void testCreateMovie_NotAcknowledged() { - // Arrange - InsertOneResult insertResult = mock(InsertOneResult.class); - when(insertResult.wasAcknowledged()).thenReturn(false); - when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); - - // Act & Assert - assertThrows(DatabaseOperationException.class, () -> movieService.createMovie(createRequest)); - verify(movieRepository).insertOne(any(Movie.class)); - verify(movieRepository, never()).findById(any()); - } - - @Test - @DisplayName("Should throw DatabaseOperationException when created movie not found") - void testCreateMovie_CreatedMovieNotFound() { - // Arrange - InsertOneResult insertResult = mock(InsertOneResult.class); - when(insertResult.wasAcknowledged()).thenReturn(true); - when(insertResult.getInsertedId()).thenReturn(new BsonObjectId(testId)); - when(movieRepository.insertOne(any(Movie.class))).thenReturn(insertResult); - when(movieRepository.findById(testId)).thenReturn(Optional.empty()); - - // Act & Assert - assertThrows(DatabaseOperationException.class, () -> movieService.createMovie(createRequest)); - verify(movieRepository).insertOne(any(Movie.class)); - verify(movieRepository).findById(testId); + verify(movieRepository, never()).save(any()); } // ==================== CREATE MOVIES BATCH TESTS ==================== @@ -287,14 +255,9 @@ void testCreateMovie_CreatedMovieNotFound() { void testCreateMoviesBatch_Success() { // Arrange List requests = Arrays.asList(createRequest, createRequest); - InsertManyResult insertResult = mock(InsertManyResult.class); - Map insertedIds = new HashMap<>(); - insertedIds.put(0, new BsonObjectId(new ObjectId())); - insertedIds.put(1, new BsonObjectId(new ObjectId())); + List savedMovies = Arrays.asList(testMovie, testMovie); - when(insertResult.wasAcknowledged()).thenReturn(true); - when(insertResult.getInsertedIds()).thenReturn(insertedIds); - when(movieRepository.insertMany(anyList())).thenReturn(insertResult); + when(movieRepository.saveAll(anyList())).thenReturn(savedMovies); // Act BatchInsertResponse result = movieService.createMoviesBatch(requests); @@ -303,21 +266,7 @@ void testCreateMoviesBatch_Success() { assertNotNull(result); assertEquals(2, result.getInsertedCount()); assertNotNull(result.getInsertedIds()); - verify(movieRepository).insertMany(anyList()); - } - - @Test - @DisplayName("Should throw DatabaseOperationException when batch insert not acknowledged") - void testCreateMoviesBatch_NotAcknowledged() { - // Arrange - List requests = Arrays.asList(createRequest); - InsertManyResult insertResult = mock(InsertManyResult.class); - when(insertResult.wasAcknowledged()).thenReturn(false); - when(movieRepository.insertMany(anyList())).thenReturn(insertResult); - - // Act & Assert - assertThrows(DatabaseOperationException.class, () -> movieService.createMoviesBatch(requests)); - verify(movieRepository).insertMany(anyList()); + verify(movieRepository).saveAll(anyList()); } // ==================== UPDATE MOVIE TESTS ==================== @@ -335,7 +284,8 @@ void testUpdateMovie_Success() { UpdateResult updateResult = mock(UpdateResult.class); when(updateResult.getMatchedCount()).thenReturn(1L); - when(movieRepository.updateOne(eq(testId), any(Document.class))).thenReturn(updateResult); + when(mongoTemplate.updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class))) + .thenReturn(updateResult); when(movieRepository.findById(testId)).thenReturn(Optional.of(testMovie)); // Act @@ -343,7 +293,7 @@ void testUpdateMovie_Success() { // Assert assertNotNull(result); - verify(movieRepository).updateOne(eq(testId), any(Document.class)); + verify(mongoTemplate).updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class)); verify(movieRepository).findById(testId); } @@ -355,7 +305,7 @@ void testUpdateMovie_InvalidId() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.updateMovie(invalidId, updateRequest)); - verify(movieRepository, never()).updateOne(any(), any()); + verify(mongoTemplate, never()).updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class)); } @Test @@ -370,7 +320,7 @@ void testUpdateMovie_EmptyRequest() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.updateMovie(validId, emptyRequest)); - verify(movieRepository, never()).updateOne(any(), any()); + verify(mongoTemplate, never()).updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class)); } @Test @@ -385,11 +335,12 @@ void testUpdateMovie_NotFound() { UpdateResult updateResult = mock(UpdateResult.class); when(updateResult.getMatchedCount()).thenReturn(0L); - when(movieRepository.updateOne(eq(testId), any(Document.class))).thenReturn(updateResult); + when(mongoTemplate.updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class))) + .thenReturn(updateResult); // Act & Assert assertThrows(ResourceNotFoundException.class, () -> movieService.updateMovie(validId, updateRequest)); - verify(movieRepository).updateOne(eq(testId), any(Document.class)); + verify(mongoTemplate).updateFirst(any(Query.class), any(org.springframework.data.mongodb.core.query.Update.class), any(Class.class)); verify(movieRepository, never()).findById(any()); } @@ -400,9 +351,7 @@ void testUpdateMovie_NotFound() { void testDeleteMovie_Success() { // Arrange String validId = testId.toHexString(); - DeleteResult deleteResult = mock(DeleteResult.class); - when(deleteResult.getDeletedCount()).thenReturn(1L); - when(movieRepository.deleteOne(testId)).thenReturn(deleteResult); + when(movieRepository.existsById(testId)).thenReturn(true); // Act DeleteResponse result = movieService.deleteMovie(validId); @@ -410,7 +359,8 @@ void testDeleteMovie_Success() { // Assert assertNotNull(result); assertEquals(1L, result.getDeletedCount()); - verify(movieRepository).deleteOne(testId); + verify(movieRepository).existsById(testId); + verify(movieRepository).deleteById(testId); } @Test @@ -421,7 +371,7 @@ void testDeleteMovie_InvalidId() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.deleteMovie(invalidId)); - verify(movieRepository, never()).deleteOne(any()); + verify(movieRepository, never()).deleteById(any()); } @Test @@ -429,13 +379,12 @@ void testDeleteMovie_InvalidId() { void testDeleteMovie_NotFound() { // Arrange String validId = testId.toHexString(); - DeleteResult deleteResult = mock(DeleteResult.class); - when(deleteResult.getDeletedCount()).thenReturn(0L); - when(movieRepository.deleteOne(testId)).thenReturn(deleteResult); + when(movieRepository.existsById(testId)).thenReturn(false); // Act & Assert assertThrows(ResourceNotFoundException.class, () -> movieService.deleteMovie(validId)); - verify(movieRepository).deleteOne(testId); + verify(movieRepository).existsById(testId); + verify(movieRepository, never()).deleteById(any()); } // ==================== FIND AND DELETE MOVIE TESTS ==================== @@ -445,7 +394,7 @@ void testDeleteMovie_NotFound() { void testFindAndDeleteMovie_Success() { // Arrange String validId = testId.toHexString(); - when(movieRepository.findOneAndDelete(testId)).thenReturn(Optional.of(testMovie)); + when(mongoTemplate.findAndRemove(any(Query.class), eq(Movie.class))).thenReturn(testMovie); // Act Movie result = movieService.findAndDeleteMovie(validId); @@ -453,7 +402,7 @@ void testFindAndDeleteMovie_Success() { // Assert assertNotNull(result); assertEquals(testMovie.getTitle(), result.getTitle()); - verify(movieRepository).findOneAndDelete(testId); + verify(mongoTemplate).findAndRemove(any(Query.class), eq(Movie.class)); } @Test @@ -464,7 +413,7 @@ void testFindAndDeleteMovie_InvalidId() { // Act & Assert assertThrows(ValidationException.class, () -> movieService.findAndDeleteMovie(invalidId)); - verify(movieRepository, never()).findOneAndDelete(any()); + verify(mongoTemplate, never()).findAndRemove(any(), any()); } @Test @@ -472,10 +421,10 @@ void testFindAndDeleteMovie_InvalidId() { void testFindAndDeleteMovie_NotFound() { // Arrange String validId = testId.toHexString(); - when(movieRepository.findOneAndDelete(testId)).thenReturn(Optional.empty()); + when(mongoTemplate.findAndRemove(any(Query.class), eq(Movie.class))).thenReturn(null); // Act & Assert assertThrows(ResourceNotFoundException.class, () -> movieService.findAndDeleteMovie(validId)); - verify(movieRepository).findOneAndDelete(testId); + verify(mongoTemplate).findAndRemove(any(Query.class), eq(Movie.class)); } } From 360d825c58b4d8674d884f2e15827e4880722b25 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 16:25:10 -0400 Subject: [PATCH 021/110] update TODOs --- server/java-spring/README.md | 42 +- server/java-spring/TODO.md | 372 ++++++++++++++++++ .../mongodb/samplemflix/model/Comment.java | 43 +- .../mongodb/samplemflix/model/Theater.java | 29 ++ .../samplemflix/service/MovieServiceImpl.java | 9 + .../samplemflix/util/ValidationUtils.java | 29 +- .../integration/MovieIntegrationTest.java | 42 +- 7 files changed, 532 insertions(+), 34 deletions(-) create mode 100644 server/java-spring/TODO.md diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 54f4498..6f4d7ac 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -102,7 +102,7 @@ Once the application is running, you can access: ## API Endpoints -### Movies +### Movies (✅ Implemented) - `GET /api/movies` - Get all movies (with filtering, sorting, pagination) - `GET /api/movies/{id}` - Get a single movie by ID @@ -114,7 +114,14 @@ Once the application is running, you can access: - `DELETE /api/movies` - Delete multiple movies - `DELETE /api/movies/{id}/find-and-delete` - Find and delete a movie -> **Note**: Full endpoint implementation is planned for later phases. See the implementation plan for details. +### Planned Endpoints + +See [TODO.md](TODO.md) for a complete list of planned features including: +- Comments API +- Users API +- Sessions API (Authentication) +- Theaters API (with geospatial queries) +- Advanced movie features (aggregations, recommendations) ## Development @@ -137,19 +144,28 @@ java -jar target/sample-mflix-spring-1.0.0.jar ## Implementation Status -This project is being implemented in phases: +### ✅ Completed Features + +- **Movies CRUD API** - Full create, read, update, delete operations +- **Spring Data MongoDB** - Repository pattern with MongoTemplate for complex queries +- **Text Search** - Full-text search on movie titles, plots, and genres +- **Filtering & Pagination** - Query parameters for filtering, sorting, and pagination +- **Custom Exception Handling** - Global exception handler with proper HTTP status codes +- **Type-Safe DTOs** - Specific response types instead of generic Maps +- **Unit Tests** - 35 tests covering service and controller layers +- **OpenAPI Documentation** - Swagger UI available at `/swagger-ui.html` +- **Database Verification** - Startup checks for database connectivity and indexes + +### 🚧 In Progress / Planned -- ✅ **Phase 1**: Project Setup and Configuration (CURRENT) -- ⏳ **Phase 2**: Database Configuration and Connection -- ⏳ **Phase 3**: Model Layer Implementation -- ⏳ **Phase 4**: Repository Layer -- ⏳ **Phase 5**: Service Layer -- ⏳ **Phase 6**: Controller Layer -- ⏳ **Phase 7**: Error Handling -- ⏳ **Phase 8**: Testing -- ⏳ **Phase 9**: Documentation and Polish +See [TODO.md](TODO.md) for a comprehensive list of planned features and improvements, including: -See `JAVA-SPRING-IMPLEMENTATION-PLAN.md` in the repository root for the complete implementation plan. +- Additional collections (Comments, Users, Sessions, Theaters) +- Authentication & Authorization (Spring Security + JWT) +- Integration tests with Testcontainers +- Advanced queries (aggregations, geospatial, vector search) +- Performance optimization and caching +- Monitoring and observability ## Technology Stack diff --git a/server/java-spring/TODO.md b/server/java-spring/TODO.md new file mode 100644 index 0000000..8be6099 --- /dev/null +++ b/server/java-spring/TODO.md @@ -0,0 +1,372 @@ +# TODO List - Java Spring Boot MongoDB Sample Application + +This document tracks remaining functionality and improvements for the Java Spring Boot backend based on the **Requirements for Sample Repositories for MongoDB Atlas** and **Sample Application Scoping** documents. + +## 📋 Scope Alignment + +This sample application must demonstrate: +- ✅ **Basic CRUD** - insertOne(), insertMany(), findOne(), find(), updateOne(), updateMany(), deleteOne(), deleteMany(), findOneAndDelete() +- 🚧 **Aggregations** - Reporting by comments, year, and director +- 🚧 **Search** - Full-text search using Search Index on plot +- 🚧 **Vector Search** - Find similar movies based on plot embeddings +- 🚧 **Geospatial** - Find theaters near coordinates +- 🚧 **Recommendations** - Potentially using Vector Search + +## ✅ Completed Features (In Scope) + +- [x] **Basic CRUD operations for Movies collection** + - [x] insertOne() - `POST /api/movies` + - [x] insertMany() - `POST /api/movies/batch` + - [x] findOne() - `GET /api/movies/{id}` + - [x] find() - `GET /api/movies` + - [x] updateOne() - `PUT /api/movies/{id}` + - [x] updateMany() - `PATCH /api/movies` + - [x] deleteOne() - `DELETE /api/movies/{id}` + - [x] deleteMany() - `DELETE /api/movies` + - [x] findOneAndDelete() - `DELETE /api/movies/{id}/find-and-delete` +- [x] Spring Data MongoDB migration +- [x] Filtering, sorting, and pagination +- [x] Custom exception handling +- [x] Type-safe response DTOs +- [x] Unit tests for service and controller layers (35 tests) +- [x] OpenAPI/Swagger documentation +- [x] Database verification on startup (checks indexes and data) +- [x] CORS configuration +- [x] Field name constants for type safety +- [x] Environment variable configuration (.env support) + +## 🚧 High Priority TODOs (Required by Scope) + +### 1. Aggregations (REQUIRED) + +**Scope Requirement**: Implement aggregation pipelines for reporting + +- [ ] **GET /api/movies/reportingByComments** - Aggregate movies with most comments + - [ ] Create aggregation pipeline using `$lookup` to join with comments collection + - [ ] Sort by comment count descending + - [ ] Add pagination support + - [ ] Add unit tests + +- [ ] **GET /api/movies/reportingByYear** - Aggregate movies by year with average rating + - [ ] Create aggregation pipeline grouping by year + - [ ] Calculate average IMDB rating per year + - [ ] Sort by year + - [ ] Add unit tests + +- [ ] **GET /api/movies/reportingByDirector** - Aggregate directors with most movies + - [ ] Create aggregation pipeline using `$unwind` on directors array + - [ ] Group by director and count movies + - [ ] Sort by movie count descending + - [ ] Add pagination support + - [ ] Add unit tests + +### 2. Search (REQUIRED) + +**Scope Requirement**: Implement Full-Text Search and Vector Search + +- [ ] **GET /api/movies/searchByPlot** - Text Search using Search Index + - [ ] Verify text index exists on plot, fullplot, genres, title fields + - [ ] Implement text search using Spring Data MongoDB TextCriteria + - [ ] Add relevance scoring + - [ ] Add pagination support + - [ ] Add unit tests + - [ ] Document index requirements in README + +- [ ] **GET /api/movies/findSimilarMovies** - Vector Search for similar movies + - [ ] Implement vector search on `plot_embedding` field + - [ ] Use MongoDB Atlas Vector Search + - [ ] Add similarity threshold parameter + - [ ] Add limit parameter + - [ ] Add unit tests + - [ ] Document vector search index requirements + +### 3. Geospatial (REQUIRED) + +**Scope Requirement**: Find theaters near coordinates + +- [ ] **GET /api/theaters/findTheatersNearCoordinates** - Geospatial query + - [ ] Create `Theater` entity (already exists, needs repository) + - [ ] Create `TheaterRepository` with geospatial query methods + - [ ] Create `TheaterService` and `TheaterServiceImpl` + - [ ] Create `TheaterController` with endpoint + - [ ] Implement `$near` or `$geoNear` query + - [ ] Add distance parameter (in meters/miles) + - [ ] Add limit parameter + - [ ] Verify geospatial index exists on `location.geo` + - [ ] Add unit tests + - [ ] Add integration tests + +### 4. Recommendations (OPTIONAL - FUTURE) + +**Scope Requirement**: Potentially use Vector Search to power recommendations + +- [ ] Design recommendation algorithm +- [ ] Implement using Vector Search on plot embeddings +- [ ] Consider user preferences/history +- [ ] Add endpoint `GET /api/movies/recommendations` +- [ ] Add tests + +### 5. Comments Collection Support (For Aggregations) + +**Note**: Required to support "reportingByComments" aggregation + +- [ ] Verify `Comment` entity model (already exists) +- [ ] Create `CommentRepository` interface +- [ ] Create `CommentService` and `CommentServiceImpl` +- [ ] Create `CommentController` with basic endpoints: + - `GET /api/comments/movie/{movieId}` - Get comments for a specific movie + - `POST /api/comments` - Create a new comment +- [ ] Add tests for Comment functionality + +### 6. Testing (REQUIRED) + +**Scope Requirement**: Unit tests and E2E tests + +- [ ] **Unit Tests** - Test individual features and driver interactions + - [x] Service layer unit tests (21 tests completed) + - [x] Controller layer unit tests (14 tests completed) + - [ ] Add tests for aggregation methods + - [ ] Add tests for search methods + - [ ] Add tests for geospatial queries + - [ ] Add tests for vector search + +- [ ] **Integration Tests** - Test complete flows + - [ ] Set up Testcontainers for MongoDB + - [ ] Create integration tests for all CRUD endpoints + - [ ] Create integration tests for aggregations + - [ ] Create integration tests for search + - [ ] Create integration tests for geospatial queries + - [ ] Test error handling and edge cases + +### 7. Error Handling & Verification (REQUIRED) + +**Scope Requirement**: Pre-flight checks and error handling + +- [x] Verify requirements function on app start (DatabaseVerification.java) + - [x] Check for indexes + - [x] Check for sample data + - [x] Create indexes if missing +- [x] Basic error handling implemented + - [x] Connection errors + - [x] Invalid input validation + - [x] Custom exceptions (ValidationException, ResourceNotFoundException, DatabaseOperationException) + - [x] Global exception handler +- [ ] Enhance error handling for new features: + - [ ] Vector search errors + - [ ] Geospatial query errors + - [ ] Aggregation pipeline errors + +### 8. Documentation (REQUIRED) + +**Scope Requirement**: Clear README and documentation + +- [x] README.md with prerequisites and setup +- [x] Environment variable configuration (.env.example) +- [x] OpenAPI/Swagger documentation +- [ ] Update README with: + - [ ] Aggregation endpoint documentation + - [ ] Search endpoint documentation + - [ ] Vector search setup instructions + - [ ] Geospatial query examples + - [ ] Index requirements +- [ ] Add code comments for MongoDB features + - [ ] Comment aggregation pipelines + - [ ] Comment vector search implementation + - [ ] Comment geospatial queries + - [ ] Note where readability was prioritized over performance + +## 📝 Medium Priority TODOs (Out of Scope - Future Enhancements) + +### 9. Additional Collections (Optional) + +**Note**: These collections are not required by the current scope but may be useful for future enhancements. + +#### Users Collection (`sample_mflix.users`) +- [ ] Create `UserRepository` interface (model already exists) +- [ ] Create `UserService` and `UserServiceImpl` +- [ ] Implement password hashing (BCrypt) +- [ ] Add email validation +- [ ] Add tests for User functionality + +#### Sessions Collection (`sample_mflix.sessions`) +- [ ] Create `SessionRepository` interface (model already exists) +- [ ] Create `SessionService` and `SessionServiceImpl` +- [ ] Implement JWT token generation and validation +- [ ] Add tests for Session functionality + +#### Embedded Movies Collection (`sample_mflix.embedded_movies`) +- [ ] Investigate purpose and schema of this collection +- [ ] Determine if separate endpoints are needed +- [ ] Document relationship to main `movies` collection + +### 10. Authentication & Authorization (Out of Scope) + +**Note**: Not required by current scope, but useful for production applications + +- [ ] Implement Spring Security +- [ ] Add JWT authentication filter +- [ ] Create `@PreAuthorize` annotations for role-based access +- [ ] Implement user roles (USER, ADMIN) +- [ ] Add authentication to existing endpoints +- [ ] Create authentication integration tests + +### 11. API Improvements (Out of Scope) + +- [ ] Add HATEOAS links to responses +- [ ] Implement API versioning (e.g., `/api/v1/movies`) +- [ ] Add request/response compression +- [ ] Implement rate limiting +- [ ] Add API key authentication option +- [ ] Create GraphQL endpoint as alternative to REST + +### 12. Data Validation & Quality (Partially Complete) + +- [x] Basic Jakarta Validation annotations +- [ ] Create custom validators for complex business rules +- [ ] Implement data sanitization for user inputs +- [ ] Add validation for embedded documents +- [ ] Create validation tests + +### 13. Performance Optimization (Out of Scope) + +**Note**: Scope prioritizes readability over performance + +- [ ] Implement caching with Spring Cache (Redis/Caffeine) +- [ ] Add database query optimization +- [ ] Implement connection pooling tuning +- [ ] Create performance benchmarks +- [ ] Implement lazy loading for large collections +- [ ] Document performance vs readability tradeoffs + +### 14. Monitoring & Observability (Out of Scope) + +- [ ] Add Spring Boot Actuator endpoints +- [ ] Implement custom health checks +- [ ] Add metrics collection (Micrometer) +- [ ] Integrate with Prometheus/Grafana +- [ ] Add distributed tracing (Sleuth/Zipkin) +- [ ] Implement structured logging (JSON format) + +### 15. Code Quality (Partially Complete) + +- [x] Unit tests for core functionality (35 tests) +- [ ] Increase test coverage to >80% +- [ ] Add code coverage reporting (JaCoCo) +- [ ] Add mutation testing (PIT) +- [ ] Integrate SonarQube for code quality analysis +- [ ] Add Checkstyle/PMD for code style enforcement + +### 16. Build & Deployment (Out of Scope) + +**Note**: Deployment is handled by Growth team per scope document + +- [ ] Create Dockerfile for containerization +- [ ] Add Docker Compose for local development +- [ ] Create Kubernetes deployment manifests +- [ ] Set up CI/CD pipeline (GitHub Actions) +- [x] Automated dependency updates (Dependabot) - handled by DevDocs team + +### 17. Security Enhancements (Out of Scope) + +**Note**: Basic security implemented, advanced features out of scope + +- [x] CORS configuration +- [x] Input validation +- [ ] Implement HTTPS/TLS +- [ ] Add CSRF protection +- [ ] Implement security headers (HSTS, CSP, etc.) +- [ ] Add security scanning (OWASP Dependency Check) + +### 18. Developer Experience (Partially Complete) + +- [x] Environment variable configuration +- [x] Database verification on startup +- [x] OpenAPI/Swagger documentation +- [ ] Add Spring Boot DevTools for hot reload +- [ ] Create seed data script for development +- [ ] Add API client examples (Postman collection) + +## 🐛 Known Issues + +- [ ] Update deprecated `@MockBean` to new Spring Boot 3.5+ alternative (warning in tests) + +## 📚 Technical Debt + +- None currently - code follows best practices and scope requirements + +## 🎯 Next Immediate Steps (Priority Order) + +Based on the **Sample Application Scoping** document, the next steps are: + +### Phase 1: Complete Required Features (In Scope) +1. **Aggregations** (REQUIRED) + - Implement `GET /api/movies/reportingByComments` + - Implement `GET /api/movies/reportingByYear` + - Implement `GET /api/movies/reportingByDirector` + +2. **Search** (REQUIRED) + - Implement `GET /api/movies/searchByPlot` (Full-Text Search) + - Implement `GET /api/movies/findSimilarMovies` (Vector Search) + +3. **Geospatial** (REQUIRED) + - Implement `GET /api/theaters/findTheatersNearCoordinates` + - Create Theater repository and service + +4. **Comments Collection** (REQUIRED for aggregations) + - Create CommentRepository and CommentService + - Implement basic endpoints to support aggregations + +5. **Testing** (REQUIRED) + - Add unit tests for all new features + - Create integration tests with Testcontainers + +6. **Documentation** (REQUIRED) + - Update README with new endpoints + - Document index requirements + - Add code comments explaining MongoDB features + - Note readability vs performance tradeoffs + +### Phase 2: Quality Assurance (In Scope) +7. **Error Handling** - Enhance for new features +8. **Code Comments** - Add more comments than typical production app +9. **Readability Review** - Ensure code prioritizes clarity over performance + +### Phase 3: Future Enhancements (Out of Scope) +10. Recommendations using Vector Search +11. Authentication & Authorization +12. Additional collections (Users, Sessions) +13. Performance optimizations +14. Advanced features + +## 📋 Scope Compliance Checklist + +### Required Features +- [x] **Basic CRUD** - All methods implemented ✅ + - [x] insertOne(), insertMany() + - [x] findOne(), find() + - [x] updateOne(), updateMany() + - [x] deleteOne(), deleteMany(), findOneAndDelete() +- [ ] **Aggregations** - Not yet implemented ⏳ + - [ ] Reporting by comments + - [ ] Reporting by year + - [ ] Reporting by director +- [ ] **Search** - Not yet implemented ⏳ + - [ ] Full-text search on plot +- [ ] **Vector Search** - Not yet implemented ⏳ + - [ ] Find similar movies +- [ ] **Geospatial** - Not yet implemented ⏳ + - [ ] Find theaters near coordinates + +### Quality Standards +- [x] **Error Handling** - Basic implementation complete ✅ +- [x] **Testing** - Unit tests complete (35 tests) ✅ +- [ ] **Integration Tests** - Pending ⏳ +- [x] **Documentation** - README complete ✅ +- [ ] **Documentation Updates** - Needs updates for new features ⏳ +- [x] **Readability** - Code follows best practices ✅ +- [x] **Code Comments** - Good coverage, needs more for complex features ⏳ +- [x] **Minimal Dependencies** - Using only Spring Boot and MongoDB ✅ +- [x] **Environment Variables** - Implemented with .env.example ✅ +- [x] **Apache License** - Applied ✅ +- [x] **Conventional Commits** - Following standard ✅ +- [x] **Clean Commit History** - Maintained ✅ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java index e7a61f4..580921d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; import java.util.Date; @@ -13,42 +16,70 @@ *

* This class maps to the comments collection in the sample_mflix database. * Comments are user reviews/comments associated with movies. + * + * TODO: Implement Comment functionality: + * - Create CommentRepository extending MongoRepository + * - Create CommentService and CommentServiceImpl + * - Create CommentController with REST endpoints + * - Add validation annotations (@NotNull, @Email, etc.) + * - Add unit tests for Comment service and controller + * - Add integration tests + * - Implement query methods (findByMovieId, findByEmail, etc.) */ @Data @Builder @NoArgsConstructor @AllArgsConstructor +@Document(collection = "comments") public class Comment { - + /** * MongoDB document ID. * Maps to the _id field in MongoDB. */ + @Id private ObjectId id; - + /** * Name of the commenter. */ private String name; - + /** * Email address of the commenter. */ private String email; - + /** * ID of the movie this comment is associated with. * References a document in the movies collection. */ + @Field("movie_id") private ObjectId movieId; - + /** * Comment text content. */ private String text; - + /** * Date when the comment was posted. */ private Date date; + + /** + * Field name constants for type-safe queries. + */ + public static class Fields { + public static final String ID = "_id"; + public static final String NAME = "name"; + public static final String EMAIL = "email"; + public static final String MOVIE_ID = "movie_id"; + public static final String TEXT = "text"; + public static final String DATE = "date"; + + private Fields() { + // Private constructor to prevent instantiation + } + } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java index 2dc8161..c76511b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java @@ -5,23 +5,37 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; /** * Domain model representing a theater document from the MongoDB theaters collection. *

* This class maps to the theaters collection in the sample_mflix database. * It includes location information with address and geospatial coordinates. + * + * TODO: Implement Theater functionality: + * - Create TheaterRepository extending MongoRepository + * - Create TheaterService and TheaterServiceImpl + * - Create TheaterController with REST endpoints + * - Implement geospatial queries (findNear, findWithinRadius) + * - Add validation annotations + * - Add unit tests for Theater service and controller + * - Add integration tests with geospatial queries + * - Add GeoJSON support for location queries */ @Data @Builder @NoArgsConstructor @AllArgsConstructor +@Document(collection = "theaters") public class Theater { /** * MongoDB document ID. * Maps to the _id field in MongoDB. */ + @Id private ObjectId id; /** @@ -102,4 +116,19 @@ public static class Geo { private double[] coordinates; } } + + /** + * Field name constants for type-safe queries. + */ + public static class Fields { + public static final String ID = "_id"; + public static final String THEATER_ID = "theaterId"; + public static final String LOCATION = "location"; + public static final String LOCATION_GEO = "location.geo"; + public static final String LOCATION_ADDRESS = "location.address"; + + private Fields() { + // Private constructor to prevent instantiation + } + } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index a2fe24d..8cfea02 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -330,4 +330,13 @@ private Update buildUpdate(UpdateMovieRequest request) { return update; } + + // TODO: Add advanced query methods + // - getMoviesByGenreStatistics() - Aggregation pipeline for genre statistics + // - getTopRatedMovies(int limit) - Movies sorted by rating + // - getMoviesByDecade(int decade) - Movies from a specific decade + // - getDirectorFilmography(String director) - All movies by a director + // - getActorFilmography(String actor) - All movies featuring an actor + // - searchSimilarMovies(String movieId) - Vector search on plot_embedding field + // - getMovieRecommendations(String userId) - Personalized recommendations } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java index 4c28104..ab46c8d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java @@ -2,17 +2,32 @@ /** * Utility class for validation operations. - * + * * This class provides helper methods for validating request data. - * - * TODO: Phase 5 - Implement validation utility methods + * + * TODO: Implement validation utility methods: + * - Email validation + * - Password strength validation + * - ObjectId format validation + * - Date range validation + * - String sanitization + * - URL validation */ public class ValidationUtils { - + private ValidationUtils() { // Private constructor to prevent instantiation } - - // TODO: Phase 5 - Add validation methods -} + // TODO: Add email validation method + // public static boolean isValidEmail(String email) + + // TODO: Add password strength validation + // public static boolean isStrongPassword(String password) + + // TODO: Add ObjectId validation (currently duplicated in service layer) + // public static boolean isValidObjectId(String id) + + // TODO: Add string sanitization for XSS prevention + // public static String sanitize(String input) +} diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java index 92d6e2b..200368f 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java @@ -2,15 +2,41 @@ /** * Integration tests for the movie API. - * + * * These tests verify the full request/response cycle including database operations. - * - * TODO: Phase 8 - Implement integration tests - * TODO: Phase 8 - Set up test MongoDB instance (Testcontainers or embedded) - * TODO: Phase 8 - Test full request/response cycle + * + * TODO: Set up Testcontainers for MongoDB + * - Add testcontainers dependency to pom.xml + * - Configure MongoDB test container + * - Set up test data initialization + * + * TODO: Implement integration tests for all endpoints: + * - GET /api/movies (with various filters) + * - GET /api/movies/{id} + * - POST /api/movies + * - POST /api/movies/batch + * - PUT /api/movies/{id} + * - PATCH /api/movies + * - DELETE /api/movies/{id} + * - DELETE /api/movies + * - DELETE /api/movies/{id}/find-and-delete + * + * TODO: Test error scenarios: + * - Invalid ObjectId format + * - Resource not found + * - Validation errors + * - Database connection failures + * + * TODO: Test performance: + * - Large dataset queries + * - Pagination performance + * - Text search performance */ public class MovieIntegrationTest { - - // TODO: Phase 8 - Add integration test methods -} + // TODO: Add @SpringBootTest annotation + // TODO: Add @Testcontainers annotation + // TODO: Add MongoDB container configuration + // TODO: Add test data setup methods + // TODO: Add integration test methods +} From 489c950e78a396a161c016c96e11a68181a91b0b Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 16:44:05 -0400 Subject: [PATCH 022/110] rename app --- server/java-spring/README.md | 2 +- server/java-spring/pom.xml | 6 +++--- .../com/mongodb/samplemflix/SampleMflixApplication.java | 2 +- .../java-spring/src/main/resources/application.properties | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 6f4d7ac..f249d24 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -1,4 +1,4 @@ -# MongoDB Sample MFlix - Java Spring Boot Backend [DRAFT] +# sample-app-java-mflix (INTERNAL) A Spring Boot REST API demonstrating MongoDB CRUD operations using Spring Data MongoDB with the sample_mflix database. diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index bd35bdf..c5b8b90 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -13,10 +13,10 @@ com.mongodb - sample-mflix-spring + sample-app-java-mflix 1.0.0 - MongoDB Sample MFlix Spring API - Spring Boot backend for MongoDB sample mflix application demonstrating CRUD operations using Spring Data MongoDB + sample-app-java-mflix + Java Spring Boot backend for MongoDB sample_mflix application demonstrating CRUD operations using Spring Data MongoDB 17 diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java index 86ea28f..b2e199a 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -31,7 +31,7 @@ public static void main(String[] args) { @GetMapping("/") public Map root() { return Map.of( - "name", "MongoDB Sample MFlix API", + "name", "sample-app-java-mflix", "version", "1.0.0", "description", "Java Spring Boot backend demonstrating MongoDB operations with the sample_mflix dataset", "endpoints", Map.of("movies", "/api/movies") diff --git a/server/java-spring/src/main/resources/application.properties b/server/java-spring/src/main/resources/application.properties index 46705ba..ff84d9a 100644 --- a/server/java-spring/src/main/resources/application.properties +++ b/server/java-spring/src/main/resources/application.properties @@ -12,7 +12,7 @@ server.port=${PORT:3001} cors.allowed.origins=${CORS_ORIGIN:http://localhost:3000} # Application Info -spring.application.name=MongoDB Sample MFlix API +spring.application.name=sample-app-java-mflix # Logging Configuration logging.level.com.mongodb.samplemflix=INFO From 5b6f266b97b37f4b5ba1e5d2556f15736b43061d Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 16:47:34 -0400 Subject: [PATCH 023/110] remove unnecessary file --- server/express/src/utils/errorHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/express/src/utils/errorHandler.ts b/server/express/src/utils/errorHandler.ts index 83a2b54..929cf29 100644 --- a/server/express/src/utils/errorHandler.ts +++ b/server/express/src/utils/errorHandler.ts @@ -28,13 +28,13 @@ export class ValidationError extends Error { * @param err - The error that was thrown * @param req - Express request object * @param res - Express response object - * @param _next - Express next function + * @param next - Express next function */ export function errorHandler( err: Error, req: Request, res: Response, - _next: NextFunction + next: NextFunction ): void { // Log the error for debugging purposes // In production, we recommend using a logging service From 605d8390945103fb1bfa29b4fce63b0121c70fda Mon Sep 17 00:00:00 2001 From: cbullinger Date: Tue, 28 Oct 2025 16:51:37 -0400 Subject: [PATCH 024/110] update README --- server/java-spring/README.md | 4 ++-- server/java-spring/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index f249d24..c874739 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -170,10 +170,10 @@ See [TODO.md](TODO.md) for a comprehensive list of planned features and improvem ## Technology Stack - **Framework**: Spring Boot 3.5.7 -- **Java Version**: 17 +- **Java Version**: 21 - **MongoDB**: Spring Data MongoDB 4.5.5 - **Build Tool**: Maven -- **API Documentation**: SpringDoc OpenAPI 2.3.0 +- **API Documentation**: SpringDoc OpenAPI 2.8.13 - **Testing**: JUnit 5, Mockito, Spring Boot Test ## Educational Purpose diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index c5b8b90..dcd237a 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -19,7 +19,7 @@ Java Spring Boot backend for MongoDB sample_mflix application demonstrating CRUD operations using Spring Data MongoDB - 17 + 21 2.8.13 4.0.0 From c23c70912a6ffb0ad3413652a60c17922f2c3c20 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 09:18:08 -0400 Subject: [PATCH 025/110] Apply Vitaliy feedback --- server/express/.gitignore | 30 ++++++++++++------- server/java-spring/.gitignore | 14 ++++----- server/java-spring/pom.xml | 21 +++++++++++++ .../samplemflix/SampleMflixApplication.java | 3 +- .../samplemflix/config/CorsConfig.java | 3 +- .../config/DatabaseVerification.java | 3 +- .../samplemflix/config/MongoConfig.java | 3 +- .../controller/MovieControllerImpl.java | 7 ++--- .../exception/GlobalExceptionHandler.java | 3 +- .../mongodb/samplemflix/model/Comment.java | 18 +---------- .../com/mongodb/samplemflix/model/Movie.java | 5 ++-- .../model/dto/BatchInsertResponse.java | 3 +- .../model/dto/CreateMovieRequest.java | 3 +- .../model/dto/UpdateMovieRequest.java | 3 +- .../model/response/ErrorResponse.java | 3 +- .../model/response/SuccessResponse.java | 3 +- .../samplemflix/service/MovieService.java | 3 +- .../samplemflix/service/MovieServiceImpl.java | 16 ++++------ .../controller/MovieControllerTest.java | 27 ++++++++--------- .../samplemflix/service/MovieServiceTest.java | 15 ++++------ 20 files changed, 88 insertions(+), 98 deletions(-) diff --git a/server/express/.gitignore b/server/express/.gitignore index d0ebe3f..db4c02d 100644 --- a/server/express/.gitignore +++ b/server/express/.gitignore @@ -1,16 +1,26 @@ -# ============================================================================= -# Express.js Backend - Specific Ignores -# ============================================================================= -# Common patterns (node_modules, .env, .DS_Store, etc.) are in root .gitignore -# This file only contains Express/TypeScript-specific patterns -# ============================================================================= +# Dependencies +node_modules/ +npm-debug.log* +package-lock.json + +# Environment variables +.env -# Build Output +# Build output dist/ -build/ # TypeScript *.tsbuildinfo -# Package Lock -package-lock.json +# Logs +logs +*.log + +# Test coverage +coverage/ + +# Optional npm cache directory +.npm + +# macOS +.DS_Store diff --git a/server/java-spring/.gitignore b/server/java-spring/.gitignore index 4a4b992..7cf3f4a 100644 --- a/server/java-spring/.gitignore +++ b/server/java-spring/.gitignore @@ -1,5 +1,10 @@ # Java/Spring Boot - Specific Ignores -# Common patterns (.env, .DS_Store, .idea/, .vscode/, logs) in root .gitignore +# Common patterns +.env +.DS_Store + +#Cache +.cache/ # Compiled Files *.class @@ -28,13 +33,6 @@ buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar -# Gradle -.gradle/ -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - # IntelliJ IDEA *.iws *.iml diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index dcd237a..77ba7a6 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -22,6 +22,7 @@ 21 2.8.13 4.0.0 + 1.12.0 @@ -97,6 +98,26 @@ + + + net.revelc.code + impsort-maven-plugin + ${impsort.plugin.version} + + + ../.cache/impsort-maven-plugin-${impsort.plugin.version} + * + true + + + + sort-imports + + sort + + + + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java index b2e199a..d7457f7 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -1,12 +1,11 @@ package com.mongodb.samplemflix; +import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Map; - /** * Main Spring Boot application class for the MongoDB Sample MFlix API. * diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java index 178cdc3..bb75d25 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java @@ -1,5 +1,6 @@ package com.mongodb.samplemflix.config; +import java.util.Arrays; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -7,8 +8,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; -import java.util.Arrays; - /** * CORS (Cross-Origin Resource Sharing) configuration for the Sample MFlix API. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java index c11a4b6..40a7b3b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java @@ -5,13 +5,12 @@ import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; import com.mongodb.samplemflix.model.Movie; +import jakarta.annotation.PostConstruct; import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import jakarta.annotation.PostConstruct; - /** * Database verification component that runs on application startup. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index 765989c..a6a1d1f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -2,13 +2,12 @@ import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -import java.util.concurrent.TimeUnit; - /** * MongoDB configuration class for the Sample MFlix application using Spring Data MongoDB. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 4a139c0..5409494 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -10,15 +10,14 @@ import com.mongodb.samplemflix.model.response.SuccessResponse; import com.mongodb.samplemflix.service.MovieService; import jakarta.validation.Valid; +import java.time.Instant; +import java.util.List; +import java.util.Map; import org.bson.Document; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.time.Instant; -import java.util.List; -import java.util.Map; - /** * REST controller for movie-related endpoints. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java index c128981..ef5f11c 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import com.mongodb.MongoWriteException; import com.mongodb.samplemflix.model.response.ErrorResponse; +import java.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -10,8 +11,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; -import java.time.Instant; - /** * Global exception handler for the application. * diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java index 580921d..98fb66f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java @@ -1,5 +1,6 @@ package com.mongodb.samplemflix.model; +import java.util.Date; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -9,8 +10,6 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; -import java.util.Date; - /** * Domain model representing a comment document from the MongoDB comments collection. *

@@ -67,19 +66,4 @@ public class Comment { */ private Date date; - /** - * Field name constants for type-safe queries. - */ - public static class Fields { - public static final String ID = "_id"; - public static final String NAME = "name"; - public static final String EMAIL = "email"; - public static final String MOVIE_ID = "movie_id"; - public static final String TEXT = "text"; - public static final String DATE = "date"; - - private Fields() { - // Private constructor to prevent instantiation - } - } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 4efe341..672f131 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -1,14 +1,13 @@ package com.mongodb.samplemflix.model; +import java.util.Date; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; -import java.util.Date; -import java.util.List; - /** * Domain model representing a movie document from the MongoDB movies collection. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java index 2196533..94edcd0 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/BatchInsertResponse.java @@ -1,12 +1,11 @@ package com.mongodb.samplemflix.model.dto; +import java.util.Collection; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.bson.BsonValue; -import java.util.Collection; - /** * Response DTO for batch insert operations. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java index 485f65b..6f4fa61 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java @@ -1,13 +1,12 @@ package com.mongodb.samplemflix.model.dto; import jakarta.validation.constraints.NotBlank; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - /** * Data Transfer Object for creating a new movie. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java index 99d9ab3..192c0f9 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -1,12 +1,11 @@ package com.mongodb.samplemflix.model.dto; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - /** * Data Transfer Object for updating an existing movie. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java index 457dd52..9bba8cd 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java @@ -1,13 +1,12 @@ package com.mongodb.samplemflix.model.response; import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.Instant; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; - /** * Error response wrapper for API error responses. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java index 251c6f3..5bc305f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java @@ -1,13 +1,12 @@ package com.mongodb.samplemflix.model.response; import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.Instant; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; - /** * Success response wrapper for API responses. *

diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 7841e65..7327749 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -7,9 +7,8 @@ import com.mongodb.samplemflix.model.dto.DeleteResponse; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; -import org.bson.Document; - import java.util.List; +import org.bson.Document; /** * Service interface for movie business logic. diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 8cfea02..2c31c85 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -14,12 +14,15 @@ import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.bson.BsonValue; import org.bson.Document; import org.bson.types.ObjectId; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; @@ -27,13 +30,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - /** * Service layer for movie business logic using Spring Data MongoDB. *

@@ -64,7 +60,7 @@ public MovieServiceImpl(MovieRepository movieRepository, MongoTemplate mongoTemp public List getAllMovies(MovieSearchQuery query) { Query mongoQuery = buildQuery(query); - int limit = Math.min(Math.max(query.getLimit() != null ? query.getLimit() : 20, 1), 100); + int limit = Math.clamp(query.getLimit() != null ? query.getLimit() : 20, 1, 100); int skip = Math.max(query.getSkip() != null ? query.getSkip() : 0, 0); mongoQuery.skip(skip).limit(limit); diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index a180d51..b9b5a65 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -1,5 +1,12 @@ package com.mongodb.samplemflix.controller; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; @@ -10,6 +17,10 @@ import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.service.MovieService; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.bson.BsonObjectId; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; @@ -17,22 +28,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - /** * Unit tests for MovieControllerImpl. * @@ -49,7 +48,7 @@ class MovieControllerTest { @Autowired private ObjectMapper objectMapper; - @MockBean + @MockitoBean private MovieService movieService; private ObjectId testId; diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java index cefee5d..ed57f3b 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -1,9 +1,11 @@ package com.mongodb.samplemflix.service; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + import com.fasterxml.jackson.databind.ObjectMapper; -import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.samplemflix.exception.DatabaseOperationException; import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; @@ -13,8 +15,7 @@ import com.mongodb.samplemflix.model.dto.MovieSearchQuery; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; -import org.bson.BsonObjectId; -import org.bson.Document; +import java.util.*; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -26,12 +27,6 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - /** * Unit tests for MovieServiceImpl using Spring Data MongoDB. * From 0f3dc54fc7054a8207327991385e4e9461e4fe00 Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:19:43 -0400 Subject: [PATCH 026/110] Apply suggestion from @cbullinger --- server/java-spring/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index c874739..05be75b 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -16,7 +16,7 @@ This application provides a REST API for managing movie data from MongoDB's samp ## Prerequisites -- Java 17 or later +- Java 21 or later - Maven 3.6 or later - MongoDB Atlas account or local MongoDB instance with sample_mflix database From 92539734e32746430c06a34786bf06d673c67957 Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:20:53 -0400 Subject: [PATCH 027/110] Apply suggestion from @cbullinger --- server/express/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/server/express/.gitignore b/server/express/.gitignore index db4c02d..f9e1d4c 100644 --- a/server/express/.gitignore +++ b/server/express/.gitignore @@ -24,3 +24,4 @@ coverage/ # macOS .DS_Store + From cc13e06f978fe2681d1a4b55f720485871a66419 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 12:04:57 -0400 Subject: [PATCH 028/110] docs: update comments throughout for consistency --- server/java-spring/README.md | 7 +--- server/java-spring/pom.xml | 18 ++++++++- .../samplemflix/SampleMflixApplication.java | 2 +- .../samplemflix/config/CorsConfig.java | 8 ++-- .../config/DatabaseVerification.java | 32 ++++++++-------- .../samplemflix/config/MongoConfig.java | 32 +++++++++++++--- .../config/ObjectIdSerializer.java | 13 +++++-- .../config/ObjectMapperConfig.java | 10 ++++- .../controller/MovieControllerImpl.java | 38 ++++++++++--------- .../mongodb/samplemflix/model/Comment.java | 6 +-- .../com/mongodb/samplemflix/model/Movie.java | 16 ++++---- .../mongodb/samplemflix/model/Theater.java | 21 ++-------- .../model/dto/CreateMovieRequest.java | 4 +- .../model/dto/MovieSearchQuery.java | 4 +- .../model/dto/UpdateMovieRequest.java | 4 +- .../model/response/ApiResponse.java | 8 ++-- .../model/response/ErrorResponse.java | 10 ++--- .../model/response/SuccessResponse.java | 10 ++--- .../samplemflix/service/MovieServiceImpl.java | 9 +++-- .../samplemflix/util/ValidationUtils.java | 11 ++---- 20 files changed, 148 insertions(+), 115 deletions(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 05be75b..4fe120e 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -144,7 +144,7 @@ java -jar target/sample-mflix-spring-1.0.0.jar ## Implementation Status -### ✅ Completed Features +### Completed Features - **Movies CRUD API** - Full create, read, update, delete operations - **Spring Data MongoDB** - Repository pattern with MongoTemplate for complex queries @@ -161,11 +161,8 @@ java -jar target/sample-mflix-spring-1.0.0.jar See [TODO.md](TODO.md) for a comprehensive list of planned features and improvements, including: - Additional collections (Comments, Users, Sessions, Theaters) -- Authentication & Authorization (Spring Security + JWT) - Integration tests with Testcontainers - Advanced queries (aggregations, geospatial, vector search) -- Performance optimization and caching -- Monitoring and observability ## Technology Stack @@ -202,7 +199,7 @@ If you encounter connection issues: If Maven build fails: -1. Ensure you have Java 17 or later installed: `java -version` +1. Ensure you have Java 21 or later installed: `java -version` 2. Ensure Maven is installed: `mvn -version` 3. Clear Maven cache: `mvn clean` 4. Try rebuilding: `mvn clean install` diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index 77ba7a6..d6bc07b 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -22,7 +22,9 @@ 21 2.8.13 4.0.0 + 3.19.0 1.12.0 + 1.17.8 @@ -57,11 +59,12 @@ lombok true + org.apache.commons commons-lang3 - 3.19.0 + ${commons.lang3.version} @@ -86,6 +89,7 @@ + org.springframework.boot spring-boot-maven-plugin @@ -98,6 +102,18 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/${byte-buddy.version}/byte-buddy-agent-${byte-buddy.version}.jar + -Xshare:off + + + net.revelc.code diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java index d7457f7..b3aff5b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -9,7 +9,7 @@ /** * Main Spring Boot application class for the MongoDB Sample MFlix API. * - * This application demonstrates MongoDB CRUD operations using the MongoDB Java Driver + *

This application demonstrates MongoDB CRUD operations using the MongoDB Java Driver * in a Spring Boot environment. It provides a REST API for managing movie data from * the sample_mflix database. * diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java index bb75d25..8e2a31b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java @@ -10,11 +10,11 @@ /** * CORS (Cross-Origin Resource Sharing) configuration for the Sample MFlix API. - *

- * This configuration allows the frontend application (typically running on a different port + * + *

This configuration allows the frontend application (typically running on a different port * during development) to make requests to this backend API. - *

- * The allowed origins are configured via the CORS_ORIGIN environment variable. + * + *

The allowed origins are configured via the CORS_ORIGIN environment variable. */ @Configuration public class CorsConfig { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java index 40a7b3b..fef480f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/DatabaseVerification.java @@ -13,11 +13,11 @@ /** * Database verification component that runs on application startup. - *

- * This component performs pre-flight checks to ensure the MongoDB database + * + *

This component performs pre-flight checks to ensure the MongoDB database * is properly configured and contains the expected data and indexes. - *

- * Verification steps: + * + *

Verification steps: * 1. Check if the movies collection exists * 2. Verify the collection contains documents * 3. Check for text search indexes on plot, title, and fullplot fields @@ -43,11 +43,11 @@ public DatabaseVerification(MongoDatabase database) { /** * Runs database verification checks after the bean is constructed. - *

- * This method is called automatically by Spring after dependency injection + * + *

This method is called automatically by Spring after dependency injection * is complete. It performs all verification steps and logs the results. - *

- * The method catches all exceptions to prevent application startup failure, + * + *

The method catches all exceptions to prevent application startup failure, * but logs errors to help developers identify issues. */ @PostConstruct @@ -70,12 +70,13 @@ public void verifyDatabase() { /** * Verifies the movies collection exists, contains data, and has required indexes. * - * This method: + *

This method: + *

      * 1. Checks if the movies collection exists (implicitly by accessing it)
      * 2. Counts documents to verify sample data is loaded
      * 3. Creates a text search index on plot, title, and fullplot fields
-     *
-     * The text search index enables full-text search functionality across movie
+     *
+ *

The text search index enables full-text search functionality across movie * descriptions and titles, which is used by the search endpoint. */ private void verifyMoviesCollection() { @@ -101,15 +102,16 @@ private void verifyMoviesCollection() { /** * Creates a text search index on the movies collection if it doesn't already exist. * - * The index is created on three fields: + *

The index is created on three fields: + *

      * - plot: Short movie description
      * - title: Movie title
      * - fullplot: Full movie description
-     *
-     * This enables the $text search operator to perform full-text search across
+     * 
+ *

This enables the $text search operator to perform full-text search across * these fields, which is used by the search endpoint in the API. * - * The index is created in the background to avoid blocking other operations. + *

The index is created in the background to avoid blocking other operations. * If the index already exists, MongoDB will ignore the duplicate creation request. * * @param moviesCollection the movies collection to create the index on diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index a6a1d1f..b0fcfce 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -2,30 +2,36 @@ import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoDatabase; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; /** * MongoDB configuration class for the Sample MFlix application using Spring Data MongoDB. - *

- * This class extends AbstractMongoClientConfiguration to customize MongoDB client settings + * + *

This class extends AbstractMongoClientConfiguration to customize MongoDB client settings * while leveraging Spring Data MongoDB's auto-configuration for repositories and templates. - *

- * Key features: + * + *

Key features: + *

  * - Connection pooling with configurable settings (max 100 connections, min 10)
  * - Connection timeout configuration (10 seconds for connect and read)
  * - Automatic POJO mapping (no manual codec configuration needed)
  * - Repository scanning and auto-configuration
  * - MongoTemplate bean creation for complex queries
- * 

- * Spring Data MongoDB automatically: + *

+ *

Spring Data MongoDB automatically: + *

  * - Creates MongoClient and MongoTemplate beans
  * - Handles POJO to BSON conversion
  * - Manages connection lifecycle
  * - Provides repository implementations
+ * 
*/ @Configuration @EnableMongoRepositories(basePackages = "com.mongodb.samplemflix.repository") @@ -73,4 +79,18 @@ protected void configureClientSettings(MongoClientSettings.Builder builder) { clusterBuilder.serverSelectionTimeout(10000, TimeUnit.MILLISECONDS) // 10s to select server ); } + + /** + * Provides a MongoDatabase bean for direct MongoDB driver access. + * + *

This bean is needed for components that require direct access to the MongoDB + * driver API (like DatabaseVerification), while still using Spring Data MongoDB + * for repository operations. + * + * @return the configured MongoDatabase instance + */ + @Bean + public MongoDatabase mongoDatabase() { + return mongoClient().getDatabase(databaseName); + } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java index 56c2649..bd50571 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectIdSerializer.java @@ -6,9 +6,15 @@ import java.io.IOException; import org.bson.types.ObjectId; -// ObjectId is a MognoDB GUID Class, an efficient 12 byte UUID starting with a -// timestamp for efficnent indexing - we need to teach Jackson how to convert it -// to JSON nicely +/** + * Custom serializer for MongoDB's ObjectId to convert it to a string representation. + * + *

MongoDB's ObjectId is a 12-byte unique identifier. By default, Jackson will serialize it + * as a base64 string, but we want to use the more human-readable hex string representation. + * + *

This custom serializer teaches Jackson to convert ObjectId to a hex string when + * writing JSON. + */ public class ObjectIdSerializer extends StdSerializer { @@ -22,4 +28,3 @@ public void serialize(ObjectId value, JsonGenerator gen, SerializerProvider prov gen.writeString(value.toHexString()); } } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java index ba55e71..6be474d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/ObjectMapperConfig.java @@ -11,6 +11,15 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +/** + * Configuration for customizing the ObjectMapper used for JSON serialization and deserialization. + * + *

This configuration disables the default timestamp serialization for dates and registers a + * custom serializer for MongoDB's ObjectId to convert it to a string representation. + * + *

It also registers a JavaTimeModule to handle Java 8 date and time types. + */ + @Configuration public class ObjectMapperConfig { @@ -36,4 +45,3 @@ public DataBufferFactory dataBufferFactory() { return new DefaultDataBufferFactory(); } } - diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 5409494..0d243c3 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -22,6 +22,7 @@ * REST controller for movie-related endpoints. *

* This controller handles all HTTP requests for movie operations including: + *

  * - GET /api/movies - Get all movies with filtering, sorting, and pagination
  * - GET /api/movies/{id} - Get a single movie by ID
  * - POST /api/movies - Create a new movie
@@ -31,6 +32,7 @@
  * - DELETE /api/movies/{id} - Delete a movie
  * - DELETE /api/movies - Delete multiple movies
  * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie
+ * 
*/ @RestController @RequestMapping("/api/movies") @@ -44,8 +46,8 @@ public MovieControllerImpl(MovieService movieService) { /** * GET /api/movies - *

- * Retrieves multiple movies with optional filtering, sorting, and pagination. + * + *

Retrieves multiple movies with optional filtering, sorting, and pagination. */ @GetMapping public ResponseEntity>> getAllMovies( @@ -85,8 +87,8 @@ public ResponseEntity>> getAllMovies( /** * GET /api/movies/{id} - *

- * Retrieves a single movie by its ObjectId. + * + *

Retrieves a single movie by its ObjectId. */ @GetMapping("/{id}") public ResponseEntity> getMovieById(@PathVariable String id) { @@ -104,8 +106,8 @@ public ResponseEntity> getMovieById(@PathVariable String /** * POST /api/movies - *

- * Creates a single new movie document. + * + *

Creates a single new movie document. */ @PostMapping public ResponseEntity> createMovie(@Valid @RequestBody CreateMovieRequest request) { @@ -123,8 +125,8 @@ public ResponseEntity> createMovie(@Valid @RequestBody Cr /** * POST /api/movies/batch - *

- * Creates multiple movie documents in a single operation. + * + *

Creates multiple movie documents in a single operation. */ @PostMapping("/batch") public ResponseEntity> createMoviesBatch( @@ -143,8 +145,8 @@ public ResponseEntity> createMoviesBatch( /** * PUT /api/movies/{id} - *

- * Updates a single movie document. + * + *

Updates a single movie document. */ @PutMapping("/{id}") public ResponseEntity> updateMovie( @@ -164,8 +166,8 @@ public ResponseEntity> updateMovie( /** * PATCH /api/movies - *

- * Updates multiple movies based on a filter. + * + *

Updates multiple movies based on a filter. */ @SuppressWarnings("unchecked") @PatchMapping @@ -189,8 +191,8 @@ public ResponseEntity> updateMoviesBatch( /** * DELETE /api/movies/{id}/find-and-delete - *

- * Finds and deletes a movie in a single atomic operation. + * + *

Finds and deletes a movie in a single atomic operation. */ @DeleteMapping("/{id}/find-and-delete") public ResponseEntity> findAndDeleteMovie(@PathVariable String id) { @@ -208,8 +210,8 @@ public ResponseEntity> findAndDeleteMovie(@PathVariable S /** * DELETE /api/movies/{id} - *

- * Deletes a single movie document. + * + *

Deletes a single movie document. */ @DeleteMapping("/{id}") public ResponseEntity> deleteMovie(@PathVariable String id) { @@ -227,8 +229,8 @@ public ResponseEntity> deleteMovie(@PathVariable /** * DELETE /api/movies - *

- * Deletes multiple movies based on a filter. + * + *

Deletes multiple movies based on a filter. */ @SuppressWarnings("unchecked") @DeleteMapping diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java index 98fb66f..15d9c94 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Comment.java @@ -12,11 +12,11 @@ /** * Domain model representing a comment document from the MongoDB comments collection. - *

- * This class maps to the comments collection in the sample_mflix database. + * + *

This class maps to the comments collection in the sample_mflix database. * Comments are user reviews/comments associated with movies. * - * TODO: Implement Comment functionality: + *

TODO: Implement Comment functionality: * - Create CommentRepository extending MongoRepository * - Create CommentService and CommentServiceImpl * - Create CommentController with REST endpoints diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 672f131..4345b0e 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -10,12 +10,12 @@ /** * Domain model representing a movie document from the MongoDB movies collection. - *

- * This class maps to the movies collection in the sample_mflix database. + * + *

This class maps to the movies collection in the sample_mflix database. * It includes all fields from the movie documents including nested objects * for awards, IMDB ratings, and Tomatoes ratings. - *

- * Note: We use Lombok annotations to reduce boilerplate code: + * + *

Note: We use Lombok annotations to reduce boilerplate code: * - @Data: Generates getters, setters, toString, equals, and hashCode * - @Builder: Provides a fluent builder pattern for object construction * - @NoArgsConstructor: Generates a no-argument constructor (required by MongoDB driver) @@ -29,12 +29,12 @@ public class Movie { /** * Field name constants for MongoDB operations. - *

- * These constants should be used when referencing field names in queries, filters, + * + *

These constants should be used when referencing field names in queries, filters, * indexes, and other MongoDB operations to ensure type safety and enable IDE * "Find Usages" functionality. - *

- * Example usage: + * + *

Example usage: *

      * filter.append(Movie.Fields.TITLE, "The Matrix");
      * Indexes.text(Movie.Fields.PLOT);
diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java
index c76511b..c0d2045 100644
--- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java
+++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Theater.java
@@ -10,11 +10,11 @@
 
 /**
  * Domain model representing a theater document from the MongoDB theaters collection.
- * 

- * This class maps to the theaters collection in the sample_mflix database. + * + *

This class maps to the theaters collection in the sample_mflix database. * It includes location information with address and geospatial coordinates. * - * TODO: Implement Theater functionality: + *

TODO: Implement Theater functionality: * - Create TheaterRepository extending MongoRepository * - Create TheaterService and TheaterServiceImpl * - Create TheaterController with REST endpoints @@ -116,19 +116,4 @@ public static class Geo { private double[] coordinates; } } - - /** - * Field name constants for type-safe queries. - */ - public static class Fields { - public static final String ID = "_id"; - public static final String THEATER_ID = "theaterId"; - public static final String LOCATION = "location"; - public static final String LOCATION_GEO = "location.geo"; - public static final String LOCATION_ADDRESS = "location.address"; - - private Fields() { - // Private constructor to prevent instantiation - } - } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java index 6f4fa61..c333064 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/CreateMovieRequest.java @@ -9,8 +9,8 @@ /** * Data Transfer Object for creating a new movie. - *

- * This DTO is used for POST /api/movies requests. + * + *

This DTO is used for POST /api/movies requests. * It includes validation annotations to ensure required fields are present. * Only the title field is required; all other fields are optional. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java index 20d6833..dc0ea46 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchQuery.java @@ -7,8 +7,8 @@ /** * Data Transfer Object for movie search query parameters. - *

- * This DTO is used to parse and validate query parameters for GET /api/movies requests. + * + *

This DTO is used to parse and validate query parameters for GET /api/movies requests. * It supports full-text search, filtering by genre/year/rating, sorting, and pagination. */ @Data diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java index 192c0f9..f855276 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -8,8 +8,8 @@ /** * Data Transfer Object for updating an existing movie. - *

- * This DTO is used for PUT /api/movies/{id} requests. + * + *

This DTO is used for PUT /api/movies/{id} requests. * All fields are optional since partial updates are allowed. * Any field that is null will not be updated in the database. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java index b76fa3d..1374efb 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ApiResponse.java @@ -2,11 +2,11 @@ /** * Generic API response interface. - *

- * This interface is implemented by both SuccessResponse and ErrorResponse + * + *

This interface is implemented by both SuccessResponse and ErrorResponse * to provide a consistent response structure across all API endpoints. - *

- * All API responses include: + * + *

All API responses include: * - success: boolean indicating if the request was successful * - timestamp: ISO 8601 timestamp of when the response was generated */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java index 9bba8cd..6fab419 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/ErrorResponse.java @@ -9,10 +9,10 @@ /** * Error response wrapper for API error responses. - *

- * This class wraps error responses with error codes, messages, and metadata. - *

- * { + * + *

This class wraps error responses with error codes, messages, and metadata. + * + *

 {
  *   success: false,
  *   message: string,
  *   error: {
@@ -21,7 +21,7 @@
  *     details?: any
  *   },
  *   timestamp: string
- * }
+ * }
*/ @Data @Builder diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java index 5bc305f..69550ee 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/response/SuccessResponse.java @@ -9,17 +9,17 @@ /** * Success response wrapper for API responses. - *

- * This class wraps successful API responses with metadata like timestamp and pagination. + * + *

This class wraps successful API responses with metadata like timestamp and pagination. * It uses a generic type parameter T to hold the response data. - *

- * { + * + *

  {
  *   success: true,
  *   message?: string,
  *   data: T,
  *   timestamp: string,
  *   pagination?: { page, limit, total, pages }
- * }
+ * }
*/ @Data @Builder diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 2c31c85..571cd07 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -32,16 +32,19 @@ /** * Service layer for movie business logic using Spring Data MongoDB. - *

- * This service handles: + * + *

This service handles: + *

  * - Business logic and validation
  * - Query construction using Spring Data MongoDB Query API
  * - Data transformation between DTOs and entities
  * - Error handling and exception throwing
- * 

+ *

* Uses both: + *
  * - MovieRepository (Spring Data) for simple CRUD operations
  * - MongoTemplate for complex queries and batch operations
+ * 
*/ @Service public class MovieServiceImpl implements MovieService { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java index ab46c8d..a496b14 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/util/ValidationUtils.java @@ -6,8 +6,6 @@ * This class provides helper methods for validating request data. * * TODO: Implement validation utility methods: - * - Email validation - * - Password strength validation * - ObjectId format validation * - Date range validation * - String sanitization @@ -19,15 +17,12 @@ private ValidationUtils() { // Private constructor to prevent instantiation } - // TODO: Add email validation method - // public static boolean isValidEmail(String email) - - // TODO: Add password strength validation - // public static boolean isStrongPassword(String password) - // TODO: Add ObjectId validation (currently duplicated in service layer) // public static boolean isValidObjectId(String id) // TODO: Add string sanitization for XSS prevention // public static String sanitize(String input) + + // TODO: Add URL validation for poster and trailer fields + // public static boolean isValidUrl(String url) } From 61559063426910e190c7b56686b20d7d99b8297a Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 30 Oct 2025 07:59:49 -0400 Subject: [PATCH 029/110] docs: update readme and remove internal todo doc --- server/java-spring/README.md | 16 -- server/java-spring/TODO.md | 372 ----------------------------------- 2 files changed, 388 deletions(-) delete mode 100644 server/java-spring/TODO.md diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 4fe120e..90b020f 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -114,15 +114,6 @@ Once the application is running, you can access: - `DELETE /api/movies` - Delete multiple movies - `DELETE /api/movies/{id}/find-and-delete` - Find and delete a movie -### Planned Endpoints - -See [TODO.md](TODO.md) for a complete list of planned features including: -- Comments API -- Users API -- Sessions API (Authentication) -- Theaters API (with geospatial queries) -- Advanced movie features (aggregations, recommendations) - ## Development ### Running Tests @@ -156,13 +147,6 @@ java -jar target/sample-mflix-spring-1.0.0.jar - **OpenAPI Documentation** - Swagger UI available at `/swagger-ui.html` - **Database Verification** - Startup checks for database connectivity and indexes -### 🚧 In Progress / Planned - -See [TODO.md](TODO.md) for a comprehensive list of planned features and improvements, including: - -- Additional collections (Comments, Users, Sessions, Theaters) -- Integration tests with Testcontainers -- Advanced queries (aggregations, geospatial, vector search) ## Technology Stack diff --git a/server/java-spring/TODO.md b/server/java-spring/TODO.md deleted file mode 100644 index 8be6099..0000000 --- a/server/java-spring/TODO.md +++ /dev/null @@ -1,372 +0,0 @@ -# TODO List - Java Spring Boot MongoDB Sample Application - -This document tracks remaining functionality and improvements for the Java Spring Boot backend based on the **Requirements for Sample Repositories for MongoDB Atlas** and **Sample Application Scoping** documents. - -## 📋 Scope Alignment - -This sample application must demonstrate: -- ✅ **Basic CRUD** - insertOne(), insertMany(), findOne(), find(), updateOne(), updateMany(), deleteOne(), deleteMany(), findOneAndDelete() -- 🚧 **Aggregations** - Reporting by comments, year, and director -- 🚧 **Search** - Full-text search using Search Index on plot -- 🚧 **Vector Search** - Find similar movies based on plot embeddings -- 🚧 **Geospatial** - Find theaters near coordinates -- 🚧 **Recommendations** - Potentially using Vector Search - -## ✅ Completed Features (In Scope) - -- [x] **Basic CRUD operations for Movies collection** - - [x] insertOne() - `POST /api/movies` - - [x] insertMany() - `POST /api/movies/batch` - - [x] findOne() - `GET /api/movies/{id}` - - [x] find() - `GET /api/movies` - - [x] updateOne() - `PUT /api/movies/{id}` - - [x] updateMany() - `PATCH /api/movies` - - [x] deleteOne() - `DELETE /api/movies/{id}` - - [x] deleteMany() - `DELETE /api/movies` - - [x] findOneAndDelete() - `DELETE /api/movies/{id}/find-and-delete` -- [x] Spring Data MongoDB migration -- [x] Filtering, sorting, and pagination -- [x] Custom exception handling -- [x] Type-safe response DTOs -- [x] Unit tests for service and controller layers (35 tests) -- [x] OpenAPI/Swagger documentation -- [x] Database verification on startup (checks indexes and data) -- [x] CORS configuration -- [x] Field name constants for type safety -- [x] Environment variable configuration (.env support) - -## 🚧 High Priority TODOs (Required by Scope) - -### 1. Aggregations (REQUIRED) - -**Scope Requirement**: Implement aggregation pipelines for reporting - -- [ ] **GET /api/movies/reportingByComments** - Aggregate movies with most comments - - [ ] Create aggregation pipeline using `$lookup` to join with comments collection - - [ ] Sort by comment count descending - - [ ] Add pagination support - - [ ] Add unit tests - -- [ ] **GET /api/movies/reportingByYear** - Aggregate movies by year with average rating - - [ ] Create aggregation pipeline grouping by year - - [ ] Calculate average IMDB rating per year - - [ ] Sort by year - - [ ] Add unit tests - -- [ ] **GET /api/movies/reportingByDirector** - Aggregate directors with most movies - - [ ] Create aggregation pipeline using `$unwind` on directors array - - [ ] Group by director and count movies - - [ ] Sort by movie count descending - - [ ] Add pagination support - - [ ] Add unit tests - -### 2. Search (REQUIRED) - -**Scope Requirement**: Implement Full-Text Search and Vector Search - -- [ ] **GET /api/movies/searchByPlot** - Text Search using Search Index - - [ ] Verify text index exists on plot, fullplot, genres, title fields - - [ ] Implement text search using Spring Data MongoDB TextCriteria - - [ ] Add relevance scoring - - [ ] Add pagination support - - [ ] Add unit tests - - [ ] Document index requirements in README - -- [ ] **GET /api/movies/findSimilarMovies** - Vector Search for similar movies - - [ ] Implement vector search on `plot_embedding` field - - [ ] Use MongoDB Atlas Vector Search - - [ ] Add similarity threshold parameter - - [ ] Add limit parameter - - [ ] Add unit tests - - [ ] Document vector search index requirements - -### 3. Geospatial (REQUIRED) - -**Scope Requirement**: Find theaters near coordinates - -- [ ] **GET /api/theaters/findTheatersNearCoordinates** - Geospatial query - - [ ] Create `Theater` entity (already exists, needs repository) - - [ ] Create `TheaterRepository` with geospatial query methods - - [ ] Create `TheaterService` and `TheaterServiceImpl` - - [ ] Create `TheaterController` with endpoint - - [ ] Implement `$near` or `$geoNear` query - - [ ] Add distance parameter (in meters/miles) - - [ ] Add limit parameter - - [ ] Verify geospatial index exists on `location.geo` - - [ ] Add unit tests - - [ ] Add integration tests - -### 4. Recommendations (OPTIONAL - FUTURE) - -**Scope Requirement**: Potentially use Vector Search to power recommendations - -- [ ] Design recommendation algorithm -- [ ] Implement using Vector Search on plot embeddings -- [ ] Consider user preferences/history -- [ ] Add endpoint `GET /api/movies/recommendations` -- [ ] Add tests - -### 5. Comments Collection Support (For Aggregations) - -**Note**: Required to support "reportingByComments" aggregation - -- [ ] Verify `Comment` entity model (already exists) -- [ ] Create `CommentRepository` interface -- [ ] Create `CommentService` and `CommentServiceImpl` -- [ ] Create `CommentController` with basic endpoints: - - `GET /api/comments/movie/{movieId}` - Get comments for a specific movie - - `POST /api/comments` - Create a new comment -- [ ] Add tests for Comment functionality - -### 6. Testing (REQUIRED) - -**Scope Requirement**: Unit tests and E2E tests - -- [ ] **Unit Tests** - Test individual features and driver interactions - - [x] Service layer unit tests (21 tests completed) - - [x] Controller layer unit tests (14 tests completed) - - [ ] Add tests for aggregation methods - - [ ] Add tests for search methods - - [ ] Add tests for geospatial queries - - [ ] Add tests for vector search - -- [ ] **Integration Tests** - Test complete flows - - [ ] Set up Testcontainers for MongoDB - - [ ] Create integration tests for all CRUD endpoints - - [ ] Create integration tests for aggregations - - [ ] Create integration tests for search - - [ ] Create integration tests for geospatial queries - - [ ] Test error handling and edge cases - -### 7. Error Handling & Verification (REQUIRED) - -**Scope Requirement**: Pre-flight checks and error handling - -- [x] Verify requirements function on app start (DatabaseVerification.java) - - [x] Check for indexes - - [x] Check for sample data - - [x] Create indexes if missing -- [x] Basic error handling implemented - - [x] Connection errors - - [x] Invalid input validation - - [x] Custom exceptions (ValidationException, ResourceNotFoundException, DatabaseOperationException) - - [x] Global exception handler -- [ ] Enhance error handling for new features: - - [ ] Vector search errors - - [ ] Geospatial query errors - - [ ] Aggregation pipeline errors - -### 8. Documentation (REQUIRED) - -**Scope Requirement**: Clear README and documentation - -- [x] README.md with prerequisites and setup -- [x] Environment variable configuration (.env.example) -- [x] OpenAPI/Swagger documentation -- [ ] Update README with: - - [ ] Aggregation endpoint documentation - - [ ] Search endpoint documentation - - [ ] Vector search setup instructions - - [ ] Geospatial query examples - - [ ] Index requirements -- [ ] Add code comments for MongoDB features - - [ ] Comment aggregation pipelines - - [ ] Comment vector search implementation - - [ ] Comment geospatial queries - - [ ] Note where readability was prioritized over performance - -## 📝 Medium Priority TODOs (Out of Scope - Future Enhancements) - -### 9. Additional Collections (Optional) - -**Note**: These collections are not required by the current scope but may be useful for future enhancements. - -#### Users Collection (`sample_mflix.users`) -- [ ] Create `UserRepository` interface (model already exists) -- [ ] Create `UserService` and `UserServiceImpl` -- [ ] Implement password hashing (BCrypt) -- [ ] Add email validation -- [ ] Add tests for User functionality - -#### Sessions Collection (`sample_mflix.sessions`) -- [ ] Create `SessionRepository` interface (model already exists) -- [ ] Create `SessionService` and `SessionServiceImpl` -- [ ] Implement JWT token generation and validation -- [ ] Add tests for Session functionality - -#### Embedded Movies Collection (`sample_mflix.embedded_movies`) -- [ ] Investigate purpose and schema of this collection -- [ ] Determine if separate endpoints are needed -- [ ] Document relationship to main `movies` collection - -### 10. Authentication & Authorization (Out of Scope) - -**Note**: Not required by current scope, but useful for production applications - -- [ ] Implement Spring Security -- [ ] Add JWT authentication filter -- [ ] Create `@PreAuthorize` annotations for role-based access -- [ ] Implement user roles (USER, ADMIN) -- [ ] Add authentication to existing endpoints -- [ ] Create authentication integration tests - -### 11. API Improvements (Out of Scope) - -- [ ] Add HATEOAS links to responses -- [ ] Implement API versioning (e.g., `/api/v1/movies`) -- [ ] Add request/response compression -- [ ] Implement rate limiting -- [ ] Add API key authentication option -- [ ] Create GraphQL endpoint as alternative to REST - -### 12. Data Validation & Quality (Partially Complete) - -- [x] Basic Jakarta Validation annotations -- [ ] Create custom validators for complex business rules -- [ ] Implement data sanitization for user inputs -- [ ] Add validation for embedded documents -- [ ] Create validation tests - -### 13. Performance Optimization (Out of Scope) - -**Note**: Scope prioritizes readability over performance - -- [ ] Implement caching with Spring Cache (Redis/Caffeine) -- [ ] Add database query optimization -- [ ] Implement connection pooling tuning -- [ ] Create performance benchmarks -- [ ] Implement lazy loading for large collections -- [ ] Document performance vs readability tradeoffs - -### 14. Monitoring & Observability (Out of Scope) - -- [ ] Add Spring Boot Actuator endpoints -- [ ] Implement custom health checks -- [ ] Add metrics collection (Micrometer) -- [ ] Integrate with Prometheus/Grafana -- [ ] Add distributed tracing (Sleuth/Zipkin) -- [ ] Implement structured logging (JSON format) - -### 15. Code Quality (Partially Complete) - -- [x] Unit tests for core functionality (35 tests) -- [ ] Increase test coverage to >80% -- [ ] Add code coverage reporting (JaCoCo) -- [ ] Add mutation testing (PIT) -- [ ] Integrate SonarQube for code quality analysis -- [ ] Add Checkstyle/PMD for code style enforcement - -### 16. Build & Deployment (Out of Scope) - -**Note**: Deployment is handled by Growth team per scope document - -- [ ] Create Dockerfile for containerization -- [ ] Add Docker Compose for local development -- [ ] Create Kubernetes deployment manifests -- [ ] Set up CI/CD pipeline (GitHub Actions) -- [x] Automated dependency updates (Dependabot) - handled by DevDocs team - -### 17. Security Enhancements (Out of Scope) - -**Note**: Basic security implemented, advanced features out of scope - -- [x] CORS configuration -- [x] Input validation -- [ ] Implement HTTPS/TLS -- [ ] Add CSRF protection -- [ ] Implement security headers (HSTS, CSP, etc.) -- [ ] Add security scanning (OWASP Dependency Check) - -### 18. Developer Experience (Partially Complete) - -- [x] Environment variable configuration -- [x] Database verification on startup -- [x] OpenAPI/Swagger documentation -- [ ] Add Spring Boot DevTools for hot reload -- [ ] Create seed data script for development -- [ ] Add API client examples (Postman collection) - -## 🐛 Known Issues - -- [ ] Update deprecated `@MockBean` to new Spring Boot 3.5+ alternative (warning in tests) - -## 📚 Technical Debt - -- None currently - code follows best practices and scope requirements - -## 🎯 Next Immediate Steps (Priority Order) - -Based on the **Sample Application Scoping** document, the next steps are: - -### Phase 1: Complete Required Features (In Scope) -1. **Aggregations** (REQUIRED) - - Implement `GET /api/movies/reportingByComments` - - Implement `GET /api/movies/reportingByYear` - - Implement `GET /api/movies/reportingByDirector` - -2. **Search** (REQUIRED) - - Implement `GET /api/movies/searchByPlot` (Full-Text Search) - - Implement `GET /api/movies/findSimilarMovies` (Vector Search) - -3. **Geospatial** (REQUIRED) - - Implement `GET /api/theaters/findTheatersNearCoordinates` - - Create Theater repository and service - -4. **Comments Collection** (REQUIRED for aggregations) - - Create CommentRepository and CommentService - - Implement basic endpoints to support aggregations - -5. **Testing** (REQUIRED) - - Add unit tests for all new features - - Create integration tests with Testcontainers - -6. **Documentation** (REQUIRED) - - Update README with new endpoints - - Document index requirements - - Add code comments explaining MongoDB features - - Note readability vs performance tradeoffs - -### Phase 2: Quality Assurance (In Scope) -7. **Error Handling** - Enhance for new features -8. **Code Comments** - Add more comments than typical production app -9. **Readability Review** - Ensure code prioritizes clarity over performance - -### Phase 3: Future Enhancements (Out of Scope) -10. Recommendations using Vector Search -11. Authentication & Authorization -12. Additional collections (Users, Sessions) -13. Performance optimizations -14. Advanced features - -## 📋 Scope Compliance Checklist - -### Required Features -- [x] **Basic CRUD** - All methods implemented ✅ - - [x] insertOne(), insertMany() - - [x] findOne(), find() - - [x] updateOne(), updateMany() - - [x] deleteOne(), deleteMany(), findOneAndDelete() -- [ ] **Aggregations** - Not yet implemented ⏳ - - [ ] Reporting by comments - - [ ] Reporting by year - - [ ] Reporting by director -- [ ] **Search** - Not yet implemented ⏳ - - [ ] Full-text search on plot -- [ ] **Vector Search** - Not yet implemented ⏳ - - [ ] Find similar movies -- [ ] **Geospatial** - Not yet implemented ⏳ - - [ ] Find theaters near coordinates - -### Quality Standards -- [x] **Error Handling** - Basic implementation complete ✅ -- [x] **Testing** - Unit tests complete (35 tests) ✅ -- [ ] **Integration Tests** - Pending ⏳ -- [x] **Documentation** - README complete ✅ -- [ ] **Documentation Updates** - Needs updates for new features ⏳ -- [x] **Readability** - Code follows best practices ✅ -- [x] **Code Comments** - Good coverage, needs more for complex features ⏳ -- [x] **Minimal Dependencies** - Using only Spring Boot and MongoDB ✅ -- [x] **Environment Variables** - Implemented with .env.example ✅ -- [x] **Apache License** - Applied ✅ -- [x] **Conventional Commits** - Following standard ✅ -- [x] **Clean Commit History** - Maintained ✅ From b10f743568bcd8eec7205398a049b95fee636656 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 30 Oct 2025 13:38:06 -0400 Subject: [PATCH 030/110] fix(movies): add back missing @Document tag --- .../src/main/java/com/mongodb/samplemflix/model/Movie.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 4345b0e..eb2fb83 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -7,6 +7,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.bson.types.ObjectId; +import org.springframework.data.mongodb.core.mapping.Document; /** * Domain model representing a movie document from the MongoDB movies collection. @@ -25,6 +26,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@Document(collection = "movies") public class Movie { /** From 5d1b7d90bc30b3743ed94d12b8ed56fd6886c2ad Mon Sep 17 00:00:00 2001 From: tmcneil-mdb Date: Thu, 30 Oct 2025 15:49:03 -0400 Subject: [PATCH 031/110] Feat finishing python crud - merging python backends together (#10) * feat(api): implement batch create, update, and delete endpoints for movies * feat(api): implement /search/atlas endpoint using MongoDB Atlas Search * feat(api): implement batch create, update, and delete endpoints for movies * feat(api): implement /search/atlas endpoint using MongoDB Atlas Search * merge changes * chore - removing gremlins, adding documentation. * chore- address feedback, fixing dependency bug * chore - cleaning up api endpoints --- server/python/main.py | 58 +- server/python/requirements.in | 4 +- server/python/requirements.txt | 4 +- server/python/src/models/models.py | 54 +- server/python/src/routers/movies.py | 1161 ++++++++++++++++++--------- 5 files changed, 867 insertions(+), 414 deletions(-) diff --git a/server/python/main.py b/server/python/main.py index a1a67e9..a782508 100644 --- a/server/python/main.py +++ b/server/python/main.py @@ -1,34 +1,50 @@ from fastapi import FastAPI from src.routers import movies from src.utils.errorHandler import register_error_handlers +from src.database.mongo_client import db +import traceback app = FastAPI() register_error_handlers(app) app.include_router(movies.router, prefix="/api/movies", tags=["movies"]) +@app.on_event("startup") +async def ensure_search_index(): + try: + movies_collection = db.get_collection("movies") + result = await movies_collection.list_search_indexes() + indexes = [idx async for idx in result] + index_names = [index["name"] for index in indexes] + if "movieSearchIndex" in index_names: + print("MongoDB Search index already exists.") + return + + # Create a mapping if the movieSearchIndex does not exist + index_definition = { + "mappings": { + "dynamic": False, + "fields": { + "plot": {"type": "string", "analyzer": "lucene.standard"}, + "fullplot": {"type": "string", "analyzer": "lucene.standard"}, + "directors": {"type": "string", "analyzer": "lucene.standard"}, + "writers": {"type": "string", "analyzer": "lucene.standard"}, + "cast": {"type": "string", "analyzer": "lucene.standard"} + } + } + } + # Creates movieSearchIndex on the movies collection + await db.command({ + "createSearchIndexes": "movies", + "indexes": [{ + "name": "movieSearchIndex", + "definition": index_definition + }] + }) + print("MongoDB Search index created.") + except Exception as e: + print(f"Error creating the search index: {e}") - - -#------------------------------------ -# Testing error endpoints. Will be removed later -#------------------------------------ - -''' -@app.get("/") -async def root(): - return {"message": "Backend is running!"} - -@app.get("/test-duplicate") -async def test_duplicate(): - from pymongo.errors import DuplicateKeyError - raise DuplicateKeyError("This is a test duplicate key error.") - -@app.get("/test-generic") -async def test_generic(): - from pymongo.errors import PyMongoError - raise PyMongoError("This is a test generic pymongo error.") -''' \ No newline at end of file diff --git a/server/python/requirements.in b/server/python/requirements.in index e8ee7b7..835786d 100644 --- a/server/python/requirements.in +++ b/server/python/requirements.in @@ -2,8 +2,8 @@ # 1. CORE WEB FRAMEWORK & ASGI SERVER # FastAPI and its main components. # ------------------------------------------------------------------------------ -fastapi~=0.120.0 # The main web framework -starlette~=0.48.0 # FastAPI's underlying ASGI toolkit +fastapi~=0.120.1 # The main web framework +starlette~=0.49.1 # FastAPI's underlying ASGI toolkit uvicorn~=0.38.0 # Production-ready ASGI server uvloop~=0.22.0 # Optional: High-performance event loop for uvicorn websockets~=15.0.0 # For WebSocket support diff --git a/server/python/requirements.txt b/server/python/requirements.txt index 3beb0d8..504c289 100644 --- a/server/python/requirements.txt +++ b/server/python/requirements.txt @@ -32,7 +32,7 @@ email-validator==2.3.0 # via # -r requirements.in # pydantic -fastapi==0.120.0 +fastapi==0.120.2 # via -r requirements.in fastapi-cli==0.0.14 # via -r requirements.in @@ -114,7 +114,7 @@ shellingham==1.5.4 # via typer sniffio==1.3.1 # via anyio -starlette==0.48.0 +starlette==0.49.1 # via # -r requirements.in # fastapi diff --git a/server/python/src/models/models.py b/server/python/src/models/models.py index e027116..c044ce9 100644 --- a/server/python/src/models/models.py +++ b/server/python/src/models/models.py @@ -60,24 +60,12 @@ class RatingFilter(BaseModel): gte: Optional[float] = Field(None, alias="$gte") lte: Optional[float] = Field(None, alias="$lte") -class MovieFilter(BaseModel): - text: Optional[TextFilter] = Field(None, alias="$text") - genres: Optional[RegexFilter] = None - year: Optional[int] = None - imdb_rating: Optional[RatingFilter] = Field(None, alias="imdb.rating") - - model_config = { - "populate_by_name" : True - } - - class Pagination(BaseModel): page: int limit: int total: int pages: int - class CreateMovieRequest(BaseModel): title: str year: Optional[int] = None @@ -92,18 +80,37 @@ class CreateMovieRequest(BaseModel): rated: Optional[str] = None runtime: Optional[int] = None poster: Optional[str] = None - -class Comment(BaseModel): - id: Optional[str] = Field(alias="_id") - name: str - email: str - movie_id: str - text: str - date: datetime - model_config = { - "populate_by_name": True - } +class UpdateMovieRequest(BaseModel): + title: Optional[str] = None + year: Optional[int] = None + plot: Optional[str] = None + fullplot: Optional[str] = None + genres: Optional[list[str]] = None + directors: Optional[list[str]] = None + writers: Optional[list[str]] = None + cast: Optional[list[str]] = None + countries: Optional[list[str]] = None + languages: Optional[list[str]] = None + rated: Optional[str] = None + runtime: Optional[int] = None + poster: Optional[str] = None + +class MovieFilter(BaseModel): + title: Optional[str] = None + year: Optional[int] = None + plot: Optional[str] = None + fullplot: Optional[str] = None + genres: Optional[list[str]] = None + directors: Optional[list[str]] = None + writers: Optional[list[str]] = None + cast: Optional[list[str]] = None + countries: Optional[list[str]] = None + languages: Optional[list[str]] = None + rated: Optional[str] = None + runtime: Optional[int] = None + poster: Optional[str] = None + class SuccessResponse(BaseModel, Generic[T]): success: bool = True message: Optional[str] @@ -122,3 +129,4 @@ class ErrorResponse(BaseModel): message: str error: ErrorDetails timestamp: str + \ No newline at end of file diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 6e8d959..76579ee 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,11 +1,11 @@ -from fastapi import APIRouter, Query -from src.database.mongo_client import get_collection -from src.models.models import CreateMovieRequest, Movie, SuccessResponse +from fastapi import APIRouter, Query, Path +from src.database.mongo_client import db, get_collection +from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse, UpdateMovieRequest from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response -import re from bson import ObjectId +import re from bson.errors import InvalidId ''' @@ -13,391 +13,244 @@ Each method demonstrates different MongoDB operations using the PyMongo driver. Implemented Endpoints: -- GET /api/movies/ : Retrieve a list of movies with optional filter, sorting, - and pagination. -- POST /api/movies/batch : Create multiple movies in a single request. - - -''' -router = APIRouter() -#------------------------------------ -# Place get_movie_by_id endpoint here -#------------------------------------ +- GET /api/movies/ : + Retrieve a list of movies with optional filtering, sorting, and pagination. + Supports text search, genre, year, rating filters, and customizable sorting. -""" - GET /api/movies/{id} +- GET /api/movies/{id} : Retrieve a single movie by its ID. - Path Parameters: - id (str): The ObjectId of the movie to retrieve. - Returns: - SuccessResponse[Movie]: A response object containing the movie data. -""" -@router.get("/{id}", response_model=SuccessResponse[Movie]) -async def get_movie_by_id(id: str): - # Validate ObjectId format - try: - object_id = ObjectId(id) - except InvalidId: - return create_error_response( - message="Invalid movie ID format", - code="INTERNAL_SERVER_ERROR", - details=f"The provided ID '{id}' is not a valid ObjectId" - ) +- POST /api/movies/ : + Create a new movie. - movies_collection = get_collection("movies") - try: - movie = await movies_collection.find_one({"_id": object_id}) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) +- POST /api/movies/batch : + Create multiple movies in a single request. - if movie is None: - return create_error_response( - message="Movie not found", - code="INTERNAL_SERVER_ERROR", - details=f"No movie found with ID: {id}" - ) +- PATCH /api/movies/{movie_id} : + Update a single movie by its ID. - movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - - return create_success_response(movie, "Movie retrieved successfully") +- PATCH /api/movies/ : + Batch update movies matching the given filter. -""" - GET /api/movies/ +- DELETE /api/movies/{id} : + Delete a single movie by its ID. - Retrieve a list of movies with optional filtering, sorting, and pagination. +- DELETE /api/movies/ : + Delete multiple movies matching the given filter. - Query Parameters: - q (str, optional): Text search query (searches title, plot, fullplot). - genre (str, optional): Filter by genre. - year (int, optional): Filter by year. - min_rating (float, optional): Minimum IMDB rating. - max_rating (float, optional): Maximum IMDB rating. - limitNum (int, optional): Number of results to return (default: 20, max: 100). - skipNum (int, optional): Number of documents to skip for pagination (default: 0). - sortBy (str, optional): Field to sort by (default: "title"). - sort_order (str, optional): Sort direction, "asc" or "desc" (default: "asc"). +- DELETE /api/movies/{id}/find-and-delete : + Find and delete a movie in a single atomic operation. - Returns: - SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. -""" +- GET /api/movies/aggregations/reportingByComments : + Aggregate movies with their most recent comments using MongoDB $lookup aggregation. -@router.get("/", response_model=SuccessResponse[List[Movie]]) -# Validate the query parameters using FastAPI's Query functionality. -async def get_all_movies( - q:str = Query(default=None), - title: str = Query(default=None), - genre:str = Query(default=None), - year:int = Query(default=None), - min_rating:float = Query(default=None), - max_rating:float = Query(default=None), - limit:int = Query(default=20, ge=1, le=100), - skip:int = Query(default=0, ge=0), - sort_by:str = Query(default="title"), - sort_order:str = Query(default="asc") -): - movies_collection = get_collection("movies") - filter_dict = {} - if q: - filter_dict["$text"] = {"$search": q} - if title: - filter_dict["title"] = {"$regex": title, "$options": "i"} - if genre: - filter_dict["genres"] = {"$regex": genre, "$options": "i"} - if year: - filter_dict["year"] = year - if min_rating is not None or max_rating is not None: - rating_filter = {} - if min_rating is not None: - rating_filter["$gte"] = min_rating - 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 - sort = [(sort_by, sort_order)] +- GET /api/movies/aggregations/reportingByYear : + Aggregate movies by year with average rating and movie count. - # Query the database with the constructed filter, sort, skip, and limit. - try: - cursor = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) - movies = [] - async for movie in cursor: - if movie is None: - continue # Skip null movies instead of raising exception - movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - - # Ensure that the year field contains int value. - if "year" in movie and not isinstance(movie["year"], int): - cleaned_year = re.sub(r"\D", "", str(movie["year"])) - try: - movie["year"] = int(cleaned_year) if cleaned_year else None - except ValueError: - movie["year"] = None - movies.append(movie) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) +- GET /api/movies/aggregations/reportingByDirectors : + Aggregate directors with the most movies and their statistics. - # Return the results wrapped in a SuccessResponse - return create_success_response(movies, f"Found {len(movies)} movies.") +- GET /api/movies/search : + Search movies using MongoDB Search across the plot, fullplot, directors, writers, and cast fields. + Supports compound search operators and fuzzy matching. -#------------------------------------ -# Place create_movie endpoint here -#------------------------------------ +Helper Functions: +- execute_aggregation(pipeline): Executes a MongoDB aggregation pipeline and returns the results. +''' + +router = APIRouter() +#---------------------------------------------------------------------------------------------------------- +# 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 +# but that allows for very generous matching. This can be adjusted as needed. +#---------------------------------------------------------------------------------------------------------- """ - POST /api/movies/ - Create a new movie. - Request Body: - movie (CreateMovieRequest): A movie object containing the movie data. - See CreateMovieRequest model for available fields. + + GET /api/movies/search + + Search movies using MongoDB Search across the plot, fullplot, directors, writers, and cast fields. + You can combine multiple fields in a single query, and control how they are combined using the `search_operator` parameter. + + Query Parameters: + plot (str, optional): Text to search against the plot field. + fullplot (str, optional): Text to search against the fullplot field. + directors (str, optional): Text to search against the directors field. + writers (str, optional): Text to search against the writers field. + 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. + Must be one of "must", "should", "mustNot", or "filter". Default is "must". + Returns: - SuccessResponse[Movie]: A response object containing the created movie data. + SuccessResponse[List[Movie]]: A response object containing the list of matching movies. """ - -@router.post("/", response_model=SuccessResponse[Movie], status_code=201) -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) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) - - # Verify that the document was created before querying it - if not result.acknowledged: - return create_error_response( - message="Failed to create movie", - 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}) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) +@router.get( + "/search", + response_model=SuccessResponse[List[Movie]], + status_code=200, + summary="Search movies using MongoDB Search." +) +async def search_movies( + plot: str = Query(default=None), + fullplot: str = Query(default=None), + directors: str = Query(default=None), + writers: str = Query(default=None), + cast: str = Query(default=None), + limit:int = Query(default=20, ge=1, le=100), + skip:int = Query(default=0, ge=0), + search_operator: str = Query(default="must") +) -> SuccessResponse[List[Movie]]: - if created_movie is None: + search_phrases = [] + + # Validate the search_operator parameter to ensure it's a valid compound operator + valid_operators = {"must", "should", "mustNot", "filter"} + if search_operator not in valid_operators: return create_error_response( - message="Movie creation verification failed", - code="INTERNAL_SERVER_ERROR", - details="Movie was created but could not be retrieved for verification" - ) + message=f"Invalid search_operator '{search_operator}'. The search_operator must be one of {valid_operators}.", + code="INVALID_SEARCH_OPERATOR", + details=None + ) - created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string - - return create_success_response(created_movie, f"Movie '{movie_data['title']}' created successfully") + # Build the search_phrases list based on which fields were provided by the user. + # Each phrase becomes a separate clause in the MongoDB Search compound query. -#------------------------------------ -# Place create_movies_batch endpoint here -#------------------------------------ + if plot: + search_phrases.append({ + # The phrase operator performs an exact phrase match on the specified field. This is useful for searching for specific phrases within text fields. + # The text operator is more flexible and allows for fuzzy matching, making it suitable for fields like names where typos may occur. + "phrase": { + "query": plot, + "path": "plot", + } + }) + if fullplot: + search_phrases.append({ + "phrase": { + "query": fullplot, + "path": "fullplot", + } + }) + if directors: + # The "fuzzy" option enables typo-tolerant (fuzzy) search within MongoDB Search. + # - maxEdits: The maximum number of single-character edits (insertions, deletions, or substitutions) + # allowed when matching the search term to indexed terms. (Range: 1-2; higher = more tolerant) + # - prefixLength: The number of initial characters that must exactly match before fuzzy matching is applied. + # (Higher values make the search stricter and faster.) + # For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/ -''' -POST /api/movies/batch + search_phrases.append({ + "text": { + "query": directors, + "path": "directors", + "fuzzy":{"maxEdits":1, "prefixLength":5} -Create multiple movies in a single request. + } + }) + if writers: + # See comments above regarding fuzzy search options. + search_phrases.append({ + "text": { + "query": writers, + "path": "writers", + "fuzzy":{"maxEdits":1, "prefixLength":5} + } + }) + if cast: + # See comments above regarding fuzzy search options. + search_phrases.append({ + "text": { + "query": cast, + "path": "cast", + "fuzzy":{"maxEdits":1, "prefixLength":5} + } + }) -Request Body: - movies (List[CreateMovieRequest]): A list of movie objects to insert. Each object should include: - - title (str): The movie title. - - year (int, optional): The release year. - - plot (str, optional): Short plot summary. - - fullplot (str, optional): Full plot summary. - - genres (List[str], optional): List of genres. - - directors (List[str], optional): List of directors. - - writers (List[str], optional): List of writers. - - cast (List[str], optional): List of cast members. - - countries (List[str], optional): List of countries. - - languages (List[str], optional): List of languages. - - rated (str, optional): Movie rating. - - runtime (int, optional): Runtime in minutes. - - poster (str, optional): Poster URL. + if not search_phrases: + return create_error_response( + message="At least one search parameter must be provided.", + code="NO_SEARCH_PARAMETERS", + details=None + ) - Returns: - SuccessResponse: A response object containing the number of inserted movies and their IDs. + # Build the aggregation pipeline for MongoDB Search. + # The $search stage uses the specified compound operator (must, should, etc.) + aggregation_pipeline = [ + { + "$search": { + "index": "movieSearchIndex", + "compound": { + search_operator: search_phrases + } + } + }, + {"$skip": skip}, + {"$limit": limit}, -''' + # Project only the fields needed in the response + { + "$project": { + "_id": 1, + "title": 1, + "year": 1, + "plot": 1, + "fullplot": 1, + "released":1, + "runtime": 1, + "poster": 1, + "genres": 1, + "directors": 1, + "writers": 1, + "cast": 1, + "countries": 1, + "languages": 1, + "rated": 1, + "awards": 1, + "imdb": 1, + } + } + ] -@router.post("/batch") -async def create_movies_batch(movies: List[CreateMovieRequest]): - movies_collection = get_collection("movies") - movies_dicts = [] - for movie in movies: - movies_dicts.append(movie.model_dump(exclude_unset=True, exclude_none=True)) - + # Execute the aggregation pipeline using the helper function try: - result = await movies_collection.insert_many(movies_dicts) + results = await execute_aggregation(aggregation_pipeline) except Exception as e: return create_error_response( - message="Database error occurred during batch creation", - code="INTERNAL_SERVER_ERROR", + message="An error occurred while performing the search.", + code="DATABASE_ERROR", details=str(e) - ) - - return create_success_response({ - "insertedCount": len(result.inserted_ids), - "insertedIds": [str(_id) for _id in result.inserted_ids] - }, - f"Successfully created {len(result.inserted_ids)} movies." - ) - - - -#------------------------------------ -# Place update_movie endpoint here -#------------------------------------ - -#------------------------------------ -# Place update_movies_by_batch endpoint here -#------------------------------------ - -#------------------------------------ -# Place delete_movie endpoint here -#------------------------------------ + ) + + # Convert ObjectId to string for each movie in the results + movies = [] + for movie in results: + movie["_id"] = str(movie["_id"]) + movies.append(movie) + return create_success_response(movies, f"Found {len(movies)} movies matching the search criteria.") + """ - DELETE /api/movies/{id} - Delete a single movie by its ID. - Path Parameters: - id (str): The ObjectId of the movie to delete. - Returns: - SuccessResponse[dict]: A response object containing deletion details. -""" - -@router.delete("/{id}", response_model=SuccessResponse[dict]) -async def delete_movie_by_id(id: str): - try: - object_id = ObjectId(id) - except InvalidId: - return create_error_response( - message="Invalid movie ID format", - code="INTERNAL_SERVER_ERROR", - details=f"The provided ID '{id}' is not a valid ObjectId" - ) - - movies_collection = get_collection("movies") - try: - # Use deleteOne() to remove a single document - result = await movies_collection.delete_one({"_id": object_id}) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) - - if result.deleted_count == 0: - return create_error_response( - message="Movie not found", - code="INTERNAL_SERVER_ERROR", - details=f"No movie found with ID: {id}" - ) - - return create_success_response( - {"deletedCount": result.deleted_count}, - "Movie deleted successfully" - ) - -#------------------------------------ -# Place delete_movies_by_batch endpoint here -#------------------------------------ - -#------------------------------------ -# Place find_and_delete_movie endpoint here -#------------------------------------ - -""" - DELETE /api/movies/{id}/find-and-delete - Finds and deletes a movie in a single atomic operation. - Demonstrates the findOneAndDelete() operation. - Path Parameters: - id (str): The ObjectId of the movie to find and delete. - Returns: - SuccessResponse[Movie]: A response object containing the deleted movie data. -""" - -@router.delete("/{id}/find-and-delete", response_model=SuccessResponse[Movie]) -async def find_and_delete_movie(id: str): - try: - object_id = ObjectId(id) - except InvalidId: - return create_error_response( - message="Invalid movie ID format", - code="INTERNAL_SERVER_ERROR", - details=f"The provided ID '{id}' is not a valid ObjectId" - ) - - movies_collection = get_collection("movies") - # Use find_one_and_delete() to find and delete in a single atomic operation - # This is useful when you need to return the deleted document - # or ensure the document exists before deletion - try: - deleted_movie = await movies_collection.find_one_and_delete({"_id": object_id}) - except Exception as e: - return create_error_response( - message="Database error occurred", - code="INTERNAL_SERVER_ERROR", - details=str(e) - ) - - if deleted_movie is None: - return create_error_response( - message="Movie not found", - 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") - -async def execute_aggregation(pipeline: list) -> list: - """Helper function to execute aggregation pipeline and return results""" - print(f"Executing pipeline: {pipeline}") # Debug logging - - 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 - - print(f"Aggregation returned {len(results)} results") # Debug logging - if len(results) <= 3: # Log first few results for debugging - for i, doc in enumerate(results): - print(f"Result {i+1}: {doc}") - - return results - -""" - GET /api/movies/reportingByComments - Aggregate movies with their most recent comments using MongoDB $lookup aggregation. - Joins movies with comments collection to show recent comment activity. - Query Parameters: - limit (int, optional): Number of results to return (default: 10, max: 50). - movie_id (str, optional): Filter by specific movie ObjectId. + GET /api/movies/aggregations/reportingByComments + Aggregate movies with their most recent comments using MongoDB $lookup aggregation. + Joins movies with comments collection to show recent comment activity. + Query Parameters: + limit (int, optional): Number of results to return (default: 10, max: 50). + movie_id (str, optional): Filter by specific movie ObjectId. Returns: SuccessResponse[List[dict]]: A response object containing movies with their most recent comments. """ -@router.get("/api/movies/reportingByComments", response_model=SuccessResponse[List[dict]]) +@router.get("/aggregations/reportingByComments", + response_model=SuccessResponse[List[dict]], + status_code=200, + summary="Aggregate movies with their most recent comments.") async def aggregate_movies_recent_commented( limit: int = Query(default=10, ge=1, le=50), movie_id: str = Query(default=None) @@ -539,14 +392,17 @@ async def aggregate_movies_recent_commented( """ - GET /api/movies/reportingByYear + GET /api/movies/aggregations/reportingByYear Aggregate movies by year with average rating and movie count. Reports yearly statistics including average rating and total movies per year. Returns: SuccessResponse[List[dict]]: A response object containing yearly movie statistics. """ -@router.get("/api/movies/reportingByYear", response_model=SuccessResponse[List[dict]]) +@router.get("/aggregations/reportingByYear", + response_model=SuccessResponse[List[dict]], + status_code=200, + summary="Aggregate movies by year with average rating and movie count.") async def aggregate_movies_by_year(): # Define aggregation pipeline to group movies by year with statistics # This pipeline demonstrates grouping, statistical calculations, and data cleaning @@ -660,7 +516,7 @@ async def aggregate_movies_by_year(): """ - GET /api/movies/reportingByDirectors + GET /api/movies/aggregations/reportingByDirectors Aggregate directors with the most movies and their statistics. Reports directors sorted by number of movies directed. Query Parameters: @@ -669,7 +525,10 @@ async def aggregate_movies_by_year(): SuccessResponse[List[dict]]: A response object containing director statistics. """ -@router.get("/api/movies/reportingByDirectors", response_model=SuccessResponse[List[dict]]) +@router.get("/aggregations/reportingByDirectors", + response_model=SuccessResponse[List[dict]], + status_code=200, + summary="Aggregate directors with the most movies and their statistics.") async def aggregate_directors_most_movies( limit: int = Query(default=20, ge=1, le=100) ): @@ -755,20 +614,590 @@ async def aggregate_directors_most_movies( f"Found {len(results)} directors with most movies" ) -# ---- Old testing endpoint, will be removed later ---- -''' -# Testing the ErrorReponse Model -@router.get("/error") -async def test_error(): + + +#------------------------------------ +# Place get_movie_by_id endpoint here +#------------------------------------ + +""" + GET /api/movies/{id} + Retrieve a single movie by its ID. + Path Parameters: + id (str): The ObjectId of the movie to retrieve. + Returns: + SuccessResponse[Movie]: A response object containing the movie data. +""" + +@router.get("/{id}", + response_model=SuccessResponse[Movie], + status_code=200, + summary="Retrieve a single movie by its ID.") +async def get_movie_by_id(id: str): + # Validate ObjectId format try: - raise ValueError("This is a test error.") - except ValueError as e: + object_id = ObjectId(id) + except InvalidId: return create_error_response( - message="A test error occurred.", - code="TEST_ERROR", - details=str(e) - ) -''' + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) + + movies_collection = get_collection("movies") + try: + movie = await movies_collection.find_one({"_id": object_id}) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + if movie is None: + return create_error_response( + message="Movie not found", + code="INTERNAL_SERVER_ERROR", + details=f"No movie found with ID: {id}" + ) + + movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + + return create_success_response(movie, "Movie retrieved successfully") + +""" + GET /api/movies/ + + Retrieve a list of movies with optional filtering, sorting, and pagination. + + Query Parameters: + q (str, optional): Text search query (searches title, plot, fullplot). + genre (str, optional): Filter by genre. + year (int, optional): Filter by year. + min_rating (float, optional): Minimum IMDB rating. + max_rating (float, optional): Maximum IMDB rating. + limitNum (int, optional): Number of results to return (default: 20, max: 100). + skipNum (int, optional): Number of documents to skip for pagination (default: 0). + sortBy (str, optional): Field to sort by (default: "title"). + sort_order (str, optional): Sort direction, "asc" or "desc" (default: "asc"). + + Returns: + SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. +""" + +@router.get("/", + response_model=SuccessResponse[List[Movie]], + status_code=200, + summary="Retrieve a list of movies with optional filtering, sorting, and pagination.") +# Validate the query parameters using FastAPI's Query functionality. +async def get_all_movies( + q:str = Query(default=None), + title: str = Query(default=None), + genre:str = Query(default=None), + year:int = Query(default=None), + min_rating:float = Query(default=None), + max_rating:float = Query(default=None), + limit:int = Query(default=20, ge=1, le=100), + skip:int = Query(default=0, ge=0), + sort_by:str = Query(default="title"), + sort_order:str = Query(default="asc") +): + movies_collection = get_collection("movies") + filter_dict = {} + if q: + filter_dict["$text"] = {"$search": q} + if title: + filter_dict["title"] = {"$regex": title, "$options": "i"} + if genre: + filter_dict["genres"] = {"$regex": genre, "$options": "i"} + if year: + filter_dict["year"] = year + if min_rating is not None or max_rating is not None: + rating_filter = {} + if min_rating is not None: + rating_filter["$gte"] = min_rating + 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 + + sort = [(sort_by, sort_order)] + + # Query the database with the constructed filter, sort, skip, and limit. + + try: + 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 = [] + + async for movie in result: + movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + # Ensure that the year field contains int value. + if "year" in movie and not isinstance(movie["year"], int): + cleaned_year = re.sub(r"\D", "", str(movie["year"])) + try: + movie["year"] = int(cleaned_year) if cleaned_year else None + except ValueError: + movie["year"] = None + + movies.append(movie) + # 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. + Request Body: + movie (CreateMovieRequest): A movie object containing the movie data. + See CreateMovieRequest model for available fields. + Returns: + SuccessResponse[Movie]: A response object containing the created movie data. +""" + +@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) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + # Verify that the document was created before querying it + if not result.acknowledged: + return create_error_response( + message="Failed to create movie", + 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}) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + if created_movie is None: + return create_error_response( + message="Movie creation verification failed", + code="INTERNAL_SERVER_ERROR", + details="Movie was created but could not be retrieved for verification" + ) + + created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string + + return create_success_response(created_movie, f"Movie '{movie_data['title']}' created successfully") + +#------------------------------------ +# Place create_movies_batch endpoint here +#------------------------------------ + +""" +POST /api/movies/batch + +Create multiple movies in a single request. + +Request Body: + movies (List[CreateMovieRequest]): A list of movie objects to insert. Each object should include: + - title (str): The movie title. + - year (int, optional): The release year. + - plot (str, optional): Short plot summary. + - fullplot (str, optional): Full plot summary. + - genres (List[str], optional): List of genres. + - directors (List[str], optional): List of directors. + - writers (List[str], optional): List of writers. + - cast (List[str], optional): List of cast members. + - countries (List[str], optional): List of countries. + - languages (List[str], optional): List of languages. + - rated (str, optional): Movie rating. + - runtime (int, optional): Runtime in minutes. + - poster (str, optional): Poster URL. + + Returns: + SuccessResponse: A response object containing the number of inserted movies and their IDs. + +""" + +@router.post( + "/batch", + response_model=SuccessResponse[dict], + status_code = 201, + summary = "Create multiple movies in a single request." + ) +async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessResponse[dict]: + movies_collection = get_collection("movies") + + #Verify that the movies list is not empty + if not movies: + return create_error_response( + message="Request body must be a non-empty list of movies.", + code="INVALID_INPUT", + details=None + ) + + movies_dicts = [] + + for movie in movies: + movies_dicts.append(movie.model_dump(exclude_unset=True, exclude_none=True)) + + try: + result = await movies_collection.insert_many(movies_dicts) + except Exception as e: + return create_error_response( + message="An error occurred while creating movies.", + code="DATABASE_ERROR", + details=str(e) + ) + + try: + result = await movies_collection.insert_many(movies_dicts) + except Exception as e: + return create_error_response( + message="Database error occurred during batch creation", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + return create_success_response({ + "insertedCount": len(result.inserted_ids), + "insertedIds": [str(_id) for _id in result.inserted_ids] + }, + f"Successfully created {len(result.inserted_ids)} movies." + ) + +#------------------------------------ +# Place update_movie endpoint here +#------------------------------------ + +""" + PATCH /api/movies/{id} + + Update a single movie by its ID. + + Path Parameters: + id (str): The ObjectId of the movie to update + + Request Body: + move_data (UpdateMovieRequest): Fields and values to update. Only provided fields will be updated. + + Returns: + SuccessResponse: The updated movie document, the number of fields modified and a success message. +""" +@router.patch( + "/{id}", + response_model=SuccessResponse[Movie], + status_code=200, + summary="Update a single movie by its ID.") +async def update_movie( + movie_data: UpdateMovieRequest, + movie_id: str = Path(..., alias="id") +) -> SuccessResponse[Movie]: + + movies_collection = get_collection("movies") + + # Validate the ObjectId + try: + movie_id = ObjectId(movie_id) + except Exception : + return create_error_response( + 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 + if not update_dict: + return create_error_response( + message="No valid fields provided for update.", + code="NO_UPDATE_DATA", + details=None + ) + + try: + result = await movies_collection.update_one( + {"_id": movie_id}, + {"$set":update_dict} + ) + except Exception as e: + return create_error_response( + message="An error occurred while updating the movie.", + code="DATABASE_ERROR", + details=str(e) + ) + + if result.matched_count == 0: + return create_error_response( + message="No movie with that _id was found.", + code="MOVIE_NOT_FOUND", + details=str(movie_id) + ) + + updatedMovie = await movies_collection.find_one({"_id": movie_id}) + print(updatedMovie) + updatedMovie["_id"] = str(updatedMovie["_id"]) + + return create_success_response(updatedMovie, f"Movie updated successfully. Modified {len(update_dict)} fields.") + +#------------------------------------ +# Place update_movies_by_batch endpoint here +#------------------------------------ + +""" + PATCH /api/movies + + Batch update movies matching the given filter + + Request Body: + filter (MoviesUpdateFilter): Criteria to select which movies to update. Only movies matching this filter will be updated. + update (UpdateMovieRequest): Fields and values to update for the matched movies. Only provided fields will be updated. + Returns: + SuccessResponse: A response object containing the number of matched and modified movies and a success message. +""" + +@router.patch("/", + response_model=SuccessResponse[dict], + status_code=200, + summary="Batch update movies matching the given filter." + ) +async def update_movies_batch( + filter: MovieFilter, + update: UpdateMovieRequest +) -> SuccessResponse[dict]: + movies_collection = get_collection("movies") + + filter_dict = filter.model_dump(exclude_unset=True, exclude_none=True) + update_dict = update.model_dump(exclude_unset=True, exclude_none=True) + + #Verify the filter and the update dicts are not empty + if not filter_dict or not update_dict: + return create_error_response( + message="Both filter and update objects are required", + code="MISSING_REQUIRED_FIELDS", + details=None + ) + + try: + result = await movies_collection.update_many(filter_dict,{"$set": update_dict}) + except Exception as e: + return create_error_response( + message="An error occurred while updating movies.", + code="DATABASE_ERROR", + details=str(e) + ) + + return create_success_response({ + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count + }, + 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. + Path Parameters: + id (str): The ObjectId of the movie to delete. + Returns: + SuccessResponse[dict]: A response object containing deletion details. +""" + +@router.delete("/{id}", + response_model=SuccessResponse[dict], + status_code=200, + summary="Delete a single movie by its ID.") +async def delete_movie_by_id(id: str): + try: + object_id = ObjectId(id) + except InvalidId: + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) + + movies_collection = get_collection("movies") + try: + # Use deleteOne() to remove a single document + result = await movies_collection.delete_one({"_id": object_id}) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + if result.deleted_count == 0: + return create_error_response( + message="Movie not found", + code="INTERNAL_SERVER_ERROR", + details=f"No movie found with ID: {id}" + ) + + return create_success_response( + {"deletedCount": result.deleted_count}, + "Movie deleted successfully" + ) + +#------------------------------------ +# Place delete_movies_by_batch endpoint here +#------------------------------------ +""" + DELETE /api/movies/ + + Delete multiple movies matching the given filter. + + Request Body: + movie_filter (MovieFilter): Criteria to select which movies to delete. Only movies matching this filter will be removed. + + Returns: + SuccessResponse: An object containing the number of deleted movies and a success message. +""" + +@router.delete( + "/", + response_model=SuccessResponse[dict], + status_code=200, + summary="Delete multiple movies matching the given filter." +) +async def delete_movies_batch(movie_filter:MovieFilter) -> SuccessResponse[dict]: + + movies_collection = get_collection("movies") + movie_filter_dict = movie_filter.model_dump(exclude_unset=True,exclude_none=True) + + if not movie_filter_dict: + return create_error_response( + message="Filter object is required and cannot be empty.", + code="MISSING_FILTER", + details=None + ) + + try: + result = await movies_collection.delete_many(movie_filter_dict) + except Exception as e: + return create_error_response( + message="An error occurred while deleting movies.", + code="DATABASE_ERROR", + details=str(e) + ) + + return create_success_response( + {"deletedCount":result.deleted_count}, + 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. + Demonstrates the findOneAndDelete() operation. + Path Parameters: + id (str): The ObjectId of the movie to find and delete. + Returns: + SuccessResponse[Movie]: A response object containing the deleted movie data. +""" + +@router.delete("/{id}/find-and-delete", + response_model=SuccessResponse[Movie], + status_code=200, + summary="Find and delete a movie in a single operation.") +async def find_and_delete_movie(id: str): + try: + object_id = ObjectId(id) + except InvalidId: + return create_error_response( + message="Invalid movie ID format", + code="INTERNAL_SERVER_ERROR", + details=f"The provided ID '{id}' is not a valid ObjectId" + ) + + movies_collection = get_collection("movies") + # Use find_one_and_delete() to find and delete in a single atomic operation + # This is useful when you need to return the deleted document + # or ensure the document exists before deletion + try: + deleted_movie = await movies_collection.find_one_and_delete({"_id": object_id}) + except Exception as e: + return create_error_response( + message="Database error occurred", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) + + if deleted_movie is None: + return create_error_response( + message="Movie not found", + 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") + +#------------------------------------ +#Helper Functions +#------------------------------------ + +""" + Helper function to execute aggregation pipeline and return results. + + Args: + pipeline: MongoDB aggregation pipeline stages + + Returns: + List of documents from aggregation result +""" + +async def execute_aggregation(pipeline: list) -> list: + """Helper function to execute aggregation pipeline and return results""" + print(f"Executing pipeline: {pipeline}") # Debug logging + + 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 + + print(f"Aggregation returned {len(results)} results") # Debug logging + if len(results) <= 3: # Log first few results for debugging + for i, doc in enumerate(results): + print(f"Result {i+1}: {doc}") + + return results # ---- Place Vector Search Here ---- From d79ce30cd1c0cdbc6ba4b681a81ad1afd81012dd Mon Sep 17 00:00:00 2001 From: cbullinger Date: Thu, 30 Oct 2025 16:19:45 -0400 Subject: [PATCH 032/110] feat(java): add aggregation endpoints --- .../controller/MovieControllerImpl.java | 100 +++++++- .../model/dto/DirectorStatisticsResult.java | 37 +++ .../model/dto/MovieWithCommentsResult.java | 127 ++++++++++ .../model/dto/MoviesByYearResult.java | 52 ++++ .../samplemflix/service/MovieService.java | 29 +++ .../samplemflix/service/MovieServiceImpl.java | 227 ++++++++++++++++++ .../controller/MovieControllerTest.java | 178 ++++++++++++++ .../integration/MovieIntegrationTest.java | 9 +- .../samplemflix/service/MovieServiceTest.java | 186 ++++++++++++++ 9 files changed, 942 insertions(+), 3 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DirectorStatisticsResult.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MoviesByYearResult.java diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 0d243c3..130635b 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -5,7 +5,10 @@ import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; import com.mongodb.samplemflix.model.dto.DeleteResponse; +import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; +import com.mongodb.samplemflix.model.dto.MoviesByYearResult; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.model.response.SuccessResponse; import com.mongodb.samplemflix.service.MovieService; @@ -20,8 +23,8 @@ /** * REST controller for movie-related endpoints. - *

- * This controller handles all HTTP requests for movie operations including: + * + *

This controller handles all HTTP requests for movie operations including: *

  * - GET /api/movies - Get all movies with filtering, sorting, and pagination
  * - GET /api/movies/{id} - Get a single movie by ID
@@ -32,6 +35,9 @@
  * - DELETE /api/movies/{id} - Delete a movie
  * - DELETE /api/movies - Delete multiple movies
  * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie
+ * - GET /api/movies/reportingByComments - Aggregate movies with most comments
+ * - GET /api/movies/reportingByYear - Aggregate movies by year with statistics
+ * - GET /api/movies/reportingByDirectors - Aggregate directors with most movies
  * 
*/ @RestController @@ -249,4 +255,94 @@ public ResponseEntity> deleteMoviesBatch( return ResponseEntity.ok(response); } + + // Aggregation endpoints for reporting + + /** + * GET /api/movies/aggregations/comments + * + *

Aggregates movies with their most recent comments. + * Demonstrates MongoDB $lookup (join) operation to combine movies with comments. + * + * @param limit Maximum number of movies to return (default: 10, max: 50) + * @param movieId Optional movie ID to filter by specific movie + * @return List of movies with their recent comments + */ + @GetMapping("/aggregations/comments") + public ResponseEntity>> getMoviesWithMostComments( + @RequestParam(defaultValue = "10") Integer limit, + @RequestParam(required = false) String movieId) { + + List results = movieService.getMoviesWithMostComments(limit, movieId); + + // Calculate total comments across all movies + int totalComments = results.stream() + .mapToInt(result -> result.getTotalComments() != null ? result.getTotalComments() : 0) + .sum(); + + String message = movieId != null + ? String.format("Found %d comments from movie", totalComments) + : String.format("Found %d comments from %d movie%s", + totalComments, results.size(), results.size() != 1 ? "s" : ""); + + SuccessResponse> response = + SuccessResponse.>builder() + .success(true) + .message(message) + .data(results) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * GET /api/movies/aggregations/years + * + *

Aggregates movies by year with statistics. + * Demonstrates MongoDB $group operation for statistical aggregation. + * + * @return List of yearly statistics including movie count and average rating + */ + @GetMapping("/aggregations/years") + public ResponseEntity>> getMoviesByYearWithStats() { + + List results = movieService.getMoviesByYearWithStats(); + + SuccessResponse> response = + SuccessResponse.>builder() + .success(true) + .message(String.format("Aggregated statistics for %d years", results.size())) + .data(results) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * GET /api/movies/aggregations/directors + * + *

Aggregates directors with the most movies. + * Demonstrates MongoDB $unwind operation for array flattening and aggregation. + * + * @param limit Maximum number of directors to return (default: 20, max: 100) + * @return List of directors with their movie count and average rating + */ + @GetMapping("/aggregations/directors") + public ResponseEntity>> getDirectorsWithMostMovies( + @RequestParam(defaultValue = "20") Integer limit) { + + List results = movieService.getDirectorsWithMostMovies(limit); + + SuccessResponse> response = + SuccessResponse.>builder() + .success(true) + .message(String.format("Found %d directors with most movies", results.size())) + .data(results) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DirectorStatisticsResult.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DirectorStatisticsResult.java new file mode 100644 index 0000000..6bf04dc --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/DirectorStatisticsResult.java @@ -0,0 +1,37 @@ +package com.mongodb.samplemflix.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for director statistics aggregation result. + * + *

This class represents the result of the reportingByDirectors aggregation + * which finds directors with the most movies and their statistics. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DirectorStatisticsResult { + + /** + * Director name. + */ + private String director; + + /** + * Number of movies directed by this director. + */ + private Integer movieCount; + + /** + * Average IMDB rating of this director's movies. + */ + private Double averageRating; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java new file mode 100644 index 0000000..ede0b30 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java @@ -0,0 +1,127 @@ +package com.mongodb.samplemflix.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Date; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for movies with their most recent comments aggregation result. + * + *

This class represents the result of the reportingByComments aggregation + * which joins movies with their comments and returns movies with the most comments. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MovieWithCommentsResult { + + /** + * Movie ID as string. + */ + private String id; + + /** + * Movie title. + */ + private String title; + + /** + * Release year. + */ + private Integer year; + + /** + * Short plot summary. + */ + private String plot; + + /** + * Poster image URL. + */ + private String poster; + + /** + * List of genres. + */ + private List genres; + + /** + * IMDB rating information. + */ + private ImdbInfo imdb; + + /** + * Most recent comments for this movie. + */ + private List recentComments; + + /** + * Total number of comments for this movie. + */ + private Integer totalComments; + + /** + * Date of the most recent comment. + */ + private Date mostRecentCommentDate; + + /** + * Nested class for IMDB information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ImdbInfo { + /** + * IMDB rating (0.0 to 10.0). + */ + private Double rating; + + /** + * Number of votes. + */ + private Integer votes; + } + + /** + * Nested class for comment information. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CommentInfo { + /** + * Comment ID as string. + */ + private String id; + + /** + * Commenter name. + */ + private String name; + + /** + * Commenter email. + */ + private String email; + + /** + * Comment text. + */ + private String text; + + /** + * Comment date. + */ + private Date date; + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MoviesByYearResult.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MoviesByYearResult.java new file mode 100644 index 0000000..1ab4da2 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MoviesByYearResult.java @@ -0,0 +1,52 @@ +package com.mongodb.samplemflix.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for movies aggregated by year with statistics. + * + *

This class represents the result of the reportingByYear aggregation + * which groups movies by release year and calculates statistics per year. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MoviesByYearResult { + + /** + * Release year. + */ + private Integer year; + + /** + * Number of movies released in this year. + */ + private Integer movieCount; + + /** + * Average IMDB rating for movies in this year. + */ + private Double averageRating; + + /** + * Highest IMDB rating for movies in this year. + */ + private Double highestRating; + + /** + * Lowest IMDB rating for movies in this year. + */ + private Double lowestRating; + + /** + * Total number of IMDB votes for all movies in this year. + */ + private Long totalVotes; +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 7327749..5f97db8 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -5,7 +5,10 @@ import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; import com.mongodb.samplemflix.model.dto.DeleteResponse; +import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; +import com.mongodb.samplemflix.model.dto.MoviesByYearResult; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import java.util.List; import org.bson.Document; @@ -32,4 +35,30 @@ public interface MovieService { DeleteResponse deleteMoviesBatch(Document filter); Movie findAndDeleteMovie(String id); + + // Aggregation endpoints for reporting + + /** + * Aggregates movies with their most recent comments. + * + * @param limit Maximum number of movies to return + * @param movieId Optional movie ID to filter by specific movie + * @return List of movies with their recent comments + */ + List getMoviesWithMostComments(Integer limit, String movieId); + + /** + * Aggregates movies by year with statistics. + * + * @return List of yearly statistics including movie count and average rating + */ + List getMoviesByYearWithStats(); + + /** + * Aggregates directors with the most movies. + * + * @param limit Maximum number of directors to return + * @return List of directors with their movie count and average rating + */ + List getDirectorsWithMostMovies(Integer limit); } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 571cd07..1955ccb 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -11,7 +11,10 @@ import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; import com.mongodb.samplemflix.model.dto.DeleteResponse; +import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; +import com.mongodb.samplemflix.model.dto.MoviesByYearResult; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; import java.util.Collection; @@ -24,6 +27,10 @@ import org.bson.types.ObjectId; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.ArrayOperators; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; @@ -330,6 +337,226 @@ private Update buildUpdate(UpdateMovieRequest request) { return update; } + // Aggregation methods for reporting + + @Override + public List getMoviesWithMostComments(Integer limit, String movieId) { + // Validate and set default limit + int resultLimit = Math.clamp(limit != null ? limit : 10, 1, 50); + + // Build match criteria + Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030); + + // Add movie ID filter if provided + if (movieId != null && !movieId.trim().isEmpty()) { + if (!ObjectId.isValid(movieId)) { + throw new ValidationException("Invalid movie ID format"); + } + matchCriteria = matchCriteria.and(Movie.Fields.ID).is(new ObjectId(movieId)); + } + + // Build aggregation pipeline + // This demonstrates $lookup (join), $unwind, $sort, $group, and $project operations + Aggregation aggregation = Aggregation.newAggregation( + // STAGE 1: Match movies with valid year data + Aggregation.match(matchCriteria), + + // STAGE 2: Lookup (join) with comments collection + Aggregation.lookup("comments", "_id", "movie_id", "comments"), + + // STAGE 3: Filter to only movies with comments + Aggregation.match(Criteria.where("comments").ne(List.of())), + + // STAGE 4: Add computed fields + Aggregation.project() + .and(Movie.Fields.ID).as("_id") + .and(Movie.Fields.TITLE).as("title") + .and(Movie.Fields.YEAR).as("year") + .and(Movie.Fields.PLOT).as("plot") + .and(Movie.Fields.POSTER).as("poster") + .and(Movie.Fields.GENRES).as("genres") + .and(Movie.Fields.IMDB).as("imdb") + .and("comments").as("comments") + .and(ArrayOperators.Size.lengthOfArray("comments")).as("totalComments") + .and(ArrayOperators.ArrayElemAt.arrayOf("comments.date").elementAt(0)).as("mostRecentCommentDate"), + + // STAGE 5: Sort by most recent comment date (descending) + Aggregation.sort(Sort.Direction.DESC, "mostRecentCommentDate"), + + // STAGE 6: Limit results + Aggregation.limit(resultLimit), + + // STAGE 7: Project final output with recent comments slice + Aggregation.project() + .and(ConditionalOperators.ifNull("_id").then("")).as("id") + .and("title").as("title") + .and("year").as("year") + .and("plot").as("plot") + .and("poster").as("poster") + .and("genres").as("genres") + .and("imdb").as("imdb") + .and(ArrayOperators.Slice.sliceArrayOf("comments").itemCount(5)).as("recentComments") + .and("totalComments").as("totalComments") + .and("mostRecentCommentDate").as("mostRecentCommentDate") + ); + + AggregationResults results = mongoTemplate.aggregate( + aggregation, "movies", Document.class); + + // Convert Document results to DTOs + return results.getMappedResults().stream() + .map(this::mapToMovieWithCommentsResult) + .collect(Collectors.toList()); + } + + @Override + public List getMoviesByYearWithStats() { + // Build aggregation pipeline + // This demonstrates $group with statistical operators and $project for data shaping + Aggregation aggregation = Aggregation.newAggregation( + // STAGE 1: Match movies with valid year data + Aggregation.match( + Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + ), + + // STAGE 2: Group by year and calculate statistics + Aggregation.group(Movie.Fields.YEAR) + .count().as("movieCount") + .avg(Movie.Fields.IMDB_RATING).as("averageRating") + .max(Movie.Fields.IMDB_RATING).as("highestRating") + .min(Movie.Fields.IMDB_RATING).as("lowestRating") + .sum("imdb.votes").as("totalVotes"), + + // STAGE 3: Project final output with renamed fields + Aggregation.project() + .and("_id").as("year") + .and("movieCount").as("movieCount") + .and("averageRating").as("averageRating") + .and("highestRating").as("highestRating") + .and("lowestRating").as("lowestRating") + .and("totalVotes").as("totalVotes") + .andExclude("_id"), + + // STAGE 4: Sort by year (descending) + Aggregation.sort(Sort.Direction.DESC, "year") + ); + + AggregationResults results = mongoTemplate.aggregate( + aggregation, "movies", MoviesByYearResult.class); + + // Round average rating to 2 decimal places + return results.getMappedResults().stream() + .peek(result -> { + if (result.getAverageRating() != null) { + result.setAverageRating( + Math.round(result.getAverageRating() * 100.0) / 100.0 + ); + } + }) + .collect(Collectors.toList()); + } + + @Override + public List getDirectorsWithMostMovies(Integer limit) { + // Validate and set default limit + int resultLimit = Math.clamp(limit != null ? limit : 20, 1, 100); + + // Build aggregation pipeline + // This demonstrates $unwind for array flattening and $group for aggregation + Aggregation aggregation = Aggregation.newAggregation( + // STAGE 1: Match movies with directors and valid year + Aggregation.match( + Criteria.where(Movie.Fields.DIRECTORS).exists(true).ne(null).ne(List.of()) + .and(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + ), + + // STAGE 2: Unwind directors array + Aggregation.unwind(Movie.Fields.DIRECTORS), + + // STAGE 3: Filter out null/empty director names + Aggregation.match( + Criteria.where(Movie.Fields.DIRECTORS).ne(null).ne("") + ), + + // STAGE 4: Group by director and calculate statistics + Aggregation.group(Movie.Fields.DIRECTORS) + .count().as("movieCount") + .avg(Movie.Fields.IMDB_RATING).as("averageRating"), + + // STAGE 5: Sort by movie count (descending) + Aggregation.sort(Sort.Direction.DESC, "movieCount"), + + // STAGE 6: Limit results + Aggregation.limit(resultLimit), + + // STAGE 7: Project final output + Aggregation.project() + .and("_id").as("director") + .and("movieCount").as("movieCount") + .and("averageRating").as("averageRating") + .andExclude("_id") + ); + + AggregationResults results = mongoTemplate.aggregate( + aggregation, "movies", DirectorStatisticsResult.class); + + // Round average rating to 2 decimal places + return results.getMappedResults().stream() + .peek(result -> { + if (result.getAverageRating() != null) { + result.setAverageRating( + Math.round(result.getAverageRating() * 100.0) / 100.0 + ); + } + }) + .collect(Collectors.toList()); + } + + /** + * Helper method to map Document to MovieWithCommentsResult. + */ + private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { + // Extract IMDB info + MovieWithCommentsResult.ImdbInfo imdbInfo = null; + Document imdbDoc = doc.get("imdb", Document.class); + if (imdbDoc != null) { + imdbInfo = MovieWithCommentsResult.ImdbInfo.builder() + .rating(imdbDoc.getDouble("rating")) + .votes(imdbDoc.getInteger("votes")) + .build(); + } + + // Extract recent comments + List recentComments = null; + @SuppressWarnings("unchecked") + List commentsDoc = (List) doc.get("recentComments"); + if (commentsDoc != null) { + recentComments = commentsDoc.stream() + .map(commentDoc -> MovieWithCommentsResult.CommentInfo.builder() + .id(commentDoc.getObjectId("_id") != null ? + commentDoc.getObjectId("_id").toHexString() : null) + .name(commentDoc.getString("name")) + .email(commentDoc.getString("email")) + .text(commentDoc.getString("text")) + .date(commentDoc.getDate("date")) + .build()) + .collect(Collectors.toList()); + } + + return MovieWithCommentsResult.builder() + .id(doc.getString("id")) + .title(doc.getString("title")) + .year(doc.getInteger("year")) + .plot(doc.getString("plot")) + .poster(doc.getString("poster")) + .genres((List) doc.get("genres")) + .imdb(imdbInfo) + .recentComments(recentComments) + .totalComments(doc.getInteger("totalComments")) + .mostRecentCommentDate(doc.getDate("mostRecentCommentDate")) + .build(); + } + // TODO: Add advanced query methods // - getMoviesByGenreStatistics() - Aggregation pipeline for genre statistics // - getTopRatedMovies(int limit) - Movies sorted by rating diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index b9b5a65..2ac237f 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -14,10 +14,14 @@ import com.mongodb.samplemflix.model.dto.BatchInsertResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; import com.mongodb.samplemflix.model.dto.DeleteResponse; +import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; +import com.mongodb.samplemflix.model.dto.MoviesByYearResult; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.service.MovieService; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -324,4 +328,178 @@ void testFindAndDeleteMovie_NotFound() throws Exception { .andExpect(jsonPath("$.success").value(false)) .andExpect(jsonPath("$.error.code").value("RESOURCE_NOT_FOUND")); } + + // ==================== AGGREGATION ENDPOINT TESTS ==================== + + @Test + @DisplayName("GET /api/movies/aggregations/comments - Should return movies with most comments") + void testGetMoviesWithMostComments_Success() throws Exception { + // Arrange + MovieWithCommentsResult.CommentInfo comment = MovieWithCommentsResult.CommentInfo.builder() + .id(new ObjectId().toHexString()) + .name("John Doe") + .email("john@example.com") + .text("Great movie!") + .date(new Date()) + .build(); + + MovieWithCommentsResult.ImdbInfo imdb = MovieWithCommentsResult.ImdbInfo.builder() + .rating(8.5) + .votes(1000) + .build(); + + MovieWithCommentsResult result = MovieWithCommentsResult.builder() + .id(testId.toHexString()) + .title("Test Movie") + .year(2024) + .plot("Test plot") + .poster("http://example.com/poster.jpg") + .genres(Arrays.asList("Action", "Drama")) + .imdb(imdb) + .recentComments(Arrays.asList(comment)) + .totalComments(5) + .mostRecentCommentDate(new Date()) + .build(); + + when(movieService.getMoviesWithMostComments(anyInt(), isNull())).thenReturn(Arrays.asList(result)); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/comments")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(1))) + .andExpect(jsonPath("$.data[0].title").value("Test Movie")) + .andExpect(jsonPath("$.data[0].year").value(2024)) + .andExpect(jsonPath("$.data[0].totalComments").value(5)) + .andExpect(jsonPath("$.data[0].recentComments", hasSize(1))) + .andExpect(jsonPath("$.data[0].recentComments[0].name").value("John Doe")); + } + + @Test + @DisplayName("GET /api/movies/aggregations/comments - Should accept limit parameter") + void testGetMoviesWithMostComments_WithLimit() throws Exception { + // Arrange + when(movieService.getMoviesWithMostComments(eq(5), isNull())).thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/comments") + .param("limit", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } + + @Test + @DisplayName("GET /api/movies/aggregations/comments - Should accept movieId parameter") + void testGetMoviesWithMostComments_WithMovieId() throws Exception { + // Arrange + String movieId = testId.toHexString(); + when(movieService.getMoviesWithMostComments(anyInt(), eq(movieId))).thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/comments") + .param("movieId", movieId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } + + @Test + @DisplayName("GET /api/movies/aggregations/comments - Should return 400 for invalid movieId") + void testGetMoviesWithMostComments_InvalidMovieId() throws Exception { + // Arrange + String invalidMovieId = "invalid-id"; + when(movieService.getMoviesWithMostComments(anyInt(), eq(invalidMovieId))) + .thenThrow(new ValidationException("Invalid movie ID format")); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/comments") + .param("movieId", invalidMovieId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } + + @Test + @DisplayName("GET /api/movies/aggregations/years - Should return movies by year with statistics") + void testGetMoviesByYearWithStats_Success() throws Exception { + // Arrange + MoviesByYearResult result1 = MoviesByYearResult.builder() + .year(2024) + .movieCount(10) + .averageRating(7.5) + .highestRating(9.0) + .lowestRating(6.0) + .totalVotes(5000L) + .build(); + + MoviesByYearResult result2 = MoviesByYearResult.builder() + .year(2023) + .movieCount(15) + .averageRating(7.8) + .highestRating(9.5) + .lowestRating(6.5) + .totalVotes(7500L) + .build(); + + when(movieService.getMoviesByYearWithStats()).thenReturn(Arrays.asList(result1, result2)); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/years")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(2))) + .andExpect(jsonPath("$.data[0].year").value(2024)) + .andExpect(jsonPath("$.data[0].movieCount").value(10)) + .andExpect(jsonPath("$.data[0].averageRating").value(7.5)) + .andExpect(jsonPath("$.data[1].year").value(2023)) + .andExpect(jsonPath("$.data[1].movieCount").value(15)); + } + + @Test + @DisplayName("GET /api/movies/aggregations/directors - Should return directors with most movies") + void testGetDirectorsWithMostMovies_Success() throws Exception { + // Arrange + DirectorStatisticsResult result1 = DirectorStatisticsResult.builder() + .director("Christopher Nolan") + .movieCount(10) + .averageRating(8.5) + .build(); + + DirectorStatisticsResult result2 = DirectorStatisticsResult.builder() + .director("Steven Spielberg") + .movieCount(25) + .averageRating(8.2) + .build(); + + when(movieService.getDirectorsWithMostMovies(anyInt())).thenReturn(Arrays.asList(result1, result2)); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/directors")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(2))) + .andExpect(jsonPath("$.data[0].director").value("Christopher Nolan")) + .andExpect(jsonPath("$.data[0].movieCount").value(10)) + .andExpect(jsonPath("$.data[0].averageRating").value(8.5)) + .andExpect(jsonPath("$.data[1].director").value("Steven Spielberg")) + .andExpect(jsonPath("$.data[1].movieCount").value(25)); + } + + @Test + @DisplayName("GET /api/movies/aggregations/directors - Should accept limit parameter") + void testGetDirectorsWithMostMovies_WithLimit() throws Exception { + // Arrange + when(movieService.getDirectorsWithMostMovies(eq(10))).thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/aggregations/directors") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java index 200368f..c734d7a 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java @@ -21,6 +21,11 @@ * - DELETE /api/movies * - DELETE /api/movies/{id}/find-and-delete * + * TODO: Implement integration tests for aggregation endpoints: + * - GET /api/movies/aggregations/comments (with and without limit/movieId) + * - GET /api/movies/aggregations/years + * - GET /api/movies/aggregations/directors (with and without limit) + * * TODO: Test error scenarios: * - Invalid ObjectId format * - Resource not found @@ -31,6 +36,7 @@ * - Large dataset queries * - Pagination performance * - Text search performance + * - Aggregation pipeline performance */ public class MovieIntegrationTest { @@ -38,5 +44,6 @@ public class MovieIntegrationTest { // TODO: Add @Testcontainers annotation // TODO: Add MongoDB container configuration // TODO: Add test data setup methods - // TODO: Add integration test methods + // TODO: Add integration test methods for CRUD operations + // TODO: Add integration test methods for aggregation endpoints } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java index ed57f3b..4af804c 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/service/MovieServiceTest.java @@ -12,10 +12,14 @@ import com.mongodb.samplemflix.model.dto.BatchInsertResponse; import com.mongodb.samplemflix.model.dto.CreateMovieRequest; import com.mongodb.samplemflix.model.dto.DeleteResponse; +import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; import com.mongodb.samplemflix.model.dto.MovieSearchQuery; +import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; +import com.mongodb.samplemflix.model.dto.MoviesByYearResult; import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.repository.MovieRepository; import java.util.*; +import org.bson.Document; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,6 +29,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.query.Query; /** @@ -422,4 +428,184 @@ void testFindAndDeleteMovie_NotFound() { assertThrows(ResourceNotFoundException.class, () -> movieService.findAndDeleteMovie(validId)); verify(mongoTemplate).findAndRemove(any(Query.class), eq(Movie.class)); } + + // ==================== AGGREGATION TESTS ==================== + + @Test + @DisplayName("Should get movies with most comments") + void testGetMoviesWithMostComments_Success() { + // Arrange + Integer limit = 10; + String movieId = null; + + Document doc1 = new Document() + .append("id", testId.toHexString()) + .append("title", "Test Movie") + .append("year", 2024) + .append("plot", "Test plot") + .append("poster", "http://example.com/poster.jpg") + .append("genres", Arrays.asList("Action", "Drama")) + .append("imdb", new Document("rating", 8.5).append("votes", 1000)) + .append("recentComments", Arrays.asList( + new Document() + .append("_id", new ObjectId()) + .append("name", "John Doe") + .append("email", "john@example.com") + .append("text", "Great movie!") + .append("date", new Date()) + )) + .append("totalComments", 5) + .append("mostRecentCommentDate", new Date()); + + @SuppressWarnings("unchecked") + AggregationResults mockResults = mock(AggregationResults.class); + when(mockResults.getMappedResults()).thenReturn(Arrays.asList(doc1)); + when(mongoTemplate.aggregate(any(Aggregation.class), eq("movies"), eq(Document.class))) + .thenReturn(mockResults); + + // Act + List results = movieService.getMoviesWithMostComments(limit, movieId); + + // Assert + assertNotNull(results); + assertEquals(1, results.size()); + assertEquals("Test Movie", results.get(0).getTitle()); + assertEquals(2024, results.get(0).getYear()); + assertEquals(5, results.get(0).getTotalComments()); + assertNotNull(results.get(0).getRecentComments()); + assertEquals(1, results.get(0).getRecentComments().size()); + verify(mongoTemplate).aggregate(any(Aggregation.class), eq("movies"), eq(Document.class)); + } + + @Test + @DisplayName("Should get movies with most comments filtered by movie ID") + void testGetMoviesWithMostComments_WithMovieId() { + // Arrange + Integer limit = 10; + String movieId = testId.toHexString(); + + @SuppressWarnings("unchecked") + AggregationResults mockResults = mock(AggregationResults.class); + when(mockResults.getMappedResults()).thenReturn(Arrays.asList()); + when(mongoTemplate.aggregate(any(Aggregation.class), eq("movies"), eq(Document.class))) + .thenReturn(mockResults); + + // Act + List results = movieService.getMoviesWithMostComments(limit, movieId); + + // Assert + assertNotNull(results); + verify(mongoTemplate).aggregate(any(Aggregation.class), eq("movies"), eq(Document.class)); + } + + @Test + @DisplayName("Should throw ValidationException for invalid movie ID in getMoviesWithMostComments") + void testGetMoviesWithMostComments_InvalidMovieId() { + // Arrange + Integer limit = 10; + String invalidMovieId = "invalid-id"; + + // Act & Assert + assertThrows(ValidationException.class, + () -> movieService.getMoviesWithMostComments(limit, invalidMovieId)); + verify(mongoTemplate, never()).aggregate(any(Aggregation.class), anyString(), any()); + } + + @Test + @DisplayName("Should get movies by year with statistics") + void testGetMoviesByYearWithStats_Success() { + // Arrange + MoviesByYearResult result1 = MoviesByYearResult.builder() + .year(2024) + .movieCount(10) + .averageRating(7.5) + .highestRating(9.0) + .lowestRating(6.0) + .totalVotes(5000L) + .build(); + + MoviesByYearResult result2 = MoviesByYearResult.builder() + .year(2023) + .movieCount(15) + .averageRating(7.8) + .highestRating(9.5) + .lowestRating(6.5) + .totalVotes(7500L) + .build(); + + @SuppressWarnings("unchecked") + AggregationResults mockResults = mock(AggregationResults.class); + when(mockResults.getMappedResults()).thenReturn(Arrays.asList(result1, result2)); + when(mongoTemplate.aggregate(any(Aggregation.class), eq("movies"), eq(MoviesByYearResult.class))) + .thenReturn(mockResults); + + // Act + List results = movieService.getMoviesByYearWithStats(); + + // Assert + assertNotNull(results); + assertEquals(2, results.size()); + assertEquals(2024, results.get(0).getYear()); + assertEquals(10, results.get(0).getMovieCount()); + assertEquals(7.5, results.get(0).getAverageRating()); + assertEquals(2023, results.get(1).getYear()); + verify(mongoTemplate).aggregate(any(Aggregation.class), eq("movies"), eq(MoviesByYearResult.class)); + } + + @Test + @DisplayName("Should get directors with most movies") + void testGetDirectorsWithMostMovies_Success() { + // Arrange + Integer limit = 20; + + DirectorStatisticsResult result1 = DirectorStatisticsResult.builder() + .director("Christopher Nolan") + .movieCount(10) + .averageRating(8.5) + .build(); + + DirectorStatisticsResult result2 = DirectorStatisticsResult.builder() + .director("Steven Spielberg") + .movieCount(25) + .averageRating(8.2) + .build(); + + @SuppressWarnings("unchecked") + AggregationResults mockResults = mock(AggregationResults.class); + when(mockResults.getMappedResults()).thenReturn(Arrays.asList(result1, result2)); + when(mongoTemplate.aggregate(any(Aggregation.class), eq("movies"), eq(DirectorStatisticsResult.class))) + .thenReturn(mockResults); + + // Act + List results = movieService.getDirectorsWithMostMovies(limit); + + // Assert + assertNotNull(results); + assertEquals(2, results.size()); + assertEquals("Christopher Nolan", results.get(0).getDirector()); + assertEquals(10, results.get(0).getMovieCount()); + assertEquals(8.5, results.get(0).getAverageRating()); + assertEquals("Steven Spielberg", results.get(1).getDirector()); + verify(mongoTemplate).aggregate(any(Aggregation.class), eq("movies"), eq(DirectorStatisticsResult.class)); + } + + @Test + @DisplayName("Should use default limit when null in getDirectorsWithMostMovies") + void testGetDirectorsWithMostMovies_DefaultLimit() { + // Arrange + Integer limit = null; + + @SuppressWarnings("unchecked") + AggregationResults mockResults = mock(AggregationResults.class); + when(mockResults.getMappedResults()).thenReturn(Arrays.asList()); + when(mongoTemplate.aggregate(any(Aggregation.class), eq("movies"), eq(DirectorStatisticsResult.class))) + .thenReturn(mockResults); + + // Act + List results = movieService.getDirectorsWithMostMovies(limit); + + // Assert + assertNotNull(results); + verify(mongoTemplate).aggregate(any(Aggregation.class), eq("movies"), eq(DirectorStatisticsResult.class)); + } } From caf7755756c9809493073960c132ecc454bfbb3c Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 30 Oct 2025 16:52:37 -0400 Subject: [PATCH 033/110] add new info --- server/python/main.py | 45 ++++++- server/python/src/database/mongo_client.py | 2 + server/python/src/routers/movies.py | 144 ++++++++++++++++++++- 3 files changed, 187 insertions(+), 4 deletions(-) diff --git a/server/python/main.py b/server/python/main.py index a782508..5af7052 100644 --- a/server/python/main.py +++ b/server/python/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from src.routers import movies from src.utils.errorHandler import register_error_handlers -from src.database.mongo_client import db +from src.database.mongo_client import db, get_collection import traceback app = FastAPI() @@ -45,6 +45,45 @@ async def ensure_search_index(): except Exception as e: print(f"Error creating the search index: {e}") - - +@app.on_event("startup") +async def vector_search_index(): + """ + Creates vector search index on application startup if it doesn't already exist. + This ensures the index is ready before any vector search requests are made. + """ + try: + + embedded_movies_collection = get_collection("embedded_movies") + + # Get list of existing indexes - convert AsyncCommandCursor to list + existing_indexes_cursor = await embedded_movies_collection.list_search_indexes() + existing_indexes = await existing_indexes_cursor.to_list(length=None) + index_names = [index.get("name") for index in existing_indexes] + + # Check if our vector_index already exists + if "vector_index" not in index_names: + + # Define the vector search index specification + index_definition = { + "name": "vector_index", + "type": "vectorSearch", + "definition": { + "fields": [ + { + "type": "vector", + "path": "plot_embedding_voyage_3_large", + "numDimensions": 2048, #Set this to 2048 to match the embedding dimensions on the path + "similarity": "cosine" + } + ] + } + } + + # Create the index + result = await embedded_movies_collection.create_search_index(index_definition) + print("Vector search index 'vector_index' ready to query.") + + except Exception as e: + print(f"Error during vector search index setup: {str(e)}") + print(f"Error type: {type(e).__name__}") diff --git a/server/python/src/database/mongo_client.py b/server/python/src/database/mongo_client.py index a608530..3f2f768 100644 --- a/server/python/src/database/mongo_client.py +++ b/server/python/src/database/mongo_client.py @@ -1,11 +1,13 @@ from pymongo import AsyncMongoClient from dotenv import load_dotenv import os +import voyageai load_dotenv() client = AsyncMongoClient(os.getenv("MONGO_URI")) db = client[os.getenv("MONGO_DB")] +voyageai.api_key = os.getenv("VOYAGE_API_KEY") def get_collection(name:str): return db[name] \ No newline at end of file diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 76579ee..e608013 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -7,6 +7,7 @@ from bson import ObjectId import re from bson.errors import InvalidId +import voyageai ''' This file contains all the business logic for movie operations. @@ -235,6 +236,107 @@ async def search_movies( movies.append(movie) return create_success_response(movies, f"Found {len(movies)} movies matching the search criteria.") + +#---------------------------------------------------------------------------------------------------------- +# MongoDB Vector Search +# +# MongoDB Vector Search based on searching the plot_embedding_voyage_3_large field. +#---------------------------------------------------------------------------------------------------------- +""" + + GET /api/movies/vector-search + + Search movies using MongoDB Vector Search to find movies with similar plots. + Uses embeddings generated by the Voyage AI model to perform semantic similarity search. + + Query Parameters: + q (str, required): Search query text to find movies with similar plots. + limit (int, optional): Number of results to return (default: 10, max: 50). + + Returns: + SuccessResponse[List[VectorSearchResult]]: A response object containing movies with similarity scores. + Each result includes: + - _id: Movie ObjectId + - title: Movie title + - 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 +model = "voyage-3-large" +outputDimension = 2048 #Set to 2048 to match the dimensions of the collection's embeddings +vo = voyageai.Client() + +# Vector Search Endpoint +@router.get("/vector-search", response_model=SuccessResponse[List[VectorSearchResult]]) +async def vector_search_movies( + q: str = Query(..., description="Search query to find similar movies by plot"), + limit: int = Query(default=10, ge=1, le=50, description="Number of results to return") +): + """ + 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 + """ + try: + # The vector search index was already created at startup time + # Generate embedding for the search query + query_embedding = get_embedding(q, input_type="query") + + # Get the embedded movies collection + embedded_movies_collection = get_collection("embedded_movies") + + # Define vector search pipeline + pipeline = [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "plot_embedding_voyage_3_large", + "queryVector": query_embedding, #2048 + "numCandidates": limit * 15, # Search more candidates for better results + "limit": limit + } + }, + { + "$project": { + "_id": 1, + "title": 1, + "plot": 1, + "score": { + "$meta": "vectorSearchScore" + } + } + } + ] + + 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"]: + try: + result["_id"] = str(result["_id"]) + except (InvalidId, TypeError): + # Handle invalid ObjectId conversion + result["_id"] = str(result["_id"]) if result["_id"] else None + + results = [VectorSearchResult(**doc) for doc in raw_results] + + return create_success_response( + results, + f"Found {len(results)} similar movies for query: '{q}'" + ) + + except Exception as e: + return create_error_response( + message="Vector search failed", + code="INTERNAL_SERVER_ERROR", + details=str(e) + ) """ GET /api/movies/aggregations/reportingByComments @@ -1199,5 +1301,45 @@ async def execute_aggregation(pipeline: list) -> list: return results -# ---- Place Vector Search Here ---- +""" + Helper function to execute aggregation pipeline and return results from a specified collection. + + Args: + collection: The MongoDB collection to run the aggregation on + pipeline: MongoDB aggregation pipeline stages + + 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""" + print(f"Executing pipeline: {pipeline}") # Debug logging + print(f"Collection: {collection.name if hasattr(collection, 'name') else 'embedded_movies'}") + + cursor = await collection.aggregate(pipeline) + results = await cursor.to_list(length=None) # Convert cursor to list + + print(f"Aggregation returned {len(results)} results") # Debug logging + if len(results) <= 3: # Log first few results for debugging + for i, doc in enumerate(results): + print(f"Result {i+1}: {doc}") + + return results + +""" + Helper function to generate vector embeddings from an input. + + Args: + data: Input data to generate embeddings for + input_type: Type of input data + + Returns: + Vector embeddings for the given input +""" +def get_embedding(data, input_type = "document"): + embeddings = vo.embed( + data, model = model, output_dimension = outputDimension, input_type = input_type + ).embeddings + return embeddings[0] From efd31b883744934964c2e86fb20b04e08f1fc79f Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 30 Oct 2025 17:00:59 -0400 Subject: [PATCH 034/110] test vs --- server/python/src/models/models.py | 10 ++++++++++ server/python/src/routers/movies.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/server/python/src/models/models.py b/server/python/src/models/models.py index c044ce9..ce94bca 100644 --- a/server/python/src/models/models.py +++ b/server/python/src/models/models.py @@ -111,6 +111,16 @@ class MovieFilter(BaseModel): runtime: Optional[int] = None poster: Optional[str] = None +class VectorSearchResult(BaseModel): + id: Optional[str] = Field(alias="_id") + title: str + plot: Optional[str] = None + score: float + + model_config = { + "populate_by_name": True + } + class SuccessResponse(BaseModel, Generic[T]): success: bool = True message: Optional[str] diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index e608013..817c933 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Query, Path from src.database.mongo_client import db, get_collection -from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse, UpdateMovieRequest +from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse, UpdateMovieRequest, VectorSearchResult from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response From 497605927f1895c48cb353a90222c47487fdce79 Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 30 Oct 2025 17:07:53 -0400 Subject: [PATCH 035/110] add new comments --- server/python/src/routers/movies.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 817c933..851de45 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -55,8 +55,15 @@ Search movies using MongoDB Search across the plot, fullplot, directors, writers, and cast fields. Supports compound search operators and fuzzy matching. +- GET /api/movies/vector-search : + Search movies using MongoDB Vector Search to enable semantic search capabilities over + the plot field. + Helper Functions: -- execute_aggregation(pipeline): Executes a MongoDB aggregation pipeline and returns the results. +- execute_aggregation(pipeline): Executes a MongoDB aggregation pipeline and returns the +results. +- execute_aggregation_on_collection(collection, pipeline): Executes a MongoDB aggregation pipeline on a specific collection and returns the results. +- get_embedding(data, input_type): Creates the vector embedding for a given input using the specified input type. ''' router = APIRouter() From 748af9f454fe0dc4413c5d3dc6e86b6b1fa6205d Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 30 Oct 2025 17:28:11 -0400 Subject: [PATCH 036/110] add error handling --- server/python/src/database/mongo_client.py | 13 +++++-- server/python/src/routers/movies.py | 40 +++++++++++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/server/python/src/database/mongo_client.py b/server/python/src/database/mongo_client.py index 3f2f768..da2d2b9 100644 --- a/server/python/src/database/mongo_client.py +++ b/server/python/src/database/mongo_client.py @@ -7,7 +7,16 @@ client = AsyncMongoClient(os.getenv("MONGO_URI")) db = client[os.getenv("MONGO_DB")] -voyageai.api_key = os.getenv("VOYAGE_API_KEY") + +# 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 def get_collection(name:str): - return db[name] \ No newline at end of file + return db[name] + +def voyage_ai_available(): + """Check if Voyage API Key is available and valid.""" + api_key = os.getenv("VOYAGE_API_KEY") + return api_key is not None and api_key.strip() != "" \ No newline at end of file diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 851de45..10981c8 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Query, Path -from src.database.mongo_client import db, get_collection +from src.database.mongo_client import db, get_collection, voyage_ai_available from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse, UpdateMovieRequest, VectorSearchResult from typing import List from datetime import datetime @@ -271,7 +271,6 @@ async def search_movies( # Specify your Voyage API key and embedding model model = "voyage-3-large" outputDimension = 2048 #Set to 2048 to match the dimensions of the collection's embeddings -vo = voyageai.Client() # Vector Search Endpoint @router.get("/vector-search", response_model=SuccessResponse[List[VectorSearchResult]]) @@ -289,10 +288,20 @@ async def vector_search_movies( Returns: SuccessResponse containing a list of movies with similarity scores """ + if not voyage_ai_available(): + return create_error_response( + message="Vector search unavailable", + 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") + query_embedding = get_embedding(q, input_type="query", client=vo) # Get the embedded movies collection embedded_movies_collection = get_collection("embedded_movies") @@ -1340,13 +1349,28 @@ async def execute_aggregation_on_collection(collection, pipeline: list) -> list: Args: data: Input data to generate embeddings for input_type: Type of input data + client: Voyage AI client instance Returns: Vector embeddings for the given input """ -def get_embedding(data, input_type = "document"): - embeddings = vo.embed( - data, model = model, output_dimension = outputDimension, input_type = input_type - ).embeddings - return embeddings[0] +def get_embedding(data, input_type = "document", client=None): + """ + Helper function to generate vector embeddings from an input. + + Args: + data: Input data to generate embeddings for + input_type: Type of input data + client: Voyage AI client instance + + 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 + return embeddings[0] From 238d734ec552bd29855811ff8cff7c0851fb2fdb Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 31 Oct 2025 12:26:31 -0400 Subject: [PATCH 037/110] tests(java): add tests for atlas search --- server/java-spring/pom.xml | 8 + .../controller/MovieControllerImpl.java | 68 +++- .../exception/GlobalExceptionHandler.java | 21 ++ .../samplemflix/service/MovieService.java | 23 ++ .../samplemflix/service/MovieServiceImpl.java | 150 ++++++++- .../controller/MovieControllerTest.java | 93 ++++++ .../AtlasSearchIntegrationTest.java | 316 ++++++++++++++++++ .../mongodb/samplemflix/integration/README.md | 134 ++++++++ .../resources/application-test.properties | 23 ++ 9 files changed, 831 insertions(+), 5 deletions(-) create mode 100644 server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java create mode 100644 server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md create mode 100644 server/java-spring/src/test/resources/application-test.properties diff --git a/server/java-spring/pom.xml b/server/java-spring/pom.xml index d6bc07b..4e2f946 100644 --- a/server/java-spring/pom.xml +++ b/server/java-spring/pom.xml @@ -66,6 +66,7 @@ commons-lang3 ${commons.lang3.version} + org.springdoc @@ -85,6 +86,12 @@ spring-boot-starter-test test + + + + com.fasterxml.jackson.core + jackson-databind + @@ -114,6 +121,7 @@ + net.revelc.code diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 130635b..2b5d42c 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -23,7 +23,7 @@ /** * REST controller for movie-related endpoints. - * + * *

This controller handles all HTTP requests for movie operations including: *

  * - GET /api/movies - Get all movies with filtering, sorting, and pagination
@@ -35,9 +35,11 @@
  * - DELETE /api/movies/{id} - Delete a movie
  * - DELETE /api/movies - Delete multiple movies
  * - DELETE /api/movies/{id}/find-and-delete - Find and delete a movie
- * - GET /api/movies/reportingByComments - Aggregate movies with most comments
- * - GET /api/movies/reportingByYear - Aggregate movies by year with statistics
- * - GET /api/movies/reportingByDirectors - Aggregate directors with most movies
+ * - GET /api/movies/aggregations/comments - Aggregate movies with most comments
+ * - GET /api/movies/aggregations/years - Aggregate movies by year with statistics
+ * - GET /api/movies/aggregations/directors - Aggregate directors with most movies
+ * - GET /api/movies/searchByPlot - Text search using Atlas Search Index based on plot
+ * - GET /api/movies/findSimilarMovies - Vector search to find similar movies based on plot embeddings
  * 
*/ @RestController @@ -345,4 +347,62 @@ public ResponseEntity>> getDirect return ResponseEntity.ok(response); } + + // Atlas Search endpoints + + /** + * GET /api/movies/searchByPlot + * + *

Searches movies by plot using MongoDB Atlas Search. + * Demonstrates text search using Atlas Search Index based on plot field. + * + * @param plot Text to search in the plot field (required) + * @param limit Maximum number of movies to return (default: 20, max: 100) + * @param skip Number of results to skip for pagination (default: 0) + * @return List of movies matching the search criteria + */ + @GetMapping("/searchByPlot") + public ResponseEntity>> searchMoviesByPlot( + @RequestParam String plot, + @RequestParam(defaultValue = "20") Integer limit, + @RequestParam(defaultValue = "0") Integer skip) { + + List movies = movieService.searchMoviesByPlot(plot, limit, skip); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message(String.format("Found %d movies matching the search criteria", movies.size())) + .data(movies) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } + + /** + * GET /api/movies/findSimilarMovies + * + *

Finds similar movies using vector search on plot embeddings. + * Demonstrates MongoDB Atlas Vector Search to find movies with similar plots. + * + * @param movieId ID of the movie to find similar movies for (required) + * @param limit Maximum number of similar movies to return (default: 10, max: 50) + * @return List of similar movies based on plot embeddings + */ + @GetMapping("/findSimilarMovies") + public ResponseEntity>> findSimilarMovies( + @RequestParam String movieId, + @RequestParam(defaultValue = "10") Integer limit) { + + List movies = movieService.findSimilarMovies(movieId, limit); + + SuccessResponse> response = SuccessResponse.>builder() + .success(true) + .message(String.format("Found %d similar movies", movies.size())) + .data(movies) + .timestamp(Instant.now().toString()) + .build(); + + return ResponseEntity.ok(response); + } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java index ef5f11c..aa6e544 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/exception/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; @@ -58,6 +59,26 @@ public ResponseEntity handleValidationException( return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, WebRequest request) { + logger.error("Missing request parameter: {}", ex.getMessage()); + + String message = String.format("Required parameter '%s' is missing", ex.getParameterName()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .success(false) + .message(message) + .error(ErrorResponse.ErrorDetails.builder() + .message(message) + .code("VALIDATION_ERROR") + .build()) + .timestamp(Instant.now().toString()) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + @ExceptionHandler(DatabaseOperationException.class) public ResponseEntity handleDatabaseOperationException( DatabaseOperationException ex, WebRequest request) { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 5f97db8..9d3ca30 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -61,4 +61,27 @@ public interface MovieService { * @return List of directors with their movie count and average rating */ List getDirectorsWithMostMovies(Integer limit); + + // Atlas Search endpoints + + /** + * Searches movies by plot using MongoDB Atlas Search. + * Demonstrates text search using Atlas Search Index. + * + * @param plotQuery Text to search in the plot field + * @param limit Maximum number of movies to return (default: 20, max: 100) + * @param skip Number of results to skip for pagination (default: 0) + * @return List of movies matching the search criteria + */ + List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip); + + /** + * Finds similar movies using vector search on plot embeddings. + * Demonstrates MongoDB Atlas Vector Search. + * + * @param movieId ID of the movie to find similar movies for + * @param limit Maximum number of similar movies to return (default: 10, max: 50) + * @return List of similar movies based on plot embeddings + */ + List findSimilarMovies(String movieId, Integer limit); } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 1955ccb..e37b38f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -557,12 +557,160 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { .build(); } + // Atlas Search methods + + @Override + public List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip) { + // Validate input + if (plotQuery == null || plotQuery.trim().isEmpty()) { + throw new ValidationException("Plot search query is required"); + } + + // Validate and set defaults for pagination + int resultLimit = Math.clamp(limit != null ? limit : 20, 1, 100); + int resultSkip = Math.max(skip != null ? skip : 0, 0); + + // Build the $search aggregation stage using Document + // Spring Data MongoDB doesn't have native support for $search, so we use Document + Document searchStage = new Document("$search", new Document() + .append("index", "movieSearchIndex") + .append("phrase", new Document() + .append("query", plotQuery.trim()) + .append("path", Movie.Fields.PLOT) + ) + ); + + Document skipStage = new Document("$skip", resultSkip); + Document limitStage = new Document("$limit", resultLimit); + + // Project only the fields needed in the response + Document projectStage = new Document("$project", new Document() + .append(Movie.Fields.ID, 1) + .append(Movie.Fields.TITLE, 1) + .append(Movie.Fields.YEAR, 1) + .append(Movie.Fields.PLOT, 1) + .append(Movie.Fields.FULLPLOT, 1) + .append(Movie.Fields.RELEASED, 1) + .append(Movie.Fields.RUNTIME, 1) + .append(Movie.Fields.POSTER, 1) + .append(Movie.Fields.GENRES, 1) + .append(Movie.Fields.DIRECTORS, 1) + .append(Movie.Fields.WRITERS, 1) + .append(Movie.Fields.CAST, 1) + .append(Movie.Fields.COUNTRIES, 1) + .append(Movie.Fields.LANGUAGES, 1) + .append(Movie.Fields.RATED, 1) + .append(Movie.Fields.AWARDS, 1) + .append(Movie.Fields.IMDB, 1) + ); + + // Execute the aggregation pipeline + // Use MongoTemplate's getCollection to run raw aggregation + // Spring Data MongoDB doesn't have native support for $search, so we use the MongoDB driver directly + try { + List aggregationPipeline = List.of(searchStage, skipStage, limitStage, projectStage); + + return mongoTemplate.getCollection("movies") + .aggregate(aggregationPipeline) + .map(doc -> mongoTemplate.getConverter().read(Movie.class, doc)) + .into(new java.util.ArrayList<>()); + } catch (Exception e) { + throw new DatabaseOperationException("Error performing Atlas Search: " + e.getMessage()); + } + } + + @Override + public List findSimilarMovies(String movieId, Integer limit) { + // Validate movie ID + if (movieId == null || movieId.trim().isEmpty()) { + throw new ValidationException("Movie ID is required"); + } + + if (!ObjectId.isValid(movieId)) { + throw new ValidationException("Invalid movie ID format"); + } + + // Validate and set default limit + int resultLimit = Math.clamp(limit != null ? limit : 10, 1, 50); + + // First, get the movie to retrieve its plot_embedding + ObjectId objectId = new ObjectId(movieId); + Document movie = mongoTemplate.getCollection("movies") + .find(new Document(Movie.Fields.ID, objectId)) + .first(); + + if (movie == null) { + throw new ResourceNotFoundException("Movie not found"); + } + + // Check if plot_embedding exists + if (!movie.containsKey("plot_embedding")) { + throw new ValidationException("Movie does not have plot embeddings for vector search"); + } + + @SuppressWarnings("unchecked") + List plotEmbedding = (List) movie.get("plot_embedding"); + + // Build the $vectorSearch aggregation stage + // Note: This requires MongoDB Atlas with a vector search index configured + Document vectorSearchStage = new Document("$vectorSearch", new Document() + .append("index", "plotEmbeddingIndex") + .append("path", "plot_embedding") + .append("queryVector", plotEmbedding) + .append("numCandidates", resultLimit * 10) + .append("limit", resultLimit + 1) // +1 to exclude the source movie + ); + + // Filter out the source movie + Document matchStage = new Document("$match", + new Document(Movie.Fields.ID, new Document("$ne", objectId)) + ); + + // Limit to final result count + Document limitStage = new Document("$limit", resultLimit); + + // Project only the fields needed in the response + Document projectStage = new Document("$project", new Document() + .append(Movie.Fields.ID, 1) + .append(Movie.Fields.TITLE, 1) + .append(Movie.Fields.YEAR, 1) + .append(Movie.Fields.PLOT, 1) + .append(Movie.Fields.FULLPLOT, 1) + .append(Movie.Fields.RELEASED, 1) + .append(Movie.Fields.RUNTIME, 1) + .append(Movie.Fields.POSTER, 1) + .append(Movie.Fields.GENRES, 1) + .append(Movie.Fields.DIRECTORS, 1) + .append(Movie.Fields.WRITERS, 1) + .append(Movie.Fields.CAST, 1) + .append(Movie.Fields.COUNTRIES, 1) + .append(Movie.Fields.LANGUAGES, 1) + .append(Movie.Fields.RATED, 1) + .append(Movie.Fields.AWARDS, 1) + .append(Movie.Fields.IMDB, 1) + .append("score", new Document("$meta", "vectorSearchScore")) + ); + + // Execute the aggregation pipeline + try { + List aggregationPipeline = List.of( + vectorSearchStage, matchStage, limitStage, projectStage + ); + + return mongoTemplate.getCollection("movies") + .aggregate(aggregationPipeline) + .map(doc -> mongoTemplate.getConverter().read(Movie.class, doc)) + .into(new java.util.ArrayList<>()); + } catch (Exception e) { + throw new DatabaseOperationException("Error performing vector search: " + e.getMessage()); + } + } + // TODO: Add advanced query methods // - getMoviesByGenreStatistics() - Aggregation pipeline for genre statistics // - getTopRatedMovies(int limit) - Movies sorted by rating // - getMoviesByDecade(int decade) - Movies from a specific decade // - getDirectorFilmography(String director) - All movies by a director // - getActorFilmography(String actor) - All movies featuring an actor - // - searchSimilarMovies(String movieId) - Vector search on plot_embedding field // - getMovieRecommendations(String userId) - Personalized recommendations } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index 2ac237f..ae3c3cd 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -502,4 +502,97 @@ void testGetDirectorsWithMostMovies_WithLimit() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data").isArray()); } + + // ==================== ATLAS SEARCH ENDPOINT TESTS ==================== + + @Test + @DisplayName("GET /api/movies/searchByPlot - Should search movies by plot successfully") + void testSearchMoviesByPlot_Success() throws Exception { + // Arrange + Movie movie1 = Movie.builder() + .id(new ObjectId()) + .title("Space Adventure") + .year(2024) + .plot("An epic space adventure across the galaxy") + .genres(Arrays.asList("Sci-Fi", "Adventure")) + .build(); + + Movie movie2 = Movie.builder() + .id(new ObjectId()) + .title("Space Quest") + .year(2023) + .plot("A thrilling space adventure to save humanity") + .genres(Arrays.asList("Sci-Fi", "Action")) + .build(); + + when(movieService.searchMoviesByPlot(eq("space adventure"), eq(20), eq(0))) + .thenReturn(Arrays.asList(movie1, movie2)); + + // Act & Assert + mockMvc.perform(get("/api/movies/searchByPlot") + .param("plot", "space adventure")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(2))) + .andExpect(jsonPath("$.data[0].title").value("Space Adventure")) + .andExpect(jsonPath("$.data[0].plot").value(containsString("space adventure"))) + .andExpect(jsonPath("$.data[1].title").value("Space Quest")); + } + + @Test + @DisplayName("GET /api/movies/searchByPlot - Should accept limit and skip parameters") + void testSearchMoviesByPlot_WithPagination() throws Exception { + // Arrange + when(movieService.searchMoviesByPlot(eq("adventure"), eq(10), eq(5))) + .thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/searchByPlot") + .param("plot", "adventure") + .param("limit", "10") + .param("skip", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } + + @Test + @DisplayName("GET /api/movies/searchByPlot - Should return 400 when plot parameter is missing") + void testSearchMoviesByPlot_MissingPlotParameter() throws Exception { + // Act & Assert + mockMvc.perform(get("/api/movies/searchByPlot")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /api/movies/searchByPlot - Should return 400 for validation error") + void testSearchMoviesByPlot_ValidationError() throws Exception { + // Arrange + when(movieService.searchMoviesByPlot(anyString(), anyInt(), anyInt())) + .thenThrow(new ValidationException("Plot query cannot be empty")); + + // Act & Assert + mockMvc.perform(get("/api/movies/searchByPlot") + .param("plot", "")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } + + @Test + @DisplayName("GET /api/movies/searchByPlot - Should return empty list when no matches found") + void testSearchMoviesByPlot_NoResults() throws Exception { + // Arrange + when(movieService.searchMoviesByPlot(eq("nonexistent"), eq(20), eq(0))) + .thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/searchByPlot") + .param("plot", "nonexistent")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(0))); + } } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java new file mode 100644 index 0000000..aea2155 --- /dev/null +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java @@ -0,0 +1,316 @@ +package com.mongodb.samplemflix.integration; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.mongodb.client.MongoCollection; +import com.mongodb.samplemflix.model.Movie; +import com.mongodb.samplemflix.service.MovieService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.bson.Document; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.ActiveProfiles; + +/** + * Integration tests for Atlas Search functionality. + * + *

These tests verify the Atlas Search endpoints work correctly with a real MongoDB Atlas instance. + * The tests require: + *

    + *
  • A MongoDB Atlas cluster (not local MongoDB)
  • + *
  • MONGODB_URI environment variable pointing to Atlas
  • + *
  • Atlas Search index creation and polling for readiness
  • + *
+ * + *

Note: These tests are disabled by default and should only be run against a test Atlas cluster. + * To enable, set the environment variable ENABLE_ATLAS_SEARCH_TESTS=true + */ +@SpringBootTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ActiveProfiles("test") +@DisplayName("Atlas Search Integration Tests") +class AtlasSearchIntegrationTest { + + @Autowired + private MovieService movieService; + + @Autowired + private MongoTemplate mongoTemplate; + + private static final String TEST_COLLECTION = "movies_search_test"; + private static final String SEARCH_INDEX_NAME = "movieSearchIndex"; + private static final int MAX_INDEX_WAIT_SECONDS = 120; + private static final int POLL_INTERVAL_SECONDS = 5; + + private List testMovieIds = new ArrayList<>(); + + @BeforeAll + void setUp() throws Exception { + // Skip tests if not running against Atlas + if (!isAtlasSearchEnabled()) { + System.out.println("Skipping Atlas Search tests - ENABLE_ATLAS_SEARCH_TESTS not set"); + return; + } + + System.out.println("Setting up Atlas Search integration tests..."); + + // Create test data + createTestMovies(); + + // Create Atlas Search index + createSearchIndex(); + + // Wait for index to be ready + waitForSearchIndexReady(); + + // Wait a bit for the newly created documents to be indexed + // Atlas Search indexes documents asynchronously + System.out.println("Waiting for test documents to be indexed..."); + try { + Thread.sleep(5000); // Wait 5 seconds for indexing + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + System.out.println("Atlas Search index is ready for testing"); + } + + @AfterAll + void tearDown() { + if (!isAtlasSearchEnabled()) { + return; + } + + System.out.println("Cleaning up Atlas Search test data..."); + + // Clean up test movies + if (!testMovieIds.isEmpty()) { + MongoCollection collection = mongoTemplate.getCollection("movies"); + testMovieIds.forEach(id -> { + collection.deleteOne(new Document("_id", new org.bson.types.ObjectId(id))); + }); + } + + // Note: We don't delete the search index as it may be used by other tests + // and takes time to recreate + } + + @Test + @DisplayName("Should search movies by plot using Atlas Search") + void testSearchMoviesByPlot_Success() { + if (!isAtlasSearchEnabled()) { + System.out.println("Skipping test - Atlas Search not enabled"); + return; + } + + // Act + List results = movieService.searchMoviesByPlot("space adventure", 10, 0); + + // Assert + assertNotNull(results, "Search results should not be null"); + assertFalse(results.isEmpty(), "Should find at least one movie with 'space adventure' in plot"); + + // Verify the results contain our test movie + boolean foundTestMovie = results.stream() + .anyMatch(movie -> movie.getPlot() != null && + movie.getPlot().contains("space adventure")); + assertTrue(foundTestMovie, "Results should contain movie with 'space adventure' in plot"); + } + + @Test + @DisplayName("Should return empty list when no movies match search query") + void testSearchMoviesByPlot_NoResults() { + if (!isAtlasSearchEnabled()) { + System.out.println("Skipping test - Atlas Search not enabled"); + return; + } + + // Act - search for something that definitely doesn't exist + List results = movieService.searchMoviesByPlot("xyzabc123nonexistent", 10, 0); + + // Assert + assertNotNull(results, "Search results should not be null"); + assertTrue(results.isEmpty(), "Should return empty list when no matches found"); + } + + @Test + @DisplayName("Should respect limit parameter in search") + void testSearchMoviesByPlot_WithLimit() { + if (!isAtlasSearchEnabled()) { + System.out.println("Skipping test - Atlas Search not enabled"); + return; + } + + // Act + List results = movieService.searchMoviesByPlot("adventure", 2, 0); + + // Assert + assertNotNull(results, "Search results should not be null"); + assertTrue(results.size() <= 2, "Should respect limit parameter"); + } + + @Test + @DisplayName("Should support pagination with skip parameter") + void testSearchMoviesByPlot_WithPagination() { + if (!isAtlasSearchEnabled()) { + System.out.println("Skipping test - Atlas Search not enabled"); + return; + } + + // Act - Get first page + List firstPage = movieService.searchMoviesByPlot("adventure", 2, 0); + + // Act - Get second page + List secondPage = movieService.searchMoviesByPlot("adventure", 2, 2); + + // Assert + assertNotNull(firstPage, "First page should not be null"); + assertNotNull(secondPage, "Second page should not be null"); + + // If we have enough results, verify pagination works + if (firstPage.size() == 2 && !secondPage.isEmpty()) { + // Verify different results on different pages + String firstPageFirstId = firstPage.get(0).getId().toHexString(); + boolean isDifferent = secondPage.stream() + .noneMatch(movie -> movie.getId().toHexString().equals(firstPageFirstId)); + assertTrue(isDifferent, "Second page should contain different results"); + } + } + + // ==================== HELPER METHODS ==================== + + private boolean isAtlasSearchEnabled() { + String enabled = System.getenv("ENABLE_ATLAS_SEARCH_TESTS"); + return "true".equalsIgnoreCase(enabled); + } + + private void createTestMovies() { + System.out.println("Creating test movies..."); + + MongoCollection collection = mongoTemplate.getCollection("movies"); + + List testMovies = Arrays.asList( + new Document() + .append("title", "Test Space Adventure") + .append("year", 2024) + .append("plot", "An epic space adventure across the galaxy") + .append("genres", Arrays.asList("Sci-Fi", "Adventure")), + new Document() + .append("title", "Test Mystery Movie") + .append("year", 2024) + .append("plot", "A detective solves a mysterious crime") + .append("genres", Arrays.asList("Mystery", "Thriller")), + new Document() + .append("title", "Test Adventure Quest") + .append("year", 2024) + .append("plot", "Heroes embark on a dangerous adventure") + .append("genres", Arrays.asList("Adventure", "Fantasy")) + ); + + testMovies.forEach(movie -> { + collection.insertOne(movie); + testMovieIds.add(movie.getObjectId("_id").toHexString()); + }); + + System.out.println("Created " + testMovieIds.size() + " test movies"); + } + + private void createSearchIndex() throws Exception { + System.out.println("Creating Atlas Search index..."); + + MongoCollection collection = mongoTemplate.getCollection("movies"); + + // Check if index already exists + List existingIndexes = new ArrayList<>(); + collection.listSearchIndexes().into(existingIndexes); + + boolean indexExists = existingIndexes.stream() + .anyMatch(idx -> SEARCH_INDEX_NAME.equals(idx.getString("name"))); + + if (indexExists) { + System.out.println("Search index already exists"); + return; + } + + // Create the search index definition + Document indexDefinition = new Document("mappings", new Document() + .append("dynamic", false) + .append("fields", new Document() + .append("plot", new Document() + .append("type", "string") + .append("analyzer", "lucene.standard")) + .append("fullplot", new Document() + .append("type", "string") + .append("analyzer", "lucene.standard")) + .append("directors", new Document() + .append("type", "string") + .append("analyzer", "lucene.standard")) + .append("writers", new Document() + .append("type", "string") + .append("analyzer", "lucene.standard")) + .append("cast", new Document() + .append("type", "string") + .append("analyzer", "lucene.standard")) + ) + ); + + // Create the index using the createSearchIndexes command + Document createIndexCommand = new Document("createSearchIndexes", "movies") + .append("indexes", Arrays.asList( + new Document("name", SEARCH_INDEX_NAME) + .append("definition", indexDefinition) + )); + + try { + mongoTemplate.getDb().runCommand(createIndexCommand); + System.out.println("Search index creation initiated"); + } catch (Exception e) { + System.err.println("Error creating search index: " + e.getMessage()); + throw e; + } + } + + private void waitForSearchIndexReady() throws Exception { + System.out.println("Waiting for search index to be ready..."); + + MongoCollection collection = mongoTemplate.getCollection("movies"); + long startTime = System.currentTimeMillis(); + long maxWaitMillis = MAX_INDEX_WAIT_SECONDS * 1000L; + + while (System.currentTimeMillis() - startTime < maxWaitMillis) { + List indexes = new ArrayList<>(); + collection.listSearchIndexes().into(indexes); + + Document searchIndex = indexes.stream() + .filter(idx -> SEARCH_INDEX_NAME.equals(idx.getString("name"))) + .findFirst() + .orElse(null); + + if (searchIndex != null) { + String status = searchIndex.getString("status"); + System.out.println("Index status: " + status); + + if ("READY".equals(status)) { + System.out.println("Search index is ready!"); + return; + } + } + + // Wait before polling again + Thread.sleep(POLL_INTERVAL_SECONDS * 1000L); + } + + throw new RuntimeException("Search index did not become ready within " + + MAX_INDEX_WAIT_SECONDS + " seconds"); + } +} diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md new file mode 100644 index 0000000..333f67a --- /dev/null +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md @@ -0,0 +1,134 @@ +# Atlas Search Integration Tests + +This directory contains integration tests for MongoDB Atlas Search functionality. + +## Overview + +The `AtlasSearchIntegrationTest` class tests the Atlas Search endpoints with a real MongoDB Atlas instance. These tests verify that: + +1. The Atlas Search index is created correctly +2. The index becomes ready for use (using polling) +3. The `searchByPlot` endpoint returns correct results +4. Pagination works correctly +5. Empty results are handled properly + +## Requirements + +These tests require: + +- **MongoDB Atlas cluster** (not local MongoDB or Testcontainers) +- **Atlas Search capability** enabled on the cluster +- **MONGODB_URI** environment variable pointing to your Atlas cluster +- **ENABLE_ATLAS_SEARCH_TESTS=true** environment variable to enable the tests + +## Running the Tests + +### Enable the Tests + +By default, these tests are **disabled** to prevent accidental runs against production databases. To enable them: + +```bash +export ENABLE_ATLAS_SEARCH_TESTS=true +``` + +### Set MongoDB URI + +Make sure your `MONGODB_URI` environment variable points to a MongoDB Atlas cluster: + +```bash +export MONGODB_URI="mongodb+srv://username:password@cluster.mongodb.net/sample_mflix?retryWrites=true&w=majority" +``` + +Or use a `.env` file in the `server/java-spring` directory: + +``` +MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/sample_mflix?retryWrites=true&w=majority +``` + +### Run the Tests + +```bash +# Run all integration tests +./mvnw test -Dtest=AtlasSearchIntegrationTest + +# Run a specific test +./mvnw test -Dtest=AtlasSearchIntegrationTest#testSearchMoviesByPlot_Success +``` + +## How the Tests Work + +### 1. Index Creation and Polling + +The tests use a `@BeforeAll` method to: + +1. Check if the `movieSearchIndex` already exists +2. If not, create it with the proper field mappings +3. Poll the index status every 5 seconds until it's "READY" +4. Wait up to 120 seconds (2 minutes) for the index to be ready +5. Throw an exception if the index doesn't become ready in time + +### 2. Test Data Setup + +The tests create temporary test movies with known plot content: + +- "An epic space adventure across the galaxy" +- "A detective solves a mysterious crime" +- "Heroes embark on a dangerous adventure" + +These movies are used to verify search functionality. + +### 3. Test Cleanup + +The `@AfterAll` method removes the test movies after all tests complete. The search index is **not** deleted because: + +- It may be used by other tests +- It takes time to recreate +- It's safe to leave in the database + +## Test Cases + +### testSearchMoviesByPlot_Success +Verifies that searching for "space adventure" returns movies with that phrase in the plot. + +### testSearchMoviesByPlot_NoResults +Verifies that searching for a non-existent phrase returns an empty list. + +### testSearchMoviesByPlot_WithLimit +Verifies that the `limit` parameter correctly limits the number of results. + +### testSearchMoviesByPlot_WithPagination +Verifies that the `skip` parameter works for pagination and returns different results on different pages. + +## Troubleshooting + +### Tests are Skipped + +If you see "Skipping Atlas Search tests - ENABLE_ATLAS_SEARCH_TESTS not set", make sure you've set the environment variable: + +```bash +export ENABLE_ATLAS_SEARCH_TESTS=true +``` + +### Index Creation Timeout + +If the tests fail with "Search index did not become ready within 120 seconds": + +1. Check that your cluster has Atlas Search enabled +2. Verify you're using a MongoDB Atlas cluster (not local MongoDB) +3. Check the Atlas UI to see if the index is being created +4. Increase `MAX_INDEX_WAIT_SECONDS` if needed + +### Connection Errors + +If you get connection errors: + +1. Verify your `MONGODB_URI` is correct +2. Check that your IP address is whitelisted in Atlas +3. Verify your database user credentials are correct + +## Notes + +- These tests use `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` to allow `@BeforeAll` and `@AfterAll` methods to be non-static +- The tests use `@ActiveProfiles("test")` to load test-specific configuration from `application-test.properties` +- The search index is shared across all tests in the class +- Test movies are created once and cleaned up after all tests complete diff --git a/server/java-spring/src/test/resources/application-test.properties b/server/java-spring/src/test/resources/application-test.properties new file mode 100644 index 0000000..1b2669c --- /dev/null +++ b/server/java-spring/src/test/resources/application-test.properties @@ -0,0 +1,23 @@ +# Test Configuration for MongoDB Atlas Search Integration Tests +# This configuration is used when running integration tests with @ActiveProfiles("test") + +# MongoDB Configuration +# For Atlas Search tests, use the same MONGODB_URI as the main application +# This should point to a MongoDB Atlas cluster (not local MongoDB) +spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.database=sample_mflix + +# Server Configuration +# Use a different port for tests to avoid conflicts +server.port=0 + +# Logging Configuration +# More verbose logging for tests +logging.level.com.mongodb.samplemflix=DEBUG +logging.level.org.mongodb.driver=INFO +logging.level.org.springframework.data.mongodb=DEBUG + +# Test-specific settings +# Disable some startup checks that may not be needed in tests +spring.main.lazy-initialization=false + From d6672d3c828d2935644b7bbc19d1491c3407410a Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 31 Oct 2025 12:52:56 -0400 Subject: [PATCH 038/110] update search to cover all query parameters --- server/java-spring/README.md | 72 +++++++++-- .../controller/MovieControllerImpl.java | 66 ++++++++--- .../model/dto/MovieSearchRequest.java | 89 ++++++++++++++ .../model/dto/UpdateMovieRequest.java | 2 +- .../samplemflix/service/MovieService.java | 20 ++++ .../samplemflix/service/MovieServiceImpl.java | 112 +++++++++++++++--- .../controller/MovieControllerTest.java | 103 +++++++++++++--- .../AtlasSearchIntegrationTest.java | 3 +- .../integration/MovieIntegrationTest.java | 2 +- .../mongodb/samplemflix/integration/README.md | 2 +- 10 files changed, 410 insertions(+), 61 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchRequest.java diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 90b020f..3bd4345 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -8,11 +8,11 @@ This application provides a REST API for managing movie data from MongoDB's samp - Spring Data MongoDB for simplified data access - CRUD operations (Create, Read, Update, Delete) -- Text search functionality +- MongoDB Atlas Search with multi-field search and compound operators - Filtering, sorting, and pagination - Comprehensive error handling - API documentation with Swagger/OpenAPI -- MongoTemplate for complex queries +- MongoTemplate for complex queries and aggregation pipelines ## Prerequisites @@ -104,16 +104,59 @@ Once the application is running, you can access: ### Movies (✅ Implemented) +#### CRUD Operations - `GET /api/movies` - Get all movies (with filtering, sorting, pagination) - `GET /api/movies/{id}` - Get a single movie by ID - `POST /api/movies` - Create a new movie - `POST /api/movies/batch` - Create multiple movies -- `PUT /api/movies/{id}` - Update a movie +- `PATCH /api/movies/{id}` - Update a movie (partial update) - `PATCH /api/movies` - Update multiple movies - `DELETE /api/movies/{id}` - Delete a movie - `DELETE /api/movies` - Delete multiple movies - `DELETE /api/movies/{id}/find-and-delete` - Find and delete a movie +#### Aggregations +- `GET /api/movies/aggregations/comments` - Get movies with most comments +- `GET /api/movies/aggregations/years` - Aggregate movies by year with statistics +- `GET /api/movies/aggregations/directors` - Aggregate directors with most movies + +#### Atlas Search +- `GET /api/movies/search` - Search movies using MongoDB Atlas Search + + **Query Parameters:** + - `plot` (optional) - Search in plot field using phrase matching + - `fullplot` (optional) - Search in fullplot field using phrase matching + - `directors` (optional) - Search in directors field with fuzzy matching + - `writers` (optional) - Search in writers field with fuzzy matching + - `cast` (optional) - Search in cast field with fuzzy matching + - `searchOperator` (optional) - Compound operator: `must` (default), `should`, `mustNot`, `filter` + - `limit` (optional) - Maximum results to return (default: 20, max: 100) + - `skip` (optional) - Number of results to skip for pagination (default: 0) + + **Examples:** + ```bash + # Search by plot + GET /api/movies/search?plot=space+adventure + + # Search by multiple fields with AND logic + GET /api/movies/search?directors=Coppola&cast=Pacino&searchOperator=must + + # Search by multiple fields with OR logic + GET /api/movies/search?plot=crime&directors=Scorsese&searchOperator=should + + # Search with pagination + GET /api/movies/search?cast=Tom+Hanks&limit=10&skip=20 + ``` + + **Note:** At least one search field must be provided. The `searchOperator` determines how multiple search criteria are combined: + - `must` - All criteria must match (AND logic) + - `should` - At least one criterion should match (OR logic) + - `mustNot` - Criteria must not match (NOT logic) + - `filter` - Criteria must match but don't affect scoring + +#### Vector Search +- `GET /api/movies/findSimilarMovies` - Find similar movies using vector search on plot embeddings + ## Development ### Running Tests @@ -139,11 +182,19 @@ java -jar target/sample-mflix-spring-1.0.0.jar - **Movies CRUD API** - Full create, read, update, delete operations - **Spring Data MongoDB** - Repository pattern with MongoTemplate for complex queries -- **Text Search** - Full-text search on movie titles, plots, and genres +- **MongoDB Atlas Search** - Multi-field search with compound operators (must, should, mustNot, filter) + - Phrase matching on plot and fullplot fields + - Fuzzy text matching on directors, writers, and cast fields + - Support for complex search queries with multiple criteria +- **MongoDB Aggregations** - Statistical aggregations by year, directors, and comments +- **Vector Search** - Find similar movies using plot embeddings (requires Atlas Vector Search) - **Filtering & Pagination** - Query parameters for filtering, sorting, and pagination - **Custom Exception Handling** - Global exception handler with proper HTTP status codes - **Type-Safe DTOs** - Specific response types instead of generic Maps -- **Unit Tests** - 35 tests covering service and controller layers +- **Comprehensive Testing** - 60 tests covering service, controller, and integration layers + - 29 controller unit tests + - 27 service unit tests + - 4 Atlas Search integration tests (requires Atlas cluster) - **OpenAPI Documentation** - Swagger UI available at `/swagger-ui.html` - **Database Verification** - Startup checks for database connectivity and indexes @@ -165,8 +216,15 @@ This application is designed as an educational sample to demonstrate: 2. Best practices for Spring Boot REST API development 3. Proper separation of concerns (Controller → Service → Repository) 4. MongoDB CRUD operations and query patterns -5. Error handling and validation in Spring Boot -6. Using MongoTemplate for complex queries alongside Spring Data repositories +5. **MongoDB Atlas Search** - Multi-field text search with compound operators + - Phrase matching for exact phrase searches + - Fuzzy text matching for typo-tolerant searches + - Compound operators (must, should, mustNot, filter) for complex queries +6. **MongoDB Aggregation Pipelines** - Statistical aggregations and data transformations +7. **Vector Search** - Semantic similarity search using embeddings +8. Error handling and validation in Spring Boot +9. Using MongoTemplate for complex queries alongside Spring Data repositories +10. Comprehensive testing strategies (unit tests, integration tests) ## Troubleshooting diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 2b5d42c..736e096 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -30,7 +30,7 @@ * - GET /api/movies/{id} - Get a single movie by ID * - POST /api/movies - Create a new movie * - POST /api/movies/batch - Create multiple movies - * - PUT /api/movies/{id} - Update a movie + * - PATCH /api/movies/{id} - Update a movie * - PATCH /api/movies - Update multiple movies * - DELETE /api/movies/{id} - Delete a movie * - DELETE /api/movies - Delete multiple movies @@ -38,7 +38,7 @@ * - GET /api/movies/aggregations/comments - Aggregate movies with most comments * - GET /api/movies/aggregations/years - Aggregate movies by year with statistics * - GET /api/movies/aggregations/directors - Aggregate directors with most movies - * - GET /api/movies/searchByPlot - Text search using Atlas Search Index based on plot + * - GET /api/movies/search - Text search using Atlas Search Index across multiple fields (plot, fullplot, directors, writers, cast) * - GET /api/movies/findSimilarMovies - Vector search to find similar movies based on plot embeddings *

*/ @@ -152,23 +152,24 @@ public ResponseEntity> createMoviesBatch( } /** - * PUT /api/movies/{id} + * PATCH /api/movies/{id} * - *

Updates a single movie document. + *

Updates a single movie document with partial updates. + * Only the fields provided in the request will be updated. */ - @PutMapping("/{id}") + @PatchMapping("/{id}") public ResponseEntity> updateMovie( @PathVariable String id, @RequestBody UpdateMovieRequest request) { Movie movie = movieService.updateMovie(id, request); - + SuccessResponse response = SuccessResponse.builder() .success(true) .message("Movie updated successfully") .data(movie) .timestamp(Instant.now().toString()) .build(); - + return ResponseEntity.ok(response); } @@ -351,23 +352,56 @@ public ResponseEntity>> getDirect // Atlas Search endpoints /** - * GET /api/movies/searchByPlot + * GET /api/movies/search + * + *

Searches movies using MongoDB Atlas Search across multiple fields. + * Demonstrates text search using Atlas Search Index with compound operators. * - *

Searches movies by plot using MongoDB Atlas Search. - * Demonstrates text search using Atlas Search Index based on plot field. + *

Supports searching across: + *

    + *
  • plot - using phrase operator for exact phrase matching
  • + *
  • fullplot - using phrase operator for exact phrase matching
  • + *
  • directors - using text operator with fuzzy matching
  • + *
  • writers - using text operator with fuzzy matching
  • + *
  • cast - using text operator with fuzzy matching
  • + *
* - * @param plot Text to search in the plot field (required) + *

At least one search field must be provided. + * + * @param plot Text to search in the plot field (optional) + * @param fullplot Text to search in the fullplot field (optional) + * @param directors Text to search in the directors field (optional) + * @param writers Text to search in the writers field (optional) + * @param cast Text to search in the cast field (optional) * @param limit Maximum number of movies to return (default: 20, max: 100) * @param skip Number of results to skip for pagination (default: 0) + * @param searchOperator Compound operator: must, should, mustNot, filter (default: must) * @return List of movies matching the search criteria */ - @GetMapping("/searchByPlot") - public ResponseEntity>> searchMoviesByPlot( - @RequestParam String plot, + @GetMapping("/search") + public ResponseEntity>> searchMovies( + @RequestParam(required = false) String plot, + @RequestParam(required = false) String fullplot, + @RequestParam(required = false) String directors, + @RequestParam(required = false) String writers, + @RequestParam(required = false) String cast, @RequestParam(defaultValue = "20") Integer limit, - @RequestParam(defaultValue = "0") Integer skip) { + @RequestParam(defaultValue = "0") Integer skip, + @RequestParam(defaultValue = "must") String searchOperator) { + + com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot(plot) + .fullplot(fullplot) + .directors(directors) + .writers(writers) + .cast(cast) + .limit(limit) + .skip(skip) + .searchOperator(searchOperator) + .build(); - List movies = movieService.searchMoviesByPlot(plot, limit, skip); + List movies = movieService.searchMovies(searchRequest); SuccessResponse> response = SuccessResponse.>builder() .success(true) diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchRequest.java new file mode 100644 index 0000000..4d4c56f --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieSearchRequest.java @@ -0,0 +1,89 @@ +package com.mongodb.samplemflix.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Data Transfer Object for Atlas Search query parameters. + * + *

This DTO is used to parse and validate query parameters for GET /api/movies/search requests. + * It supports searching across multiple fields using MongoDB Atlas Search with compound operators. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MovieSearchRequest { + + /** + * Search query for the plot field. + * Uses phrase operator for exact phrase matching. + */ + private String plot; + + /** + * Search query for the fullplot field. + * Uses phrase operator for exact phrase matching. + */ + private String fullplot; + + /** + * Search query for the directors field. + * Uses text operator with fuzzy matching (maxEdits=1, prefixLength=5). + */ + private String directors; + + /** + * Search query for the writers field. + * Uses text operator with fuzzy matching (maxEdits=1, prefixLength=5). + */ + private String writers; + + /** + * Search query for the cast field. + * Uses text operator with fuzzy matching (maxEdits=1, prefixLength=5). + */ + private String cast; + + /** + * Maximum number of results to return. + * Default: 20, Range: 1-100 + */ + private Integer limit; + + /** + * Number of results to skip for pagination. + * Default: 0, Minimum: 0 + */ + private Integer skip; + + /** + * Compound search operator to use. + * Valid values: "must", "should", "mustNot", "filter" + * Default: "must" + * + *

    + *
  • must - All clauses must match (AND logic)
  • + *
  • should - At least one clause should match (OR logic)
  • + *
  • mustNot - Clauses must not match (NOT logic)
  • + *
  • filter - Clauses must match but don't affect scoring
  • + *
+ */ + private String searchOperator; + + /** + * Checks if at least one search field is provided. + * + * @return true if at least one search field has a value + */ + public boolean hasSearchFields() { + return (plot != null && !plot.trim().isEmpty()) || + (fullplot != null && !fullplot.trim().isEmpty()) || + (directors != null && !directors.trim().isEmpty()) || + (writers != null && !writers.trim().isEmpty()) || + (cast != null && !cast.trim().isEmpty()); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java index f855276..f3a674f 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/UpdateMovieRequest.java @@ -9,7 +9,7 @@ /** * Data Transfer Object for updating an existing movie. * - *

This DTO is used for PUT /api/movies/{id} requests. + *

This DTO is used for PATCH /api/movies/{id} requests. * All fields are optional since partial updates are allowed. * Any field that is null will not be updated in the database. */ diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 9d3ca30..69fd882 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -64,15 +64,35 @@ public interface MovieService { // Atlas Search endpoints + /** + * Searches movies using MongoDB Atlas Search across multiple fields. + * Demonstrates text search using Atlas Search Index with compound operators. + * + *

Supports searching across: + *

    + *
  • plot - using phrase operator for exact phrase matching
  • + *
  • fullplot - using phrase operator for exact phrase matching
  • + *
  • directors - using text operator with fuzzy matching
  • + *
  • writers - using text operator with fuzzy matching
  • + *
  • cast - using text operator with fuzzy matching
  • + *
+ * + * @param searchRequest Search parameters including fields to search and compound operator + * @return List of movies matching the search criteria + */ + List searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest); + /** * Searches movies by plot using MongoDB Atlas Search. * Demonstrates text search using Atlas Search Index. * + * @deprecated Use {@link #searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest)} instead * @param plotQuery Text to search in the plot field * @param limit Maximum number of movies to return (default: 20, max: 100) * @param skip Number of results to skip for pagination (default: 0) * @return List of movies matching the search criteria */ + @Deprecated List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip); /** diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index e37b38f..046e269 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -560,24 +560,91 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { // Atlas Search methods @Override - public List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip) { - // Validate input - if (plotQuery == null || plotQuery.trim().isEmpty()) { - throw new ValidationException("Plot search query is required"); + public List searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest) { + // Validate that at least one search field is provided + if (!searchRequest.hasSearchFields()) { + throw new ValidationException("At least one search parameter must be provided"); + } + + // Validate search operator + String operator = searchRequest.getSearchOperator() != null ? + searchRequest.getSearchOperator() : "must"; + + if (!operator.equals("must") && !operator.equals("should") && + !operator.equals("mustNot") && !operator.equals("filter")) { + throw new ValidationException( + "Invalid search_operator '" + operator + "'. " + + "The search_operator must be one of: must, should, mustNot, filter" + ); } // Validate and set defaults for pagination - int resultLimit = Math.clamp(limit != null ? limit : 20, 1, 100); - int resultSkip = Math.max(skip != null ? skip : 0, 0); + int resultLimit = Math.clamp( + searchRequest.getLimit() != null ? searchRequest.getLimit() : 20, 1, 100 + ); + int resultSkip = Math.max( + searchRequest.getSkip() != null ? searchRequest.getSkip() : 0, 0 + ); + + // Build search phrases list + java.util.List searchPhrases = new java.util.ArrayList<>(); + + // Add plot search if provided (using phrase operator) + if (searchRequest.getPlot() != null && !searchRequest.getPlot().trim().isEmpty()) { + searchPhrases.add(new Document("phrase", new Document() + .append("query", searchRequest.getPlot().trim()) + .append("path", Movie.Fields.PLOT) + )); + } + + // Add fullplot search if provided (using phrase operator) + if (searchRequest.getFullplot() != null && !searchRequest.getFullplot().trim().isEmpty()) { + searchPhrases.add(new Document("phrase", new Document() + .append("query", searchRequest.getFullplot().trim()) + .append("path", Movie.Fields.FULLPLOT) + )); + } - // Build the $search aggregation stage using Document - // Spring Data MongoDB doesn't have native support for $search, so we use Document + // Add directors search if provided (using text operator with fuzzy matching) + if (searchRequest.getDirectors() != null && !searchRequest.getDirectors().trim().isEmpty()) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getDirectors().trim()) + .append("path", Movie.Fields.DIRECTORS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 5) + ) + )); + } + + // Add writers search if provided (using text operator with fuzzy matching) + if (searchRequest.getWriters() != null && !searchRequest.getWriters().trim().isEmpty()) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getWriters().trim()) + .append("path", Movie.Fields.WRITERS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 5) + ) + )); + } + + // Add cast search if provided (using text operator with fuzzy matching) + if (searchRequest.getCast() != null && !searchRequest.getCast().trim().isEmpty()) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getCast().trim()) + .append("path", Movie.Fields.CAST) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 5) + ) + )); + } + + // Build the $search aggregation stage with compound operator Document searchStage = new Document("$search", new Document() .append("index", "movieSearchIndex") - .append("phrase", new Document() - .append("query", plotQuery.trim()) - .append("path", Movie.Fields.PLOT) - ) + .append("compound", new Document(operator, searchPhrases)) ); Document skipStage = new Document("$skip", resultSkip); @@ -605,10 +672,10 @@ public List searchMoviesByPlot(String plotQuery, Integer limit, Integer s ); // Execute the aggregation pipeline - // Use MongoTemplate's getCollection to run raw aggregation - // Spring Data MongoDB doesn't have native support for $search, so we use the MongoDB driver directly try { - List aggregationPipeline = List.of(searchStage, skipStage, limitStage, projectStage); + java.util.List aggregationPipeline = java.util.List.of( + searchStage, skipStage, limitStage, projectStage + ); return mongoTemplate.getCollection("movies") .aggregate(aggregationPipeline) @@ -619,6 +686,21 @@ public List searchMoviesByPlot(String plotQuery, Integer limit, Integer s } } + @Override + @Deprecated + public List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip) { + // Delegate to the new searchMovies method + com.mongodb.samplemflix.model.dto.MovieSearchRequest request = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot(plotQuery) + .limit(limit) + .skip(skip) + .searchOperator("must") + .build(); + + return searchMovies(request); + } + @Override public List findSimilarMovies(String movieId, Integer limit) { // Validate movie ID diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index ae3c3cd..66f82d5 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -227,7 +227,7 @@ void testCreateMoviesBatch_Success() throws Exception { // ==================== UPDATE MOVIE TESTS ==================== @Test - @DisplayName("PUT /api/movies/{id} - Should update movie successfully") + @DisplayName("PATCH /api/movies/{id} - Should update movie successfully") void testUpdateMovie_Success() throws Exception { // Arrange String movieId = testId.toHexString(); @@ -241,7 +241,7 @@ void testUpdateMovie_Success() throws Exception { .thenReturn(updatedMovie); // Act & Assert - mockMvc.perform(put("/api/movies/{id}", movieId) + mockMvc.perform(patch("/api/movies/{id}", movieId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(updateRequest))) .andExpect(status().isOk()) @@ -251,7 +251,7 @@ void testUpdateMovie_Success() throws Exception { } @Test - @DisplayName("PUT /api/movies/{id} - Should return 404 when movie not found") + @DisplayName("PATCH /api/movies/{id} - Should return 404 when movie not found") void testUpdateMovie_NotFound() throws Exception { // Arrange String movieId = testId.toHexString(); @@ -259,7 +259,7 @@ void testUpdateMovie_NotFound() throws Exception { .thenThrow(new ResourceNotFoundException("Movie not found")); // Act & Assert - mockMvc.perform(put("/api/movies/{id}", movieId) + mockMvc.perform(patch("/api/movies/{id}", movieId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(updateRequest))) .andExpect(status().isNotFound()) @@ -506,7 +506,7 @@ void testGetDirectorsWithMostMovies_WithLimit() throws Exception { // ==================== ATLAS SEARCH ENDPOINT TESTS ==================== @Test - @DisplayName("GET /api/movies/searchByPlot - Should search movies by plot successfully") + @DisplayName("GET /api/movies/search - Should search movies by plot successfully") void testSearchMoviesByPlot_Success() throws Exception { // Arrange Movie movie1 = Movie.builder() @@ -525,11 +525,11 @@ void testSearchMoviesByPlot_Success() throws Exception { .genres(Arrays.asList("Sci-Fi", "Action")) .build(); - when(movieService.searchMoviesByPlot(eq("space adventure"), eq(20), eq(0))) + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) .thenReturn(Arrays.asList(movie1, movie2)); // Act & Assert - mockMvc.perform(get("/api/movies/searchByPlot") + mockMvc.perform(get("/api/movies/search") .param("plot", "space adventure")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) @@ -541,14 +541,14 @@ void testSearchMoviesByPlot_Success() throws Exception { } @Test - @DisplayName("GET /api/movies/searchByPlot - Should accept limit and skip parameters") + @DisplayName("GET /api/movies/search - Should accept limit and skip parameters") void testSearchMoviesByPlot_WithPagination() throws Exception { // Arrange - when(movieService.searchMoviesByPlot(eq("adventure"), eq(10), eq(5))) + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) .thenReturn(Arrays.asList()); // Act & Assert - mockMvc.perform(get("/api/movies/searchByPlot") + mockMvc.perform(get("/api/movies/search") .param("plot", "adventure") .param("limit", "10") .param("skip", "5")) @@ -558,22 +558,28 @@ void testSearchMoviesByPlot_WithPagination() throws Exception { } @Test - @DisplayName("GET /api/movies/searchByPlot - Should return 400 when plot parameter is missing") + @DisplayName("GET /api/movies/search - Should return 400 when no search parameters provided") void testSearchMoviesByPlot_MissingPlotParameter() throws Exception { + // Arrange + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) + .thenThrow(new ValidationException("At least one search parameter must be provided")); + // Act & Assert - mockMvc.perform(get("/api/movies/searchByPlot")) - .andExpect(status().isBadRequest()); + mockMvc.perform(get("/api/movies/search")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); } @Test - @DisplayName("GET /api/movies/searchByPlot - Should return 400 for validation error") + @DisplayName("GET /api/movies/search - Should return 400 for validation error") void testSearchMoviesByPlot_ValidationError() throws Exception { // Arrange - when(movieService.searchMoviesByPlot(anyString(), anyInt(), anyInt())) + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) .thenThrow(new ValidationException("Plot query cannot be empty")); // Act & Assert - mockMvc.perform(get("/api/movies/searchByPlot") + mockMvc.perform(get("/api/movies/search") .param("plot", "")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.success").value(false)) @@ -581,18 +587,77 @@ void testSearchMoviesByPlot_ValidationError() throws Exception { } @Test - @DisplayName("GET /api/movies/searchByPlot - Should return empty list when no matches found") + @DisplayName("GET /api/movies/search - Should return empty list when no matches found") void testSearchMoviesByPlot_NoResults() throws Exception { // Arrange - when(movieService.searchMoviesByPlot(eq("nonexistent"), eq(20), eq(0))) + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) .thenReturn(Arrays.asList()); // Act & Assert - mockMvc.perform(get("/api/movies/searchByPlot") + mockMvc.perform(get("/api/movies/search") .param("plot", "nonexistent")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data").isArray()) .andExpect(jsonPath("$.data", hasSize(0))); } + + @Test + @DisplayName("GET /api/movies/search - Should search by multiple fields") + void testSearchMovies_MultipleFields() throws Exception { + // Arrange + Movie movie = Movie.builder() + .id(new ObjectId()) + .title("The Godfather") + .year(1972) + .plot("The aging patriarch of an organized crime dynasty transfers control to his son") + .directors(Arrays.asList("Francis Ford Coppola")) + .cast(Arrays.asList("Marlon Brando", "Al Pacino")) + .build(); + + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) + .thenReturn(Arrays.asList(movie)); + + // Act & Assert + mockMvc.perform(get("/api/movies/search") + .param("directors", "Coppola") + .param("cast", "Pacino")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data", hasSize(1))) + .andExpect(jsonPath("$.data[0].title").value("The Godfather")); + } + + @Test + @DisplayName("GET /api/movies/search - Should accept searchOperator parameter") + void testSearchMovies_WithSearchOperator() throws Exception { + // Arrange + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) + .thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/movies/search") + .param("plot", "adventure") + .param("searchOperator", "should")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()); + } + + @Test + @DisplayName("GET /api/movies/search - Should return 400 for invalid searchOperator") + void testSearchMovies_InvalidSearchOperator() throws Exception { + // Arrange + when(movieService.searchMovies(any(com.mongodb.samplemflix.model.dto.MovieSearchRequest.class))) + .thenThrow(new ValidationException("Invalid search_operator 'invalid'. The search_operator must be one of: must, should, mustNot, filter")); + + // Act & Assert + mockMvc.perform(get("/api/movies/search") + .param("plot", "adventure") + .param("searchOperator", "invalid")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java index aea2155..fdc161d 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java @@ -9,6 +9,7 @@ import com.mongodb.samplemflix.service.MovieService; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.bson.Document; import org.junit.jupiter.api.AfterAll; @@ -266,7 +267,7 @@ private void createSearchIndex() throws Exception { // Create the index using the createSearchIndexes command Document createIndexCommand = new Document("createSearchIndexes", "movies") - .append("indexes", Arrays.asList( + .append("indexes", Collections.singletonList( new Document("name", SEARCH_INDEX_NAME) .append("definition", indexDefinition) )); diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java index c734d7a..91f86fe 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MovieIntegrationTest.java @@ -15,7 +15,7 @@ * - GET /api/movies/{id} * - POST /api/movies * - POST /api/movies/batch - * - PUT /api/movies/{id} + * - PATCH /api/movies/{id} * - PATCH /api/movies * - DELETE /api/movies/{id} * - DELETE /api/movies diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md index 333f67a..d57ed7e 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md @@ -8,7 +8,7 @@ The `AtlasSearchIntegrationTest` class tests the Atlas Search endpoints with a r 1. The Atlas Search index is created correctly 2. The index becomes ready for use (using polling) -3. The `searchByPlot` endpoint returns correct results +3. The `/search` endpoint returns correct results 4. Pagination works correctly 5. Empty results are handled properly From d02258e42b8119b3bde4111e16e2c8c87aab3f78 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 31 Oct 2025 13:20:17 -0400 Subject: [PATCH 039/110] small fixes --- server/java-spring/README.md | 2 +- .../controller/MovieControllerImpl.java | 6 +-- .../repository/MovieRepository.java | 4 +- .../samplemflix/service/MovieService.java | 13 ----- .../samplemflix/service/MovieServiceImpl.java | 37 ++------------ .../AtlasSearchIntegrationTest.java | 49 ++++++++++++++++--- 6 files changed, 52 insertions(+), 59 deletions(-) diff --git a/server/java-spring/README.md b/server/java-spring/README.md index 3bd4345..cc5d69c 100644 --- a/server/java-spring/README.md +++ b/server/java-spring/README.md @@ -155,7 +155,7 @@ Once the application is running, you can access: - `filter` - Criteria must match but don't affect scoring #### Vector Search -- `GET /api/movies/findSimilarMovies` - Find similar movies using vector search on plot embeddings +- `GET /api/movies/find-similar-movies` - Find similar movies using vector search on plot embeddings ## Development diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 736e096..2a05597 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -39,7 +39,7 @@ * - GET /api/movies/aggregations/years - Aggregate movies by year with statistics * - GET /api/movies/aggregations/directors - Aggregate directors with most movies * - GET /api/movies/search - Text search using Atlas Search Index across multiple fields (plot, fullplot, directors, writers, cast) - * - GET /api/movies/findSimilarMovies - Vector search to find similar movies based on plot embeddings + * - GET /api/movies/find-similar-movies - Vector search to find similar movies based on plot embeddings * */ @RestController @@ -414,7 +414,7 @@ public ResponseEntity>> searchMovies( } /** - * GET /api/movies/findSimilarMovies + * GET /api/movies/find-similar-movies * *

Finds similar movies using vector search on plot embeddings. * Demonstrates MongoDB Atlas Vector Search to find movies with similar plots. @@ -423,7 +423,7 @@ public ResponseEntity>> searchMovies( * @param limit Maximum number of similar movies to return (default: 10, max: 50) * @return List of similar movies based on plot embeddings */ - @GetMapping("/findSimilarMovies") + @GetMapping("/find-similar-movies") public ResponseEntity>> findSimilarMovies( @RequestParam String movieId, @RequestParam(defaultValue = "10") Integer limit) { diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java index 77cb013..35c85ab 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/repository/MovieRepository.java @@ -8,13 +8,13 @@ /** * Spring Data MongoDB repository for movie data access. * - * This repository extends MongoRepository which provides: + *

This repository extends MongoRepository which provides: * - Basic CRUD operations (save, findById, findAll, delete, etc.) * - Pagination and sorting support * - Query derivation from method names * - Custom query support via @Query annotation * - * For complex queries not supported by Spring Data, you can inject MongoTemplate + *

For complex queries not supported by Spring Data, you can inject MongoTemplate * in the service layer. */ @Repository diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java index 69fd882..b4ac462 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieService.java @@ -82,19 +82,6 @@ public interface MovieService { */ List searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest); - /** - * Searches movies by plot using MongoDB Atlas Search. - * Demonstrates text search using Atlas Search Index. - * - * @deprecated Use {@link #searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest)} instead - * @param plotQuery Text to search in the plot field - * @param limit Maximum number of movies to return (default: 20, max: 100) - * @param skip Number of results to skip for pagination (default: 0) - * @return List of movies matching the search criteria - */ - @Deprecated - List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip); - /** * Finds similar movies using vector search on plot embeddings. * Demonstrates MongoDB Atlas Vector Search. diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 046e269..0176b7e 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -7,15 +7,7 @@ import com.mongodb.samplemflix.exception.ResourceNotFoundException; import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; -import com.mongodb.samplemflix.model.dto.BatchInsertResponse; -import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; -import com.mongodb.samplemflix.model.dto.CreateMovieRequest; -import com.mongodb.samplemflix.model.dto.DeleteResponse; -import com.mongodb.samplemflix.model.dto.DirectorStatisticsResult; -import com.mongodb.samplemflix.model.dto.MovieSearchQuery; -import com.mongodb.samplemflix.model.dto.MovieWithCommentsResult; -import com.mongodb.samplemflix.model.dto.MoviesByYearResult; -import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; +import com.mongodb.samplemflix.model.dto.*; import com.mongodb.samplemflix.repository.MovieRepository; import java.util.Collection; import java.util.List; @@ -549,7 +541,7 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { .year(doc.getInteger("year")) .plot(doc.getString("plot")) .poster(doc.getString("poster")) - .genres((List) doc.get("genres")) + .genres(doc.getList("genres", String.class)) .imdb(imdbInfo) .recentComments(recentComments) .totalComments(doc.getInteger("totalComments")) @@ -560,7 +552,7 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { // Atlas Search methods @Override - public List searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest) { + public List searchMovies(MovieSearchRequest searchRequest) { // Validate that at least one search field is provided if (!searchRequest.hasSearchFields()) { throw new ValidationException("At least one search parameter must be provided"); @@ -686,21 +678,8 @@ public List searchMovies(com.mongodb.samplemflix.model.dto.MovieSearchReq } } - @Override - @Deprecated - public List searchMoviesByPlot(String plotQuery, Integer limit, Integer skip) { - // Delegate to the new searchMovies method - com.mongodb.samplemflix.model.dto.MovieSearchRequest request = - com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() - .plot(plotQuery) - .limit(limit) - .skip(skip) - .searchOperator("must") - .build(); - - return searchMovies(request); - } + // TODO: Implement vector search @Override public List findSimilarMovies(String movieId, Integer limit) { // Validate movie ID @@ -787,12 +766,4 @@ public List findSimilarMovies(String movieId, Integer limit) { throw new DatabaseOperationException("Error performing vector search: " + e.getMessage()); } } - - // TODO: Add advanced query methods - // - getMoviesByGenreStatistics() - Aggregation pipeline for genre statistics - // - getTopRatedMovies(int limit) - Movies sorted by rating - // - getMoviesByDecade(int decade) - Movies from a specific decade - // - getDirectorFilmography(String director) - All movies by a director - // - getActorFilmography(String actor) - All movies featuring an actor - // - getMovieRecommendations(String userId) - Personalized recommendations } diff --git a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java index fdc161d..dfd2e14 100644 --- a/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java +++ b/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/AtlasSearchIntegrationTest.java @@ -78,7 +78,7 @@ void setUp() throws Exception { // Atlas Search indexes documents asynchronously System.out.println("Waiting for test documents to be indexed..."); try { - Thread.sleep(5000); // Wait 5 seconds for indexing + Thread.sleep(10000); // Wait 10 seconds for indexing } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -115,7 +115,14 @@ void testSearchMoviesByPlot_Success() { } // Act - List results = movieService.searchMoviesByPlot("space adventure", 10, 0); + com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot("space adventure") + .limit(10) + .skip(0) + .searchOperator("must") + .build(); + List results = movieService.searchMovies(searchRequest); // Assert assertNotNull(results, "Search results should not be null"); @@ -137,7 +144,14 @@ void testSearchMoviesByPlot_NoResults() { } // Act - search for something that definitely doesn't exist - List results = movieService.searchMoviesByPlot("xyzabc123nonexistent", 10, 0); + com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot("xyzabc123nonexistent") + .limit(10) + .skip(0) + .searchOperator("must") + .build(); + List results = movieService.searchMovies(searchRequest); // Assert assertNotNull(results, "Search results should not be null"); @@ -153,7 +167,14 @@ void testSearchMoviesByPlot_WithLimit() { } // Act - List results = movieService.searchMoviesByPlot("adventure", 2, 0); + com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot("adventure") + .limit(2) + .skip(0) + .searchOperator("must") + .build(); + List results = movieService.searchMovies(searchRequest); // Assert assertNotNull(results, "Search results should not be null"); @@ -169,10 +190,24 @@ void testSearchMoviesByPlot_WithPagination() { } // Act - Get first page - List firstPage = movieService.searchMoviesByPlot("adventure", 2, 0); - + com.mongodb.samplemflix.model.dto.MovieSearchRequest firstPageRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot("adventure") + .limit(2) + .skip(0) + .searchOperator("must") + .build(); + List firstPage = movieService.searchMovies(firstPageRequest); + // Act - Get second page - List secondPage = movieService.searchMoviesByPlot("adventure", 2, 2); + com.mongodb.samplemflix.model.dto.MovieSearchRequest secondPageRequest = + com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() + .plot("adventure") + .limit(2) + .skip(2) + .searchOperator("must") + .build(); + List secondPage = movieService.searchMovies(secondPageRequest); // Assert assertNotNull(firstPage, "First page should not be null"); From 246e795cab4c2fe52270e364d26fca1d4c5166ef Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 31 Oct 2025 17:35:53 -0400 Subject: [PATCH 040/110] docs(java): update details for swagger docs --- .../samplemflix/SampleMflixApplication.java | 3 + .../samplemflix/config/CorsConfig.java | 28 ++- .../samplemflix/config/MongoConfig.java | 2 +- .../samplemflix/config/OpenApiConfig.java | 49 ++++ .../controller/MovieControllerImpl.java | 227 +++++++++--------- .../samplemflix/service/MovieServiceImpl.java | 28 ++- .../src/main/resources/application.properties | 9 + 7 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 server/java-spring/src/main/java/com/mongodb/samplemflix/config/OpenApiConfig.java diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java index b3aff5b..72ba6a9 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/SampleMflixApplication.java @@ -1,5 +1,6 @@ package com.mongodb.samplemflix; +import io.swagger.v3.oas.annotations.Hidden; import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -26,7 +27,9 @@ public static void main(String[] args) { /** * Root endpoint providing basic information about the API. + * Hidden from Swagger UI documentation. */ + @Hidden @GetMapping("/") public Map root() { return Map.of( diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java index 8e2a31b..300b939 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/CorsConfig.java @@ -1,6 +1,5 @@ package com.mongodb.samplemflix.config; -import java.util.Arrays; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,22 +29,29 @@ public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); - - // Allow credentials (cookies, authorization headers) - config.setAllowCredentials(true); - - // Set allowed origins from environment variable - config.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); - + + // Allow wildcard origins for Swagger UI + config.setAllowCredentials(false); + + // Set allowed origins - include localhost for Swagger UI + // Parse the configured origins and add localhost variations for Swagger UI + String[] origins = allowedOrigins.split(","); + for (String origin : origins) { + config.addAllowedOrigin(origin.trim()); + } + // Add localhost origins for Swagger UI (on any port) + config.addAllowedOriginPattern("http://localhost:*"); + config.addAllowedOriginPattern("http://127.0.0.1:*"); + // Allow all headers config.addAllowedHeader("*"); - + // Allow all HTTP methods config.addAllowedMethod("*"); - + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); - + return new CorsFilter(source); } } diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index b0fcfce..df4395c 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -72,7 +72,7 @@ protected void configureClientSettings(MongoClientSettings.Builder builder) { // Configure socket timeouts to prevent hanging connections .applyToSocketSettings(socketBuilder -> socketBuilder.connectTimeout(10000, TimeUnit.MILLISECONDS) // 10s to establish connection - .readTimeout(10000, TimeUnit.MILLISECONDS) // 10s to wait for server response + .readTimeout(60000, TimeUnit.MILLISECONDS) // 60s to wait for server response (increased for aggregations) ) // Configure server selection timeout .applyToClusterSettings(clusterBuilder -> diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/config/OpenApiConfig.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/OpenApiConfig.java new file mode 100644 index 0000000..a085010 --- /dev/null +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/config/OpenApiConfig.java @@ -0,0 +1,49 @@ +package com.mongodb.samplemflix.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * OpenAPI/Swagger configuration for the Sample MFlix API. + * + * This configuration provides metadata for the API documentation + * and ensures the "Try it out" functionality works correctly. + */ +@Configuration +public class OpenApiConfig { + + @Value("${server.port:3001}") + private String serverPort; + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Sample MFlix API") + .version("1.0.0") + .description("Java Spring Boot backend demonstrating MongoDB operations with the sample_mflix dataset. " + + "This API provides CRUD operations for movies, text search, filtering, and pagination.") + .contact(new Contact() + .name("MongoDB Documentation Team") + .url("https://www.mongodb.com/docs")) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0.html"))) + .servers(List.of( + new Server() + .url("http://localhost:" + serverPort) + .description("Local development server"), + new Server() + .url("http://localhost:3001") + .description("Default local server") + )); + } +} + diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 2a05597..15b7dfd 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -12,6 +12,9 @@ import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.model.response.SuccessResponse; import com.mongodb.samplemflix.service.MovieService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.time.Instant; import java.util.List; @@ -44,6 +47,7 @@ */ @RestController @RequestMapping("/api/movies") +@Tag(name = "Movies", description = "Movie management endpoints for CRUD operations, search, and aggregations") public class MovieControllerImpl { private final MovieService movieService; @@ -52,21 +56,31 @@ public MovieControllerImpl(MovieService movieService) { this.movieService = movieService; } - /** - * GET /api/movies - * - *

Retrieves multiple movies with optional filtering, sorting, and pagination. - */ + @Operation( + summary = "Get all movies with optional filtering, sorting, and pagination", + description = "Retrieve a list of movies with optional filtering by text search, genre, year, and rating. " + + "Supports sorting and pagination. Text search (q parameter) uses MongoDB text index to search " + + "across plot, title, and fullplot fields." + ) @GetMapping public ResponseEntity>> getAllMovies( + @Parameter(description = "Text search query (searches plot, title, fullplot)") @RequestParam(required = false) String q, + @Parameter(description = "Filter by genre (case-insensitive partial match)") @RequestParam(required = false) String genre, + @Parameter(description = "Filter by exact year") @RequestParam(required = false) Integer year, + @Parameter(description = "Minimum IMDB rating (inclusive)") @RequestParam(required = false) Double minRating, + @Parameter(description = "Maximum IMDB rating (inclusive)") @RequestParam(required = false) Double maxRating, + @Parameter(description = "Number of results to return (default: 20)") @RequestParam(defaultValue = "20") Integer limit, + @Parameter(description = "Number of results to skip for pagination (default: 0)") @RequestParam(defaultValue = "0") Integer skip, + @Parameter(description = "Field to sort by (default: title)") @RequestParam(defaultValue = "title") String sortBy, + @Parameter(description = "Sort order: 'asc' or 'desc' (default: asc)") @RequestParam(defaultValue = "asc") String sortOrder) { MovieSearchQuery query = MovieSearchQuery.builder() @@ -93,13 +107,14 @@ public ResponseEntity>> getAllMovies( return ResponseEntity.ok(response); } - /** - * GET /api/movies/{id} - * - *

Retrieves a single movie by its ObjectId. - */ + @Operation( + summary = "Get a single movie by ID", + description = "Retrieve a single movie by its MongoDB ObjectId." + ) @GetMapping("/{id}") - public ResponseEntity> getMovieById(@PathVariable String id) { + public ResponseEntity> getMovieById( + @Parameter(description = "Movie ObjectId (24-character hex string)", required = true) + @PathVariable String id) { Movie movie = movieService.getMovieById(id); SuccessResponse response = SuccessResponse.builder() @@ -112,13 +127,14 @@ public ResponseEntity> getMovieById(@PathVariable String return ResponseEntity.ok(response); } - /** - * POST /api/movies - * - *

Creates a single new movie document. - */ + @Operation( + summary = "Create a new movie", + description = "Create a single new movie document. Only the title field is required; all other fields are optional." + ) @PostMapping - public ResponseEntity> createMovie(@Valid @RequestBody CreateMovieRequest request) { + public ResponseEntity> createMovie( + @Parameter(description = "Movie data to create", required = true) + @Valid @RequestBody CreateMovieRequest request) { Movie movie = movieService.createMovie(request); SuccessResponse response = SuccessResponse.builder() @@ -131,13 +147,13 @@ public ResponseEntity> createMovie(@Valid @RequestBody Cr return ResponseEntity.status(HttpStatus.CREATED).body(response); } - /** - * POST /api/movies/batch - * - *

Creates multiple movie documents in a single operation. - */ + @Operation( + summary = "Create multiple movies in batch", + description = "Create multiple movie documents in a single operation using insertMany." + ) @PostMapping("/batch") public ResponseEntity> createMoviesBatch( + @Parameter(description = "List of movies to create", required = true) @RequestBody List requests) { BatchInsertResponse result = movieService.createMoviesBatch(requests); @@ -150,16 +166,16 @@ public ResponseEntity> createMoviesBatch( return ResponseEntity.status(HttpStatus.CREATED).body(response); } - - /** - * PATCH /api/movies/{id} - * - *

Updates a single movie document with partial updates. - * Only the fields provided in the request will be updated. - */ + + @Operation( + summary = "Update a movie by ID", + description = "Update a single movie document by its ObjectId using updateOne with $set operator." + ) @PatchMapping("/{id}") public ResponseEntity> updateMovie( + @Parameter(description = "Movie ObjectId to update", required = true) @PathVariable String id, + @Parameter(description = "Updated movie data (only provided fields will be updated)", required = true) @RequestBody UpdateMovieRequest request) { Movie movie = movieService.updateMovie(id, request); @@ -173,14 +189,15 @@ public ResponseEntity> updateMovie( return ResponseEntity.ok(response); } - /** - * PATCH /api/movies - * - *

Updates multiple movies based on a filter. - */ + @Operation( + summary = "Update multiple movies in batch", + description = "Update multiple movies matching the given filter using updateMany. " + + "Request body should contain 'filter' and 'update' objects." + ) @SuppressWarnings("unchecked") @PatchMapping public ResponseEntity> updateMoviesBatch( + @Parameter(description = "Request body with 'filter' and 'update' objects", required = true) @RequestBody Map body) { Document filter = new Document((Map) body.get("filter")); Document update = new Document((Map) body.get("update")); @@ -198,13 +215,15 @@ public ResponseEntity> updateMoviesBatch( return ResponseEntity.ok(response); } - /** - * DELETE /api/movies/{id}/find-and-delete - * - *

Finds and deletes a movie in a single atomic operation. - */ + @Operation( + summary = "Find and delete a movie atomically", + description = "Find and delete a movie in a single atomic operation using findOneAndDelete. " + + "Returns the deleted movie document." + ) @DeleteMapping("/{id}/find-and-delete") - public ResponseEntity> findAndDeleteMovie(@PathVariable String id) { + public ResponseEntity> findAndDeleteMovie( + @Parameter(description = "Movie ObjectId to find and delete", required = true) + @PathVariable String id) { Movie movie = movieService.findAndDeleteMovie(id); SuccessResponse response = SuccessResponse.builder() @@ -217,13 +236,14 @@ public ResponseEntity> findAndDeleteMovie(@PathVariable S return ResponseEntity.ok(response); } - /** - * DELETE /api/movies/{id} - * - *

Deletes a single movie document. - */ + @Operation( + summary = "Delete a movie by ID", + description = "Delete a single movie document by its ObjectId using deleteOne." + ) @DeleteMapping("/{id}") - public ResponseEntity> deleteMovie(@PathVariable String id) { + public ResponseEntity> deleteMovie( + @Parameter(description = "Movie ObjectId to delete", required = true) + @PathVariable String id) { DeleteResponse result = movieService.deleteMovie(id); SuccessResponse response = SuccessResponse.builder() @@ -236,14 +256,15 @@ public ResponseEntity> deleteMovie(@PathVariable return ResponseEntity.ok(response); } - /** - * DELETE /api/movies - * - *

Deletes multiple movies based on a filter. - */ + @Operation( + summary = "Delete multiple movies in batch", + description = "Delete multiple movies matching the given filter using deleteMany. " + + "Request body should contain a 'filter' object." + ) @SuppressWarnings("unchecked") @DeleteMapping public ResponseEntity> deleteMoviesBatch( + @Parameter(description = "Request body with 'filter' object", required = true) @RequestBody Map body) { Document filter = new Document((Map) body.get("filter")); @@ -261,19 +282,16 @@ public ResponseEntity> deleteMoviesBatch( // Aggregation endpoints for reporting - /** - * GET /api/movies/aggregations/comments - * - *

Aggregates movies with their most recent comments. - * Demonstrates MongoDB $lookup (join) operation to combine movies with comments. - * - * @param limit Maximum number of movies to return (default: 10, max: 50) - * @param movieId Optional movie ID to filter by specific movie - * @return List of movies with their recent comments - */ + @Operation( + summary = "Aggregate movies with their most recent comments", + description = "Aggregates movies with their most recent comments using MongoDB $lookup (join) operation. " + + "Demonstrates how to combine data from the movies and comments collections." + ) @GetMapping("/aggregations/comments") public ResponseEntity>> getMoviesWithMostComments( + @Parameter(description = "Maximum number of movies to return (default: 10, max: 50)") @RequestParam(defaultValue = "10") Integer limit, + @Parameter(description = "Optional movie ID to filter by specific movie") @RequestParam(required = false) String movieId) { List results = movieService.getMoviesWithMostComments(limit, movieId); @@ -299,14 +317,11 @@ public ResponseEntity>> getMoviesW return ResponseEntity.ok(response); } - /** - * GET /api/movies/aggregations/years - * - *

Aggregates movies by year with statistics. - * Demonstrates MongoDB $group operation for statistical aggregation. - * - * @return List of yearly statistics including movie count and average rating - */ + @Operation( + summary = "Aggregate movies by year with statistics", + description = "Aggregates movies by year with statistics including movie count and average rating. " + + "Demonstrates MongoDB $group operation for statistical aggregation." + ) @GetMapping("/aggregations/years") public ResponseEntity>> getMoviesByYearWithStats() { @@ -323,17 +338,14 @@ public ResponseEntity>> getMoviesByYear return ResponseEntity.ok(response); } - /** - * GET /api/movies/aggregations/directors - * - *

Aggregates directors with the most movies. - * Demonstrates MongoDB $unwind operation for array flattening and aggregation. - * - * @param limit Maximum number of directors to return (default: 20, max: 100) - * @return List of directors with their movie count and average rating - */ + @Operation( + summary = "Aggregate directors with the most movies", + description = "Aggregates directors with the most movies and their statistics. " + + "Demonstrates MongoDB $unwind operation for array flattening and aggregation." + ) @GetMapping("/aggregations/directors") public ResponseEntity>> getDirectorsWithMostMovies( + @Parameter(description = "Maximum number of directors to return (default: 20, max: 100)") @RequestParam(defaultValue = "20") Integer limit) { List results = movieService.getDirectorsWithMostMovies(limit); @@ -351,42 +363,30 @@ public ResponseEntity>> getDirect // Atlas Search endpoints - /** - * GET /api/movies/search - * - *

Searches movies using MongoDB Atlas Search across multiple fields. - * Demonstrates text search using Atlas Search Index with compound operators. - * - *

Supports searching across: - *

    - *
  • plot - using phrase operator for exact phrase matching
  • - *
  • fullplot - using phrase operator for exact phrase matching
  • - *
  • directors - using text operator with fuzzy matching
  • - *
  • writers - using text operator with fuzzy matching
  • - *
  • cast - using text operator with fuzzy matching
  • - *
- * - *

At least one search field must be provided. - * - * @param plot Text to search in the plot field (optional) - * @param fullplot Text to search in the fullplot field (optional) - * @param directors Text to search in the directors field (optional) - * @param writers Text to search in the writers field (optional) - * @param cast Text to search in the cast field (optional) - * @param limit Maximum number of movies to return (default: 20, max: 100) - * @param skip Number of results to skip for pagination (default: 0) - * @param searchOperator Compound operator: must, should, mustNot, filter (default: must) - * @return List of movies matching the search criteria - */ + @Operation( + summary = "Search movies using MongoDB Atlas Search", + description = "Search movies using MongoDB Atlas Search across multiple fields (plot, fullplot, directors, writers, cast). " + + "You can combine multiple fields in a single query and control how they are combined using the searchOperator parameter. " + + "At least one search field must be provided. " + + "Plot and fullplot use phrase operator for exact matching, while directors, writers, and cast use text operator with fuzzy matching." + ) @GetMapping("/search") public ResponseEntity>> searchMovies( + @Parameter(description = "Text to search in the plot field (phrase matching)") @RequestParam(required = false) String plot, + @Parameter(description = "Text to search in the fullplot field (phrase matching)") @RequestParam(required = false) String fullplot, + @Parameter(description = "Text to search in the directors field (fuzzy matching)") @RequestParam(required = false) String directors, + @Parameter(description = "Text to search in the writers field (fuzzy matching)") @RequestParam(required = false) String writers, + @Parameter(description = "Text to search in the cast field (fuzzy matching)") @RequestParam(required = false) String cast, + @Parameter(description = "Maximum number of movies to return (default: 20, max: 100)") @RequestParam(defaultValue = "20") Integer limit, + @Parameter(description = "Number of results to skip for pagination (default: 0)") @RequestParam(defaultValue = "0") Integer skip, + @Parameter(description = "Compound operator: must, should, mustNot, or filter (default: must)") @RequestParam(defaultValue = "must") String searchOperator) { com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = @@ -413,19 +413,16 @@ public ResponseEntity>> searchMovies( return ResponseEntity.ok(response); } - /** - * GET /api/movies/find-similar-movies - * - *

Finds similar movies using vector search on plot embeddings. - * Demonstrates MongoDB Atlas Vector Search to find movies with similar plots. - * - * @param movieId ID of the movie to find similar movies for (required) - * @param limit Maximum number of similar movies to return (default: 10, max: 50) - * @return List of similar movies based on plot embeddings - */ + @Operation( + summary = "Find similar movies using vector search", + description = "Find similar movies using MongoDB Atlas Vector Search on plot embeddings. " + + "Demonstrates how to use vector search to find movies with similar plots based on semantic similarity." + ) @GetMapping("/find-similar-movies") public ResponseEntity>> findSimilarMovies( + @Parameter(description = "ID of the movie to find similar movies for", required = true) @RequestParam String movieId, + @Parameter(description = "Maximum number of similar movies to return (default: 10, max: 50)") @RequestParam(defaultValue = "10") Integer limit) { List movies = movieService.findSimilarMovies(movieId, limit); diff --git a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 0176b7e..f49b05d 100644 --- a/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -349,17 +349,22 @@ public List getMoviesWithMostComments(Integer limit, St // Build aggregation pipeline // This demonstrates $lookup (join), $unwind, $sort, $group, and $project operations + // Optimized to limit movies before $lookup to reduce processing time Aggregation aggregation = Aggregation.newAggregation( // STAGE 1: Match movies with valid year data Aggregation.match(matchCriteria), - // STAGE 2: Lookup (join) with comments collection + // STAGE 2: Limit movies before lookup (optimization: process fewer documents) + // We'll get more results than needed, then filter and re-limit after lookup + Aggregation.limit(resultLimit * 10), + + // STAGE 3: Lookup (join) with comments collection Aggregation.lookup("comments", "_id", "movie_id", "comments"), - // STAGE 3: Filter to only movies with comments + // STAGE 4: Filter to only movies with comments Aggregation.match(Criteria.where("comments").ne(List.of())), - // STAGE 4: Add computed fields + // STAGE 5: Add computed fields Aggregation.project() .and(Movie.Fields.ID).as("_id") .and(Movie.Fields.TITLE).as("title") @@ -372,13 +377,13 @@ public List getMoviesWithMostComments(Integer limit, St .and(ArrayOperators.Size.lengthOfArray("comments")).as("totalComments") .and(ArrayOperators.ArrayElemAt.arrayOf("comments.date").elementAt(0)).as("mostRecentCommentDate"), - // STAGE 5: Sort by most recent comment date (descending) + // STAGE 6: Sort by most recent comment date (descending) Aggregation.sort(Sort.Direction.DESC, "mostRecentCommentDate"), - // STAGE 6: Limit results + // STAGE 7: Limit results to requested amount Aggregation.limit(resultLimit), - // STAGE 7: Project final output with recent comments slice + // STAGE 8: Project final output with recent comments slice Aggregation.project() .and(ConditionalOperators.ifNull("_id").then("")).as("id") .and("title").as("title") @@ -535,8 +540,17 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { .collect(Collectors.toList()); } + // Extract movie ID - handle both String and ObjectId types + String movieId = null; + Object idObj = doc.get("id"); + if (idObj instanceof String) { + movieId = (String) idObj; + } else if (idObj instanceof ObjectId) { + movieId = ((ObjectId) idObj).toHexString(); + } + return MovieWithCommentsResult.builder() - .id(doc.getString("id")) + .id(movieId) .title(doc.getString("title")) .year(doc.getInteger("year")) .plot(doc.getString("plot")) diff --git a/server/java-spring/src/main/resources/application.properties b/server/java-spring/src/main/resources/application.properties index ff84d9a..57c86f9 100644 --- a/server/java-spring/src/main/resources/application.properties +++ b/server/java-spring/src/main/resources/application.properties @@ -26,3 +26,12 @@ spring.jackson.serialization.write-dates-as-timestamps=false springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html springdoc.swagger-ui.operationsSorter=method +# Disable "Try it out" by default (user must click to enable) +springdoc.swagger-ui.tryItOutEnabled=false +# Display request duration in Swagger UI +springdoc.swagger-ui.displayRequestDuration=true +# Enable deep linking for direct access to operations +springdoc.swagger-ui.deepLinking=true +# Show request/response examples +springdoc.swagger-ui.defaultModelsExpandDepth=1 +springdoc.swagger-ui.defaultModelExpandDepth=1 From 09927990b13aa4d85fa8c0156f169ebbac524cf4 Mon Sep 17 00:00:00 2001 From: Jordan Smith <45415425+jordan-smith721@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:02:38 -0500 Subject: [PATCH 041/110] feat: implement movie details page and optimize components (#9) --- .../ActionButtons/ActionButtons.module.css | 84 ++++ .../ActionButtons/ActionButtons.tsx | 45 ++ client/app/components/ActionButtons/index.ts | 1 + .../components/AddMovieForm/AddMovieForm.tsx | 427 ++++++++++++++++++ client/app/components/AddMovieForm/index.ts | 1 + .../BatchEditMovieForm/BatchEditMovieForm.tsx | 363 +++++++++++++++ .../components/BatchEditMovieForm/index.ts | 1 + .../EditMovieForm/EditMovieForm.module.css | 308 +++++++++++++ .../EditMovieForm/EditMovieForm.tsx | 329 ++++++++++++++ client/app/components/EditMovieForm/index.ts | 1 + .../LoadingSkeleton.module.css | 215 +++++++++ .../LoadingSkeleton/LoadingSkeleton.tsx | 113 +++++ .../app/components/LoadingSkeleton/index.ts | 8 + .../components/MovieCard/MovieCard.module.css | 36 +- client/app/components/MovieCard/MovieCard.tsx | 41 +- .../PageSizeSelector.module.css | 56 +++ .../PageSizeSelector/PageSizeSelector.tsx | 42 ++ .../app/components/PageSizeSelector/index.ts | 1 + .../Pagination/Pagination.module.css | 113 +++++ .../app/components/Pagination/Pagination.tsx | 81 ++++ client/app/components/Pagination/index.ts | 1 + client/app/components/index.ts | 16 +- client/app/globals.css | 12 + client/app/layout.module.css | 79 ++++ client/app/layout.tsx | 29 ++ client/app/lib/api.ts | 317 ++++++++++++- client/app/lib/constants.ts | 3 +- client/app/movie/[id]/error.module.css | 91 ++++ client/app/movie/[id]/error.tsx | 53 +++ client/app/movie/[id]/loading.tsx | 22 + client/app/movie/[id]/not-found.module.css | 50 ++ client/app/movie/[id]/not-found.tsx | 16 + client/app/movie/[id]/page.module.css | 317 +++++++++++++ client/app/movie/[id]/page.tsx | 385 ++++++++++++++++ client/app/movies/loading.module.css | 45 -- client/app/movies/loading.tsx | 24 +- client/app/movies/movies.module.css | 302 ++++++++++++- client/app/movies/page.tsx | 358 ++++++++++++++- client/app/types/movie.ts | 48 +- .../src/controllers/movieController.ts | 32 +- 40 files changed, 4362 insertions(+), 104 deletions(-) create mode 100644 client/app/components/ActionButtons/ActionButtons.module.css create mode 100644 client/app/components/ActionButtons/ActionButtons.tsx create mode 100644 client/app/components/ActionButtons/index.ts create mode 100644 client/app/components/AddMovieForm/AddMovieForm.tsx create mode 100644 client/app/components/AddMovieForm/index.ts create mode 100644 client/app/components/BatchEditMovieForm/BatchEditMovieForm.tsx create mode 100644 client/app/components/BatchEditMovieForm/index.ts create mode 100644 client/app/components/EditMovieForm/EditMovieForm.module.css create mode 100644 client/app/components/EditMovieForm/EditMovieForm.tsx create mode 100644 client/app/components/EditMovieForm/index.ts create mode 100644 client/app/components/LoadingSkeleton/LoadingSkeleton.module.css create mode 100644 client/app/components/LoadingSkeleton/LoadingSkeleton.tsx create mode 100644 client/app/components/LoadingSkeleton/index.ts create mode 100644 client/app/components/PageSizeSelector/PageSizeSelector.module.css create mode 100644 client/app/components/PageSizeSelector/PageSizeSelector.tsx create mode 100644 client/app/components/PageSizeSelector/index.ts create mode 100644 client/app/components/Pagination/Pagination.module.css create mode 100644 client/app/components/Pagination/Pagination.tsx create mode 100644 client/app/components/Pagination/index.ts create mode 100644 client/app/layout.module.css create mode 100644 client/app/movie/[id]/error.module.css create mode 100644 client/app/movie/[id]/error.tsx create mode 100644 client/app/movie/[id]/loading.tsx create mode 100644 client/app/movie/[id]/not-found.module.css create mode 100644 client/app/movie/[id]/not-found.tsx create mode 100644 client/app/movie/[id]/page.module.css create mode 100644 client/app/movie/[id]/page.tsx delete mode 100644 client/app/movies/loading.module.css diff --git a/client/app/components/ActionButtons/ActionButtons.module.css b/client/app/components/ActionButtons/ActionButtons.module.css new file mode 100644 index 0000000..7372279 --- /dev/null +++ b/client/app/components/ActionButtons/ActionButtons.module.css @@ -0,0 +1,84 @@ +/** + * Action Buttons Styles + * + * CSS Module for the action buttons component. + * Provides consistent styling with the rest of the application. + */ + +.actionButtons { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + justify-content: flex-start; +} + +.button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.editButton { + background: #0070f3; + color: white; + border: 1px solid #0070f3; +} + +.editButton:hover:not(:disabled) { + background: #0051cc; + border-color: #0051cc; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 112, 243, 0.3); +} + +.deleteButton { + background: #dc2626; + color: white; + border: 1px solid #dc2626; +} + +.deleteButton:hover:not(:disabled) { + background: #b91c1c; + border-color: #b91c1c; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .actionButtons { + flex-direction: column; + gap: 0.75rem; + } + + .button { + width: 100%; + padding: 0.875rem 1rem; + } +} + +@media (max-width: 480px) { + .actionButtons { + gap: 0.5rem; + } + + .button { + padding: 0.75rem 1rem; + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/client/app/components/ActionButtons/ActionButtons.tsx b/client/app/components/ActionButtons/ActionButtons.tsx new file mode 100644 index 0000000..eae57b1 --- /dev/null +++ b/client/app/components/ActionButtons/ActionButtons.tsx @@ -0,0 +1,45 @@ +'use client'; + +/** + * Action Buttons Component + * + * Provides Edit and Delete actions for movie details page + */ + +import styles from './ActionButtons.module.css'; + +interface ActionButtonsProps { + onEdit: () => void; + onDelete: () => void; + isLoading?: boolean; + disabled?: boolean; +} + +export default function ActionButtons({ + onEdit, + onDelete, + isLoading = false, + disabled = false +}: ActionButtonsProps) { + return ( +

+ + + +
+ ); +} \ No newline at end of file diff --git a/client/app/components/ActionButtons/index.ts b/client/app/components/ActionButtons/index.ts new file mode 100644 index 0000000..eaca7eb --- /dev/null +++ b/client/app/components/ActionButtons/index.ts @@ -0,0 +1 @@ +export { default } from './ActionButtons'; \ No newline at end of file diff --git a/client/app/components/AddMovieForm/AddMovieForm.tsx b/client/app/components/AddMovieForm/AddMovieForm.tsx new file mode 100644 index 0000000..d040adf --- /dev/null +++ b/client/app/components/AddMovieForm/AddMovieForm.tsx @@ -0,0 +1,427 @@ +'use client'; + +/** + * Add Movie Form Component + * + * Form for creating new movies with validation. + * Supports both single movie creation and batch creation. + */ + +import { useState } from 'react'; +import { Movie } from '../../types/movie'; +import styles from '../EditMovieForm/EditMovieForm.module.css'; + +interface AddMovieFormProps { + onSave: (movieData: Omit[]) => void; + onCancel: () => void; + isLoading?: boolean; +} + +interface MovieFormData { + title: string; + year: string; + plot: string; + runtime: string; + rated: string; + genres: string; + directors: string; + writers: string; + cast: string; + countries: string; + languages: string; + poster: string; +} + +const getInitialFormData = (): MovieFormData => ({ + title: '', + year: '', + plot: '', + runtime: '', + rated: '', + genres: '', + directors: '', + writers: '', + cast: '', + countries: '', + languages: '', + poster: '', +}); + +export default function AddMovieForm({ + onSave, + onCancel, + isLoading = false +}: AddMovieFormProps) { + const [movieForms, setMovieForms] = useState([getInitialFormData()]); + const [errors, setErrors] = useState>>({}); + + const validateForm = (formData: MovieFormData, index: number) => { + const newErrors: Record = {}; + + if (!formData.title.trim()) { + newErrors.title = 'Title is required'; + } + + if (formData.year && (parseInt(formData.year) < 1800 || parseInt(formData.year) > new Date().getFullYear() + 5)) { + newErrors.year = 'Please enter a valid year'; + } + + if (formData.runtime && (parseInt(formData.runtime) < 1 || parseInt(formData.runtime) > 1000)) { + newErrors.runtime = 'Please enter a valid runtime in minutes'; + } + + return newErrors; + }; + + const validateAllForms = () => { + const allErrors: Record> = {}; + let hasErrors = false; + + movieForms.forEach((formData, index) => { + const formErrors = validateForm(formData, index); + if (Object.keys(formErrors).length > 0) { + allErrors[index] = formErrors; + hasErrors = true; + } + }); + + setErrors(allErrors); + return !hasErrors; + }; + + const convertFormDataToMovie = (formData: MovieFormData): Omit => { + const movieData: Omit = { + title: formData.title.trim(), + year: formData.year ? parseInt(formData.year) : undefined, + plot: formData.plot.trim() || undefined, + runtime: formData.runtime ? parseInt(formData.runtime) : undefined, + rated: formData.rated.trim() || undefined, + genres: formData.genres ? formData.genres.split(',').map(g => g.trim()).filter(g => g) : undefined, + directors: formData.directors ? formData.directors.split(',').map(d => d.trim()).filter(d => d) : undefined, + writers: formData.writers ? formData.writers.split(',').map(w => w.trim()).filter(w => w) : undefined, + cast: formData.cast ? formData.cast.split(',').map(c => c.trim()).filter(c => c) : undefined, + countries: formData.countries ? formData.countries.split(',').map(c => c.trim()).filter(c => c) : undefined, + languages: formData.languages ? formData.languages.split(',').map(l => l.trim()).filter(l => l) : undefined, + poster: formData.poster.trim() || undefined, + }; + + // Remove undefined values + return Object.fromEntries( + Object.entries(movieData).filter(([_, value]) => value !== undefined) + ) as Omit; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateAllForms()) { + return; + } + + // Convert all form data to movie objects + const moviesData = movieForms.map(convertFormDataToMovie); + onSave(moviesData); + }; + + const handleInputChange = (index: number, field: string, value: string) => { + setMovieForms(prev => prev.map((form, i) => + i === index ? { ...form, [field]: value } : form + )); + + // Clear error when user starts typing + if (errors[index]?.[field]) { + setErrors(prev => ({ + ...prev, + [index]: { ...prev[index], [field]: '' } + })); + } + }; + + const handleAddMore = () => { + setMovieForms(prev => [...prev, getInitialFormData()]); + }; + + const handleRemoveForm = (index: number) => { + if (movieForms.length > 1) { + setMovieForms(prev => prev.filter((_, i) => i !== index)); + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[index]; + // Reindex errors for forms after the removed one + Object.keys(newErrors).forEach(key => { + const keyNum = parseInt(key); + if (keyNum > index) { + newErrors[keyNum - 1] = newErrors[keyNum]; + delete newErrors[keyNum]; + } + }); + return newErrors; + }); + } + }; + + const renderMovieForm = (formData: MovieFormData, index: number) => { + const formErrors = errors[index] || {}; + + return ( +
+ {movieForms.length > 1 && ( +
+

Movie {index + 1}

+ +
+ )} + +
+ {/* Title */} +
+ + handleInputChange(index, 'title', e.target.value)} + className={`${styles.input} ${formErrors.title ? styles.inputError : ''}`} + disabled={isLoading} + required + /> + {formErrors.title && {formErrors.title}} +
+ + {/* Year */} +
+ + handleInputChange(index, 'year', e.target.value)} + className={`${styles.input} ${formErrors.year ? styles.inputError : ''}`} + disabled={isLoading} + min="1800" + max={new Date().getFullYear() + 5} + /> + {formErrors.year && {formErrors.year}} +
+ + {/* Runtime */} +
+ + handleInputChange(index, 'runtime', e.target.value)} + className={`${styles.input} ${formErrors.runtime ? styles.inputError : ''}`} + disabled={isLoading} + min="1" + max="1000" + /> + {formErrors.runtime && {formErrors.runtime}} +
+ + {/* Rated */} +
+ + handleInputChange(index, 'rated', e.target.value)} + className={styles.input} + disabled={isLoading} + placeholder="e.g., PG-13, R, G" + /> +
+ + {/* Poster URL */} +
+ + handleInputChange(index, 'poster', e.target.value)} + className={styles.input} + disabled={isLoading} + placeholder="https://..." + /> +
+
+ + {/* Plot */} +
+ +