import logging from typing import Annotated, List, Type, TypeVar from fastapi import Depends, Query from pydantic.types import Json, constr from sqlalchemy import Select, desc, func, or_ from src.database.schema import CommonParams from .core import DbSession log = logging.getLogger(__name__) # allows only printable characters QueryStr = constr(pattern=r"^[ -~]+$", min_length=1) def common_parameters( db_session: DbSession, # type: ignore current_user: QueryStr = Query(None, alias="currentUser"), # type: ignore page: int = Query(1, gt=0, le=1000000), items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, le=1000000), query_str: QueryStr = Query(None, alias="q"), # type: ignore filter_spec: QueryStr = Query(None, alias="filter"), # type: ignore sort_by: List[str] = Query([], alias="sortBy[]"), descending: List[bool] = Query([], alias="descending[]"), exclude: List[str] = Query([], alias="exclude[]"), all: int = Query(0, ge=0, le=1000000), ): return { "db_session": db_session, "page": page, "items_per_page": items_per_page, "query_str": query_str, "filter_spec": filter_spec, "sort_by": sort_by, "descending": descending, "current_user": current_user, "all": bool(all), } CommonParameters = Annotated[ dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool, Depends(common_parameters), ] T = TypeVar("T", bound=CommonParams) def get_params_factory(model_type: Type[T]): async def wrapper( db_session: DbSession, params: Annotated[model_type, Query()] # type: ignore ): res = params.model_dump() return { "db_session": db_session, "all": params.is_all, **res } return wrapper def search(*, query_str: str, query: Query, model, sort=False): """Perform a search based on the query.""" search_model = model if not query_str.strip(): return query search = [] if hasattr(search_model, "search_vector"): vector = search_model.search_vector search.append(vector.op("@@")(func.tsq_parse(query_str))) if hasattr(search_model, "name"): search.append( search_model.name.ilike(f"%{query_str}%"), ) search.append(search_model.name == query_str) if not search: raise Exception(f"Search not supported for model: {model}") query = query.filter(or_(*search)) if sort: query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str)))) return query.params(term=query_str) async def search_filter_sort_paginate( db_session: DbSession, model, query_str: str = None, filter_spec: str | dict | None = None, page: int = 1, items_per_page: int = 5, sort_by: List[str] = None, descending: List[bool] = None, current_user: str = None, exclude: List[str] = None, all: bool = False, ): """Common functionality for searching, filtering, sorting, and pagination.""" # Check if model is Select if not isinstance(model, Select): query = Select(model) else: query = model if query_str: sort = False if sort_by else True query = search(query_str=query_str, query=query, model=model, sort=sort) # Get total count count_query = Select(func.count()).select_from(query.subquery()) total = await db_session.scalar(count_query) if all: result = await db_session.execute(query) items = result.scalars().all() return { "items": items, "itemsPerPage": total, "page": 1, "total": total, "totalPages": 1, } query = query.offset((page - 1) * items_per_page).limit(items_per_page) result = await db_session.execute(query) items = result.scalars().all() return { "items": items, "itemsPerPage": items_per_page, "page": page, "total": total, "totalPages": (total + items_per_page - 1) // items_per_page, }