satupeta-main/app/services/base.py
2026-01-27 09:11:58 +07:00

205 lines
7.7 KiB
Python

from functools import lru_cache
from typing import Any, Dict, Generic, List, Tuple, Type, TypeVar, Union
from sqlalchemy import or_
from uuid6 import UUID
from app.core.database import Base
from app.core.exceptions import NotFoundException, UnprocessableEntity
from app.repositories import BaseRepository
ModelType = TypeVar("ModelType", bound=Base)
RepositoryType = TypeVar("RepositoryType", bound=BaseRepository)
class BaseService(Generic[ModelType, RepositoryType]):
"""Optimized base service dengan caching dan performance improvements."""
def __init__(self, model: Type[ModelType], repository: Type[RepositoryType]):
self.model_class = model
self.repository = repository
self._valid_columns = set(self.model_class.__table__.columns.keys())
self._has_soft_delete = hasattr(self.model_class, "is_deleted")
@lru_cache(maxsize=256)
def _parse_filter_item(self, filter_item: str) -> Tuple[str, str]:
"""Cache parsing filter."""
try:
col, value = filter_item.split("=", 1)
return col.strip(), value.strip()
except ValueError:
raise UnprocessableEntity(f"Invalid filter {filter_item} must be 'name=value'")
@lru_cache(maxsize=256)
def _parse_sort_item(self, sort_item: str) -> Tuple[str, str]:
"""Cache parsing sort."""
try:
col, order = sort_item.split(":", 1)
return col.strip(), order.strip().lower()
except ValueError:
raise UnprocessableEntity(f"Invalid sort {sort_item}. Must be 'name:asc' or 'name:desc'")
def _validate_column(self, col: str) -> None:
"""Validate column dengan cache."""
if col not in self._valid_columns:
raise UnprocessableEntity(f"Invalid column: {col}")
def _convert_value(self, col: str, value: str) -> Any:
"""Convert value ke tipe yang sesuai."""
if col == "id":
try:
return UUID(value)
except ValueError:
raise UnprocessableEntity(f"Invalid UUID value: {value}")
if isinstance(value, str) and value.lower() in {"true", "false", "t", "f"}:
return value.lower() in {"true", "t"}
if value.isdigit():
return int(value)
try:
return float(value)
except ValueError:
return value
def _build_filters(self, filters: Union[str, list[str]]) -> List:
"""Build filters dengan optimization."""
list_model_filters = []
if isinstance(filters, str):
filters = [filters]
if self._has_soft_delete:
filters.append("is_deleted=false")
for filter_item in filters:
if isinstance(filter_item, list):
or_conditions = []
for values in filter_item:
col, value = self._parse_filter_item(values)
self._validate_column(col)
converted_value = self._convert_value(col, value)
if isinstance(converted_value, bool):
or_conditions.append(getattr(self.model_class, col).is_(converted_value))
else:
or_conditions.append(getattr(self.model_class, col) == converted_value)
if or_conditions:
list_model_filters.append(or_(*or_conditions))
else:
col, value = self._parse_filter_item(filter_item)
self._validate_column(col)
converted_value = self._convert_value(col, value)
if isinstance(converted_value, bool):
list_model_filters.append(getattr(self.model_class, col).is_(converted_value))
else:
list_model_filters.append(getattr(self.model_class, col) == converted_value)
return list_model_filters
def _build_sort(self, sort: Union[str, list[str]]) -> List:
"""Build sort dengan optimization."""
if not sort:
return []
list_sort = []
if isinstance(sort, str):
sort = [sort]
for sort_item in sort:
col, order = self._parse_sort_item(sort_item)
self._validate_column(col)
if order == "asc":
list_sort.append(getattr(self.model_class, col).asc())
elif order == "desc":
list_sort.append(getattr(self.model_class, col).desc())
else:
raise UnprocessableEntity(f"Invalid sort order '{order}' for {col}")
return list_sort
async def find_by_id(self, id: UUID, relationships: List[str] = None) -> ModelType:
"""Find record by ID dengan optional eager loading."""
record = await self.repository.find_by_id(id, relationships=relationships)
if not record:
raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.")
return record
async def find_all(
self,
filters: Union[str, list[str]] = None,
sort: Union[str, list[str]] = None,
search: str = "",
group_by: str = None,
limit: int = 100,
offset: int = 0,
relationships: List[str] = None,
searchable_columns: List[str] = None,
) -> Tuple[List[ModelType], int]:
"""Optimized find_all."""
if group_by:
self._validate_column(group_by)
list_model_filters = self._build_filters(filters or [])
list_sort = self._build_sort(sort or [])
return await self.repository.find_all(
filters=list_model_filters,
sort=list_sort,
search=search,
group_by=group_by,
limit=limit,
offset=offset,
relationships=relationships,
searchable_columns=searchable_columns,
)
async def create(self, data: Dict[str, Any]) -> ModelType:
"""Create new record."""
return await self.repository.create(data)
async def update(self, id: UUID, data: Dict[str, Any], refresh: bool = True) -> ModelType:
"""Update existing record."""
# Check existence first
if not await self.repository.exists(id):
raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.")
updated = await self.repository.update(id, data, refresh=refresh)
if not updated:
raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.")
return updated
async def delete(self, id: UUID, permanent: bool = False) -> None:
"""Delete record dengan soft delete support."""
if not await self.repository.exists(id):
raise NotFoundException(f"{self.model_class.__name__} with UUID {id} not found.")
if self._has_soft_delete and not permanent:
delete_data = {"is_deleted": True}
if hasattr(self.model_class, "is_active"):
delete_data["is_active"] = False
await self.repository.update(id, delete_data, refresh=False)
else:
await self.repository.delete(id)
# Bulk operations
async def bulk_create(self, data_list: List[Dict[str, Any]], batch_size: int = 1000) -> List[ModelType]:
"""Bulk create dengan validation."""
return await self.repository.bulk_create(data_list, batch_size=batch_size, return_records=True)
async def exists_by_id(self, id: UUID) -> bool:
"""Check existence tanpa fetch object."""
return await self.repository.exists(id)
async def count_by_filters(self, filters: Union[str, list[str]] = None) -> int:
"""Count records dengan filters."""
list_model_filters = self._build_filters(filters or [])
return await self.repository.count(list_model_filters)