"""User repository — adapter over ``AsyncSession`` implementing ``app.domain.ports.UserRepository``. Translates between ORM rows (``UserModel``) and domain entities (``User`` / ``Credentials``). The domain never sees ORM objects. """ import uuid from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.domain.entities import Credentials, SubsonicCredentials, User from app.domain.errors import NotFoundError from app.infrastructure.db.models import UserModel def _to_entity(row: UserModel) -> User: return User( id=row.id, username=row.username, is_superuser=row.is_superuser, is_active=row.is_active, created_at=row.created_at, updated_at=row.updated_at, ) class SqlAlchemyUserRepository: def __init__(self, session: AsyncSession) -> None: self._session = session async def _get_row(self, user_id: uuid.UUID) -> UserModel: row = await self._session.get(UserModel, user_id) if row is None: raise NotFoundError("User not found.") return row async def get_by_id(self, user_id: uuid.UUID) -> User | None: row = await self._session.get(UserModel, user_id) return _to_entity(row) if row is not None else None async def get_credentials_by_username(self, username: str) -> Credentials | None: row = ( await self._session.execute(select(UserModel).where(UserModel.username == username)) ).scalar_one_or_none() if row is None: return None return Credentials(user=_to_entity(row), password_hash=row.password_hash) async def add(self, *, username: str, password_hash: str, is_superuser: bool) -> User: row = UserModel( username=username, password_hash=password_hash, is_superuser=is_superuser, is_active=True, ) self._session.add(row) await self._session.flush() await self._session.refresh(row) return _to_entity(row) async def list(self, *, limit: int, offset: int) -> list[User]: rows = ( await self._session.execute( select(UserModel).order_by(UserModel.created_at).limit(limit).offset(offset) ) ).scalars() return [_to_entity(row) for row in rows] async def set_password_hash(self, user_id: uuid.UUID, password_hash: str) -> None: row = await self._get_row(user_id) row.password_hash = password_hash await self._session.flush() async def set_superuser(self, user_id: uuid.UUID, is_superuser: bool) -> User: row = await self._get_row(user_id) row.is_superuser = is_superuser await self._session.flush() await self._session.refresh(row) return _to_entity(row) async def set_active(self, user_id: uuid.UUID, is_active: bool) -> User: row = await self._get_row(user_id) row.is_active = is_active await self._session.flush() await self._session.refresh(row) return _to_entity(row) async def count(self) -> int: return ( await self._session.execute(select(func.count()).select_from(UserModel)) ).scalar_one() async def get_subsonic_credentials_by_username( self, username: str ) -> SubsonicCredentials | None: row = ( await self._session.execute(select(UserModel).where(UserModel.username == username)) ).scalar_one_or_none() if row is None: return None return SubsonicCredentials(user=_to_entity(row), password_enc=row.subsonic_password_enc) async def get_subsonic_password_enc(self, user_id: uuid.UUID) -> str | None: row = await self._get_row(user_id) return row.subsonic_password_enc async def set_subsonic_password_enc(self, user_id: uuid.UUID, password_enc: str) -> None: row = await self._get_row(user_id) row.subsonic_password_enc = password_enc await self._session.flush()