205 lines
7.7 KiB
Python
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)
|