diff --git a/crud.py b/crud.py index 941a5d9..1db4814 100644 --- a/crud.py +++ b/crud.py @@ -5,6 +5,7 @@ from typing import Optional import httpx from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash +from lnbits.utils.cache import Cache from .models import ( Account, @@ -41,6 +42,17 @@ from .core.validation import ( db = Database("ext_castle") +# ===== CACHING ===== +# Cache for account and permission lookups to reduce DB queries +# TTLs: accounts=300s (5min), permissions=60s (1min) + +account_cache = Cache() # 5 minutes for accounts (rarely change) +permission_cache = Cache() # 1 minute for permissions (may change frequently) + +# Cache TTLs +ACCOUNT_CACHE_TTL = 300 # 5 minutes +PERMISSION_CACHE_TTL = 60 # 1 minute + # ===== ACCOUNT OPERATIONS ===== @@ -56,25 +68,57 @@ async def create_account(data: CreateAccount) -> Account: created_at=datetime.now(), ) await db.insert("accounts", account) + + # Invalidate cache for this account (Cache class doesn't have delete method, use pop) + account_cache._values.pop(f"account:id:{account_id}", None) + account_cache._values.pop(f"account:name:{data.name}", None) + return account async def get_account(account_id: str) -> Optional[Account]: - return await db.fetchone( + """Get account by ID with caching""" + cache_key = f"account:id:{account_id}" + + # Try cache first + cached = account_cache.get(cache_key) + if cached is not None: + return cached + + # Query DB + account = await db.fetchone( "SELECT * FROM accounts WHERE id = :id", {"id": account_id}, Account, ) + # Cache result (even if None) + account_cache.set(cache_key, account, ACCOUNT_CACHE_TTL) + + return account + async def get_account_by_name(name: str) -> Optional[Account]: - """Get account by name (hierarchical format)""" - return await db.fetchone( + """Get account by name (hierarchical format) with caching""" + cache_key = f"account:name:{name}" + + # Try cache first + cached = account_cache.get(cache_key) + if cached is not None: + return cached + + # Query DB + account = await db.fetchone( "SELECT * FROM accounts WHERE name = :name", {"name": name}, Account, ) + # Cache result (even if None) + account_cache.set(cache_key, account, ACCOUNT_CACHE_TTL) + + return account + async def get_all_accounts() -> list[Account]: return await db.fetchall( @@ -845,6 +889,10 @@ async def create_account_permission( }, ) + # Invalidate permission cache for this user (Cache class doesn't have delete method, use pop) + permission_cache._values.pop(f"permissions:user:{permission.user_id}", None) + permission_cache._values.pop(f"permissions:user:{permission.user_id}:{permission.permission_type.value}", None) + return permission @@ -875,9 +923,20 @@ async def get_account_permission(permission_id: str) -> Optional["AccountPermiss async def get_user_permissions( user_id: str, permission_type: Optional["PermissionType"] = None ) -> list["AccountPermission"]: - """Get all permissions for a specific user""" + """Get all permissions for a specific user with caching""" from .models import AccountPermission, PermissionType + # Build cache key + cache_key = f"permissions:user:{user_id}" + if permission_type: + cache_key += f":{permission_type.value}" + + # Try cache first + cached = permission_cache.get(cache_key) + if cached is not None: + return cached + + # Query DB if permission_type: rows = await db.fetchall( """ @@ -904,7 +963,7 @@ async def get_user_permissions( {"user_id": user_id, "now": datetime.now()}, ) - return [ + permissions = [ AccountPermission( id=row["id"], user_id=row["user_id"], @@ -918,6 +977,11 @@ async def get_user_permissions( for row in rows ] + # Cache result + permission_cache.set(cache_key, permissions, PERMISSION_CACHE_TTL) + + return permissions + async def get_account_permissions(account_id: str) -> list["AccountPermission"]: """Get all permissions for a specific account""" @@ -950,11 +1014,19 @@ async def get_account_permissions(account_id: str) -> list["AccountPermission"]: async def delete_account_permission(permission_id: str) -> None: """Delete (revoke) an account permission""" + # Get permission first to invalidate cache + permission = await get_account_permission(permission_id) + await db.execute( "DELETE FROM account_permissions WHERE id = :id", {"id": permission_id}, ) + # Invalidate permission cache for this user (Cache class doesn't have delete method, use pop) + if permission: + permission_cache._values.pop(f"permissions:user:{permission.user_id}", None) + permission_cache._values.pop(f"permissions:user:{permission.user_id}:{permission.permission_type.value}", None) + async def check_user_has_permission( user_id: str, account_id: str, permission_type: "PermissionType"