diff --git a/backend/app/app/api/api_v1/endpoints/expenses.py b/backend/app/app/api/api_v1/endpoints/expenses.py index e5db6cb..f21b4af 100644 --- a/backend/app/app/api/api_v1/endpoints/expenses.py +++ b/backend/app/app/api/api_v1/endpoints/expenses.py @@ -10,6 +10,7 @@ from app import crud, models, schemas from app.api import deps +from app.utilities.redis import invalidate_user_cache router = APIRouter() @@ -152,6 +153,10 @@ async def create_expense( expense = await crud.expense.create_with_owner( db=db, obj_in=expense_in, owner_id=current_user.id ) + + # Invalidate user's cached data since a new expense was created + await invalidate_user_cache(current_user.id) + return expense @@ -168,6 +173,10 @@ async def create_expenses_bulk( expenses = await crud.expense.create_multi_with_owner( db=db, obj_list=expenses_in, owner_id=current_user.id ) + + # Invalidate user's cached data since expenses were created + await invalidate_user_cache(current_user.id) + return expenses @@ -332,6 +341,9 @@ async def update_expense( amount=amount_difference, ) + # Invalidate user's cached data since expense was updated + await invalidate_user_cache(current_user.id) + return updated_expense @@ -391,6 +403,9 @@ async def delete_expense( amount=-expense.amount, ) + # Invalidate user's cached data since expense was deleted + await invalidate_user_cache(current_user.id) + return schemas.DeletionResponse(message=f"Item {id} deleted") @@ -476,6 +491,9 @@ async def delete_expenses_bulk( amount=-expense.amount, ) + # Invalidate user's cached data since expenses were deleted + await invalidate_user_cache(current_user.id) + return schemas.BulkDeletionResponse( message=f"Deleted {len(removed_expenses)} expenses", deleted_ids=[e.id for e in removed_expenses], diff --git a/backend/app/app/api/api_v1/endpoints/incomes.py b/backend/app/app/api/api_v1/endpoints/incomes.py index d87b485..f3b9673 100644 --- a/backend/app/app/api/api_v1/endpoints/incomes.py +++ b/backend/app/app/api/api_v1/endpoints/incomes.py @@ -10,6 +10,7 @@ from app import crud, models, schemas from app.api import deps from app.api.deps import DateFilterType +from app.utilities.redis import invalidate_user_cache router = APIRouter() @@ -143,6 +144,10 @@ async def create_income( income = await crud.income.create_with_owner( db=db, obj_in=income_in, owner_id=current_user.id ) + + # Invalidate user's cached data since a new income was created + await invalidate_user_cache(current_user.id) + return income @@ -159,6 +164,10 @@ async def create_incomes_bulk( incomes = await crud.income.create_multi_with_owner( db=db, obj_list=incomes_in, owner_id=current_user.id ) + + # Invalidate user's cached data since incomes were created + await invalidate_user_cache(current_user.id) + return incomes @@ -311,6 +320,9 @@ async def update_income( amount=amount_difference, ) + # Invalidate user's cached data since income was updated + await invalidate_user_cache(current_user.id) + return updated_income @@ -369,6 +381,8 @@ async def delete_income( ) await db.commit() + # Invalidate user's cached data since income was deleted + await invalidate_user_cache(current_user.id) return schemas.DeletionResponse(message=f"Item {id} deleted") @@ -452,6 +466,9 @@ async def delete_incomes_bulk( amount=-income.amount, ) + # Invalidate user's cached data since incomes were deleted + await invalidate_user_cache(current_user.id) + return schemas.BulkDeletionResponse( message=f"Deleted {len(removed_incomes)} incomes", deleted_ids=[i.id for i in removed_incomes], diff --git a/backend/app/app/api/api_v1/endpoints/transfers.py b/backend/app/app/api/api_v1/endpoints/transfers.py index 6b43fda..25f14ee 100644 --- a/backend/app/app/api/api_v1/endpoints/transfers.py +++ b/backend/app/app/api/api_v1/endpoints/transfers.py @@ -9,6 +9,7 @@ from app import crud, models, schemas from app.api import deps from app.api.deps import DateFilterType +from app.utilities.redis import invalidate_user_cache router = APIRouter() @@ -146,6 +147,9 @@ async def create_transfer( if transfer is None: raise HTTPException(status_code=400, detail="Account not found") + # Invalidate user's cached data since a new transfer was created + await invalidate_user_cache(current_user.id) + return transfer @@ -260,6 +264,9 @@ async def update_transfer( amount=amount_difference, ) + # Invalidate user's cached data since transfer was updated + await invalidate_user_cache(current_user.id) + return updated_transfer @router.delete("/{id}", response_model=schemas.DeletionResponse) @@ -291,4 +298,7 @@ async def delete_transfer( amount=-transfer.amount ) + # Invalidate user's cached data since transfer was deleted + await invalidate_user_cache(current_user.id) + return schemas.DeletionResponse(message=f"Item {id} deleted") diff --git a/backend/app/app/api/api_v2/endpoints/data.py b/backend/app/app/api/api_v2/endpoints/data.py index 28512e1..7dcf599 100644 --- a/backend/app/app/api/api_v2/endpoints/data.py +++ b/backend/app/app/api/api_v2/endpoints/data.py @@ -20,6 +20,7 @@ get_df, transaction_charts, ) +from app.utilities.redis import get_user_data, store_user_data router = APIRouter() @@ -101,8 +102,13 @@ async def get_all_data( current_user: models.User = Depends(deps.get_current_active_user), ) -> Any: """ - Massive data retrieval for the dashboard. + Massive data retrieval for the dashboard with intelligent caching. """ + # Try to get cached data first + cached_data = await get_user_data(current_user.id, date_filter_type.value, date) + if cached_data: + return cached_data + start_date: Date | None = None end_date: Date | None = None results = None @@ -223,7 +229,7 @@ async def get_all_data( ) = results if incomes_actual == [] and expenses_actual == []: - return { + empty_response = { "currency": current_user.country, "language": current_user.country, "accounts": jsonable_encoder(accounts), @@ -242,6 +248,11 @@ async def get_all_data( "accounts": [], }, } + + # Cache the empty response as well + await store_user_data(current_user.id, date_filter_type.value, date, empty_response) + + return empty_response dfs = get_df( expenses=jsonable_encoder(expenses_actual), @@ -282,7 +293,7 @@ async def get_all_data( incomes_df=dfs["incomes"], expenses_df=dfs["expenses"], transfers_df=dfs["transfers"] ) - return { + response_data = { "currency": current_user.country, "language": current_user.country, "accounts": jsonable_encoder(accounts), @@ -301,6 +312,11 @@ async def get_all_data( "accounts": account_chart, }, } + + # Cache the response data for future requests + await store_user_data(current_user.id, date_filter_type.value, date, response_data) + + return response_data # @router.post("/", response_model=schemas.Expense) diff --git a/backend/app/app/utilities/redis.py b/backend/app/app/utilities/redis.py index b9ac594..735259d 100644 --- a/backend/app/app/utilities/redis.py +++ b/backend/app/app/utilities/redis.py @@ -57,3 +57,50 @@ async def delete_transaction(transaction_id: str): except Exception as e: logging.error(f"Error deleting transaction {transaction_id}: {str(e)}") return False + +# User data caching functionality +async def store_user_data(user_id: int, date_filter_type: str, date: str, data, expire_time=1800): + """Store get_all_data response in Redis with expiration time (30 min default)""" + try: + cache_key = f"user_data:{user_id}:{date_filter_type}:{date}" + + await r.set(cache_key, json.dumps(data, cls=DateEncoder)) + await r.expire(cache_key, expire_time) + + logging.info(f"Cached user data for user {user_id} with key {cache_key}") + return True + except Exception as e: + logging.error(f"Redis error storing user data for user {user_id}: {str(e)}") + return False + +async def get_user_data(user_id: int, date_filter_type: str, date: str): + """Retrieve cached get_all_data response from Redis""" + try: + cache_key = f"user_data:{user_id}:{date_filter_type}:{date}" + cached_data = await r.get(cache_key) + + if cached_data: + logging.info(f"Cache hit for user {user_id} with key {cache_key}") + return json.loads(cached_data) + + logging.info(f"Cache miss for user {user_id} with key {cache_key}") + return None + except (Exception, json.JSONDecodeError) as e: + logging.error(f"Error retrieving user data for user {user_id}: {str(e)}") + return None + +async def invalidate_user_cache(user_id: int): + """Invalidate all cached data for a specific user when they add/update/delete data""" + try: + # Get all keys that match the user's cache pattern + pattern = f"user_data:{user_id}:*" + keys = await r.keys(pattern) + + if keys: + await r.delete(*keys) + logging.info(f"Invalidated {len(keys)} cache entries for user {user_id}") + + return True + except Exception as e: + logging.error(f"Error invalidating cache for user {user_id}: {str(e)}") + return False