"""Redis caching for entity permission visibility. Extracted from entity_permission_service.py for modularity. """ from __future__ import annotations import json import uuid import redis.asyncio as aioredis from sqlalchemy.ext.asyncio import AsyncSession from app.services.permission_resolver import get_visible_ids CACHE_TTL = 300 # 5 minutes CACHE_PREFIX = "ep_vis" # entity permission visibility async def _invalidate_user_cache( redis: aioredis.Redis | None, tenant_id: uuid.UUID, user_id: uuid.UUID, entity_type: str, ) -> None: """Invalidate the visibility cache for a user + entity type.""" if redis is None: return cache_key = f"{CACHE_PREFIX}:{user_id}:{tenant_id}:{entity_type}" await redis.delete(cache_key) async def get_cached_visible_ids( db: AsyncSession, redis: aioredis.Redis, tenant_id: uuid.UUID, user_id: uuid.UUID, entity_type: str, ) -> tuple[set[uuid.UUID], dict[uuid.UUID, str]]: """Get visible IDs from Redis cache or resolve from DB. Cache key: ep_vis:{user_id}:{tenant_id}:{entity_type} Cache value: JSON {visible_ids: [str], access_map: {str: str}} TTL: 5 minutes """ cache_key = f"{CACHE_PREFIX}:{user_id}:{tenant_id}:{entity_type}" raw = await redis.get(cache_key) if raw is not None: data = json.loads(raw) visible = {uuid.UUID(eid) for eid in data.get("visible_ids", [])} access_map = {uuid.UUID(eid): level for eid, level in data.get("access_map", {}).items()} return visible, access_map # Cache miss — resolve from DB visible, access_map = await get_visible_ids(db, tenant_id, user_id, entity_type) # Store in cache cache_data = { "visible_ids": [str(eid) for eid in visible], "access_map": {str(eid): level for eid, level in access_map.items()}, } await redis.setex(cache_key, CACHE_TTL, json.dumps(cache_data)) return visible, access_map async def invalidate_all_user_entity_cache( redis: aioredis.Redis, tenant_id: uuid.UUID, user_id: uuid.UUID, ) -> None: """Invalidate all entity permission caches for a user.""" pattern = f"{CACHE_PREFIX}:{user_id}:{tenant_id}:*" batch_size = 200 cursor: int | bytes | str = 0 while True: cursor, keys = await redis.scan(cursor=cursor, match=pattern, count=batch_size) if keys: await redis.delete(*keys) if int(cursor) == 0: break