Files
docs/templates/app/infra/adapters/service.py

128 lines
3.8 KiB
Python

"""# Summary
Generic CRUD service adaptor over a Mongo repository.
Bridge pydantic models and mongo-ops documents, exposing create, read, update,
patch, delete, and search operations for every domain that needs them.
"""
from typing import Generic, Type, TypeVar
from infra.db.repos import mongo
from pydantic import BaseModel
from mongo_ops import BaseDocument
from mongo_ops.models import PyObjectId
Model = TypeVar("Model", bound=BaseModel)
Schema = TypeVar("Schema", bound=BaseDocument)
class CrudsServiceAdaptor(Generic[Model, Schema]):
"""Adapt a generic Mongo repository to a model/schema CRUD service.
Attributes:
repo (mongo.BaseRepository): Repository backing all CRUD operations.
model (Type[Model]): Pydantic model type handled by this service.
schema (Type[Schema]): Document schema type persisted by the repository.
"""
repo: mongo.BaseRepository
model: Type[Model]
schema: Type[Schema]
def __init__(self, *, repo=None):
"""Initialize the adaptor with a backing repository.
Args:
repo (mongo.BaseRepository): Repository used for all operations.
"""
self.repo = repo
@staticmethod
def model_to_dict(model_obj: Model) -> dict:
"""Serialize a model into a sparse dictionary.
Args:
model_obj (Model): Model instance to serialize.
Returns:
dict: Fields that are set and not ``None``.
"""
return model_obj.model_dump(exclude_unset=True, exclude_none=True)
async def create(self, data: Model | dict) -> Schema:
"""Create a document from a model or raw mapping.
Args:
data (Model | dict): Model instance or field mapping to persist.
Returns:
Schema: Persisted document.
"""
if isinstance(data, dict):
data = self.schema(**data)
return await self.repo.create(data)
async def read(self, id: PyObjectId) -> Schema | None:
"""Read a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
Schema | None: Matching document, or ``None`` when absent.
"""
return await self.repo.get_by_id(id)
async def update(self, id: PyObjectId, data: Model | dict) -> Schema | None:
"""Replace a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (Model | dict): Replacement model or field mapping.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.update(
id,
data if isinstance(data, dict) else self.model_to_dict(data),
)
async def patch(self, id: PyObjectId, data: dict) -> Schema | None:
"""Partially update a document by primary key.
Args:
id (PyObjectId): Document identifier.
data (dict): Fields to patch.
Returns:
Schema | None: Updated document, or ``None`` when absent.
"""
return await self.repo.patch(id, data)
async def delete(self, id: PyObjectId) -> bool:
"""Delete a document by primary key.
Args:
id (PyObjectId): Document identifier.
Returns:
bool: ``True`` when a document was deleted.
"""
return await self.repo.delete(id)
async def search(self, filter: dict | None = None, *args, **kwargs) -> list[Schema]:
"""List documents matching an optional filter.
Args:
filter (dict | None): Query filter passed to the repository.
Returns:
list[Schema]: Matching documents.
Notes:
Extra positional and keyword arguments are forwarded to the
repository.
"""
return await self.repo.get_many(filter=filter, *args, **kwargs)