diff --git a/backend/api/__init__.py b/backend/api/__init__.py deleted file mode 100644 index 343e63f..0000000 --- a/backend/api/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import asyncpg -from asyncpg.pool import Pool - -from api.utils import log_api_message -from settings import DB_URL, ENV - - -class DummyPool: - async def close(self): - pass - - async def fetch(self, *args, **kwargs): - pass - - async def fetchval(self, *args, **kwargs): - pass - - -async def DbSession() -> Pool: - if ENV == "test": - # see the a mock pool implemented in specs/resolvers/__init__.py - return DummyPool() - - try: - pool = await asyncpg.create_pool(DB_URL) - return pool - except Exception as e: - message = f"Error initializing database connection: {e}" - log_api_message(__name__, message) - raise diff --git a/backend/api/helpers.py b/backend/api/helpers.py deleted file mode 100644 index 942f716..0000000 --- a/backend/api/helpers.py +++ /dev/null @@ -1,15 +0,0 @@ -from datetime import datetime - -from api.resolvers.constants import INVALID_DATE_RANGE - - -def check_date_validity(checkin_date: datetime, checkout_date: datetime) -> None: - if checkout_date <= checkin_date: - raise ValueError(INVALID_DATE_RANGE) - - -def calculate_total_charge( - daily_rate: int, checkin_date: datetime, checkout_date: datetime -) -> int: - calculated_days = (checkout_date - checkin_date).days - return daily_rate * calculated_days diff --git a/backend/api/models.py b/backend/api/models.py deleted file mode 100644 index 05521c2..0000000 --- a/backend/api/models.py +++ /dev/null @@ -1,27 +0,0 @@ -import datetime -from typing import Any, Dict - -from pydantic import BaseModel - -from api import utils - - -class Room(BaseModel): - id: str - num_beds: int - allow_smoking: bool - daily_rate: int - cleaning_fee: int - - -class Reservation(BaseModel): - id: int - room_id: str - checkin_date: datetime.datetime - checkout_date: datetime.datetime - total_charge: int - - -class ApiData(BaseModel): - data: Dict[str, Any] - status: utils.StatusCode diff --git a/backend/api/resolvers/__init__.py b/backend/api/resolvers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/resolvers/constants.py b/backend/api/resolvers/constants.py deleted file mode 100644 index 9892509..0000000 --- a/backend/api/resolvers/constants.py +++ /dev/null @@ -1,4 +0,0 @@ -INVALID_DATE_RANGE = "Check-out date must be greater than check-in date." -INVALID_DATE_CHECK_IN = "Check-in date must be greater than today." -INVALID_DATE_OVERLAP = "Dates overlap with an existing reservations." -RESERVATION_EXISTS = "Reservation already exists." diff --git a/backend/api/resolvers/data.py b/backend/api/resolvers/data.py deleted file mode 100644 index 93b3a76..0000000 --- a/backend/api/resolvers/data.py +++ /dev/null @@ -1,124 +0,0 @@ -from datetime import datetime -from typing import Any, Dict - -import api.utils as utils -from api.helpers import calculate_total_charge -from api.models import Reservation, Room -from api.resolvers.constants import INVALID_DATE_CHECK_IN, INVALID_DATE_OVERLAP - - -async def create_reservation( - db, room_id: str, checkin_date: datetime, checkout_date: datetime -) -> Dict[str, Any]: - available = await is_room_available( - db, room_id=room_id, checkin_date=checkin_date, checkout_date=checkout_date - ) - if not available["success"]: - for message in available["errors"]: - utils.log_api_message(__name__, message) - return {"success": False, "errors": available["errors"]} - - room_result = await fetch_room(db, room_id) - room = room_result["room"][0] - daily_rate = room.daily_rate - total_charge = calculate_total_charge(daily_rate, checkin_date, checkout_date) - - reservation_insert_query = """ - INSERT INTO reservations - (room_id, checkin_date, checkout_date, total_charge) - VALUES ($1, $2, $3, $4) - RETURNING id - """ - await db.fetchval( - reservation_insert_query, room_id, checkin_date, checkout_date, total_charge - ) - reservations = await fetch_all_rows(db, Reservation) - return reservations - - -async def delete_reservation(db, reservation_id: str) -> Dict[str, Any]: - reservation = await fetch_reservation(db, reservation_id) - if reservation: - await db.execute("DELETE FROM reservations WHERE id = $1", reservation_id) - return await fetch_all_rows(db, Reservation) - else: - errors = ["Reservation not found"] - return {"success": False, "errors": errors, "reservations": None} - - -async def is_room_available( - db, room_id: str, checkin_date: datetime, checkout_date: datetime -) -> Dict[str, Any]: - try: - if checkin_date < datetime.now(): - message: str = INVALID_DATE_CHECK_IN - utils.log_api_message(__name__, message) - return {"success": False, "errors": [message]} - - query = """ - SELECT COUNT(*) FROM reservations - WHERE room_id = $1 - AND ( - (checkin_date >= $2 AND checkin_date < $3) - OR (checkout_date > $2 AND checkout_date <= $3) - OR (checkin_date <= $2 AND checkout_date >= $3) - ) - """ - count = await db.fetchval(query, room_id, checkin_date, checkout_date) - - if count == 0: - return {"success": True, "errors": None} - else: - return {"success": False, "errors": [INVALID_DATE_OVERLAP]} - - except Exception as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - - -async def fetch_all_rows(db, entity_type) -> Dict[str, Any]: - table_name = entity_type.__name__.lower() + "s" - query = f"SELECT * FROM {table_name}" - rows = await db.fetch(query) - if rows: - entities = [entity_type(**dict(row)) for row in rows] - return {"success": True, f"{table_name}": entities} - else: - raise ValueError("No reserved rooms found") - - -async def fetch_by_id(db, entity_type, id) -> Dict[str, Any]: - table_name = entity_type.__name__.lower() + "s" - query = f"SELECT * FROM {table_name} WHERE id = $1" - rows = await db.fetch(query, id) - if rows: - entities = [entity_type(**dict(row)) for row in rows] - return {"success": True, f"{entity_type.__name__.lower()}": entities} - else: - return {"success": False, f"{entity_type.__name__.lower()}": None} - - -async def fetch_available_rooms(db, checkin_date, checkout_date) -> Dict[str, Any]: - rooms = await fetch_all_rows(db, Room) - available_rooms = [ - room - for room in rooms - if await is_room_available(db, room.id, checkin_date, checkout_date) - ] - return {"success": True, "rooms": available_rooms} - - -async def fetch_room(db, room_id: str) -> Room: - result = await fetch_by_id(db, Room, id=room_id) - if result["success"]: - return result - else: - raise ValueError(f"Room with id {room_id} not found") - - -async def fetch_reservation(db, reservation_id: id): - result = await fetch_by_id(db, Reservation, id=reservation_id) - if result["success"]: - return result - else: - raise ValueError(f"Reservation with id of {reservation_id} not found") diff --git a/backend/api/resolvers/mutations.py b/backend/api/resolvers/mutations.py deleted file mode 100644 index fd0e0c7..0000000 --- a/backend/api/resolvers/mutations.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import Any, Dict, cast - -import api.utils as utils -from api import DbSession -from api.helpers import check_date_validity -from api.resolvers.data import create_reservation, delete_reservation - - -async def create_reservation_resolver(obj, info, input: dict) -> Dict[str, Any]: - db = await DbSession() - try: - room_id = cast(str, input.get("room_id")) - checkin_date = utils.convert_to_local_date_from_str( - str(input.get("checkin_date")) - ) - checkout_date = utils.convert_to_local_date_from_str( - str(input.get("checkout_date")) - ) - - check_date_validity(checkin_date, checkout_date) - - result = await create_reservation(db, room_id, checkin_date, checkout_date) - return result - except ValueError as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() - - -async def delete_reservation_resolver(obj, info, reservationId: int) -> Dict[str, Any]: - db = await DbSession() - try: - result = await delete_reservation(db, reservationId) - return result - except ValueError as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() diff --git a/backend/api/resolvers/queries.py b/backend/api/resolvers/queries.py deleted file mode 100644 index e9c0435..0000000 --- a/backend/api/resolvers/queries.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Any, Dict - -import api.utils as utils -from api import DbSession -from api.models import Reservation, Room -from api.resolvers.data import fetch_all_rows, fetch_available_rooms, fetch_reservation - - -async def get_reservation_resolver(obj, info, id) -> Dict[str, Any]: - try: - db = await DbSession() - - result = await fetch_reservation(db, id) - return result - except ValueError as error: - message = f"Error retrieving reservation: {str(error)}" - utils.log_api_message(__name__, message) - return { - "success": False, - "errors": [message], - } - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() - - -async def get_all_reservations_resolver(obj, info) -> Dict[str, Any]: - db = await DbSession() - try: - return await fetch_all_rows(db, Reservation) - except ValueError as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() - - -async def get_all_rooms_resolver(obj, info) -> Dict[str, Any]: - db = await DbSession() - try: - return await fetch_all_rows(db, Room) - except ValueError as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() - - -async def get_available_rooms_resolver(obj, info, input: dict) -> Dict[str, Any]: - db = await DbSession() - try: - checkin_date = utils.convert_to_local_date_from_str( - str(input.get("checkin_date")) - ) - checkout_date = utils.convert_to_local_date_from_str( - str(input.get("checkout_date")) - ) - - return await fetch_available_rooms(db, checkin_date, checkout_date) - except ValueError as error: - utils.log_api_message(__name__, str(error)) - return {"success": False, "errors": [str(error)]} - except Exception as e: - error = f"Unexpected error: {str(e)}" - utils.log_api_message(__name__, f"Unexpected error: {error}") - return {"success": False, "errors": [error]} - finally: - await db.close() diff --git a/backend/api/utils.py b/backend/api/utils.py deleted file mode 100644 index 81c650f..0000000 --- a/backend/api/utils.py +++ /dev/null @@ -1,70 +0,0 @@ -import inspect -import logging -from datetime import datetime, timezone -from enum import Enum -from typing import cast - -from fastapi import status - -from settings import API_NAME, API_PORT, DB_URL, ENV, IS_DEBUG - -Logger = logging.getLogger() - - -def api_port() -> int: - return cast(int, API_PORT) - - -def is_debug() -> bool: - return cast(bool, IS_DEBUG) - - -def runtime_environment() -> str: - return str(ENV) - - -def database_host() -> str: - return str(DB_URL) - - -def convert_to_local_date(dt_utc: datetime) -> datetime: - return dt_utc.astimezone(timezone.utc).replace(tzinfo=None) - - -def convert_to_local_date_from_str(date_str: str) -> datetime: - dt_utc = datetime.strptime(date_str, "%Y-%m-%d") - return dt_utc.astimezone(timezone.utc).replace(tzinfo=None) - - -def get_calling_function_name() -> str: - frame = inspect.currentframe() - - calling_frame = frame.f_back if frame is not None else None - return calling_frame.f_code.co_name if calling_frame is not None else "" - - -def setup_file_logger() -> None: - current_date = datetime.now().strftime("%Y%m%d") - log_file = f"logs/{current_date}.log" - logging.basicConfig( - filename=log_file, - level=logging.INFO, - ) - - start_message = f"🚀 {API_NAME} ready at localhost:{API_PORT}/{ENV}/graphql" - Logger.info(start_message) - - -def log_api_message(module_name: str, message: str) -> None: - caller = get_calling_function_name() - Logger.info(f"{module_name}.{caller} message(s):\n\t{message}") - - -def logger_exit_message() -> None: - Logger.info(f"🛑 {API_NAME} stopped!") - - -class StatusCode(str, Enum): - OK = status.HTTP_200_OK - NOT_FOUND = status.HTTP_404_NOT_FOUND - BAD_REQUEST = status.HTTP_400_BAD_REQUEST diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py deleted file mode 100644 index 03cb89a..0000000 --- a/backend/routes/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import APIRouter, FastAPI - - -def create_app(*routes): - app = FastAPI() - routers = APIRouter() - for route in routes: - routers.include_router(route.router) - - app.include_router(routers) - return app diff --git a/backend/routes/about.py b/backend/routes/about.py deleted file mode 100644 index 22f86ca..0000000 --- a/backend/routes/about.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import APIRouter - -import api.utils as utils -from api.models import ApiData -from settings import API_NAME - - -class AboutRoute: - def __init__(self): - self.router = APIRouter() - self.prefix = f"/{utils.runtime_environment()}/about" - self._setup_routes() - - def _setup_routes(self): - @self.router.get(self.prefix, tags=["about"]) - async def about_handler() -> ApiData: - try: - desc = """This is a GraphQL API that allows you to create and - list reservations as well as the ability to list available rooms - for a given date range.""" - data = {"name": f"{API_NAME}", "description": desc} - response = ApiData(data=data, status=utils.StatusCode.OK) - except Exception as error: - messages = str(error) - utils.log_api_message(__name__, messages) - data = {"success": False, "errors": messages} - response = ApiData(data=data, status=utils.StatusCode.BAD_REQUEST) - return response diff --git a/backend/routes/graphql.py b/backend/routes/graphql.py deleted file mode 100644 index f3474ee..0000000 --- a/backend/routes/graphql.py +++ /dev/null @@ -1,66 +0,0 @@ -import json - -from ariadne import ( - ObjectType, - load_schema_from_path, - make_executable_schema, - snake_case_fallback_resolvers, -) -from ariadne.asgi import GraphQL -from fastapi import APIRouter, Request, Response - -import api.utils as utils -from api.models import ApiData -from api.resolvers.mutations import ( - create_reservation_resolver, - delete_reservation_resolver, -) -from api.resolvers.queries import ( - get_all_reservations_resolver, - get_all_rooms_resolver, - get_available_rooms_resolver, - get_reservation_resolver, -) - -query = ObjectType("Query") -query.set_field("getReservation", get_reservation_resolver) -query.set_field("getAllReservations", get_all_reservations_resolver) -query.set_field("getAllRooms", get_all_rooms_resolver) -query.set_field("getAvailableRooms", get_available_rooms_resolver) - -mutation = ObjectType("Mutation") -mutation.set_field("createReservation", create_reservation_resolver) -mutation.set_field("deleteReservation", delete_reservation_resolver) - -type_defs = load_schema_from_path("schema.graphql") -schema = make_executable_schema( - type_defs, query, mutation, snake_case_fallback_resolvers -) - -graphql_app = GraphQL(schema, debug=utils.is_debug()) - - -class GraphqlRoute: - def __init__(self): - self.router = APIRouter() - self.prefix = f"/{utils.runtime_environment()}/graphql" - self._setup_routes() - - def _setup_routes(self): - @self.router.post(self.prefix, tags=["graphql"]) - async def graphql_handler(request: Request) -> ApiData: - try: - result = await graphql_app.handle_request(request) - result = result.body.decode() - inner_data = json.loads(result).get("data", {}) - response = ApiData(data=inner_data, status=utils.StatusCode.OK) - except Exception as error: - messages = str(error) - utils.log_api_message(__name__, messages) - data = {"success": False, "errors": messages} - response = ApiData(data=data, status=utils.StatusCode.BAD_REQUEST) - return response - - @self.router.get(self.prefix, tags=["playground"]) - async def playground_handler(request: Request) -> Response: - return await graphql_app.handle_request(request) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 02138e6..05fdfac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "frontend", "dependencies": { "@reduxjs/toolkit": "^2.0.1", "axios": "^1.6.7",