Loading src/open_exposure_gateway/adapters/database/mappers.py +21 −0 Changes for src/open_exposure_gateway/adapters/database/mappers.py: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ from open_exposure_gateway.adapters.database.sql import ( AppDeploymentRow, AppInstanceRow, AppRegistrationRow, CallbackCredentialRow, CallbackDeliveryRow, CallbackRegistrationRow, OperationRow, Loading @@ -13,6 +14,7 @@ from open_exposure_gateway.domain.models import ( AppDeployment, AppInstance, AppRegistration, CallbackCredential, CallbackDelivery, CallbackRegistration, Operation, Loading Loading @@ -203,6 +205,25 @@ class CallbackRegistrationMapper: ) class CallbackCredentialMapper: @staticmethod def to_domain(row: CallbackCredentialRow) -> CallbackCredential: return CallbackCredential( callback_registration_id=row.callback_registration_id, encrypted_token=row.encrypted_token, expires_at=row.expires_at, created_at=row.created_at, ) @staticmethod def to_row(domain: CallbackCredential) -> CallbackCredentialRow: return CallbackCredentialRow( callback_registration_id=domain.callback_registration_id, encrypted_token=domain.encrypted_token, expires_at=domain.expires_at, ) class CallbackDeliveryMapper: @staticmethod def to_domain(row: CallbackDeliveryRow) -> CallbackDelivery: Loading src/open_exposure_gateway/adapters/database/repos/callback_credentials.py 0 → 100644 +50 −0 Changes for src/open_exposure_gateway/adapters/database/repos/callback_credentials.py: 50 added lines, 0 removed lines. Original line number Diff line number Diff line from datetime import datetime from typing import Any, cast from uuid import UUID from sqlalchemy import CursorResult, delete, select from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import CallbackCredentialMapper from open_exposure_gateway.adapters.database.sql import CallbackCredentialRow from open_exposure_gateway.domain.models import CallbackCredential from open_exposure_gateway.ports.database.callbacks import CallbackCredentialRepository class SqlCallbackCredentialRepository(CallbackCredentialRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def get_by_registration_id( self, callback_registration_id: UUID ) -> CallbackCredential | None: stmt = select(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == callback_registration_id ) row = await self._session.scalar(stmt) return CallbackCredentialMapper.to_domain(row) if row is not None else None async def save(self, callback_credential: CallbackCredential) -> CallbackCredential: merged = await self._session.merge(CallbackCredentialMapper.to_row(callback_credential)) await self._session.flush() stmt = select(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == merged.callback_registration_id ) row = await self._session.scalar(stmt) if row is None: raise RuntimeError("Saved callback credential could not be reloaded") return CallbackCredentialMapper.to_domain(row) async def delete_by_registration_id(self, callback_registration_id: UUID) -> None: stmt = delete(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == callback_registration_id ) await self._session.execute(stmt) async def delete_expired(self, now: datetime) -> int: stmt = delete(CallbackCredentialRow).where( CallbackCredentialRow.expires_at.is_not(None), CallbackCredentialRow.expires_at <= now, ) result = cast(CursorResult[Any], await self._session.execute(stmt)) return result.rowcount src/open_exposure_gateway/adapters/database/sql.py +20 −0 Changes for src/open_exposure_gateway/adapters/database/sql.py: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from sqlalchemy import ( ForeignKey, Index, Integer, LargeBinary, MetaData, String, Text, Loading Loading @@ -151,6 +152,25 @@ class CallbackRegistrationRow(Base): ) class CallbackCredentialRow(Base): # Separate table rather than a column on callback_registrations: create_all never # alters an existing table. __tablename__ = "callback_credentials" __table_args__ = (Index("idx_callback_credentials_expires_at", "expires_at"),) callback_registration_id: Mapped[UUID] = mapped_column( ForeignKey("callback_registrations.id"), primary_key=True ) # nonce + AES-GCM ciphertext; see application/callback_secrets.py. encrypted_token: Mapped[bytes] = mapped_column(LargeBinary, nullable=False) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) class CallbackDeliveryRow(Base): __tablename__ = "callback_deliveries" Loading src/open_exposure_gateway/dependencies.py +8 −0 Changes for src/open_exposure_gateway/dependencies.py: 8 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,9 @@ from open_exposure_gateway.adapters.database.repos.app_instances import ( from open_exposure_gateway.adapters.database.repos.app_registrations import ( SqlAppRegistrationRepository, ) from open_exposure_gateway.adapters.database.repos.callback_credentials import ( SqlCallbackCredentialRepository, ) from open_exposure_gateway.adapters.database.repos.callback_deliveries import ( SqlCallbackDeliveryRepository, ) Loading Loading @@ -49,6 +52,7 @@ from open_exposure_gateway.core.exceptions import ( ) from open_exposure_gateway.core.state import AppState from open_exposure_gateway.ports.database.callbacks import ( CallbackCredentialRepository, CallbackDeliveryRepository, CallbackRegistrationRepository, ) Loading Loading @@ -215,6 +219,10 @@ def get_callback_delivery_repo(session: SessionDep) -> CallbackDeliveryRepositor return SqlCallbackDeliveryRepository(session) def get_callback_credential_repo(session: SessionDep) -> CallbackCredentialRepository: return SqlCallbackCredentialRepository(session) def get_qod_callback_client(request: Request) -> QodCallbackDeliveryPort: return get_app_state(request=request).qod_callback_client Loading src/open_exposure_gateway/domain/models/__init__.py +2 −0 Changes for src/open_exposure_gateway/domain/models/__init__.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line from open_exposure_gateway.domain.models.callbacks import ( CallbackCredential, CallbackDelivery, CallbackRegistration, ) Loading @@ -23,6 +24,7 @@ __all__ = [ "AppInstanceState", "AppRegistration", "AppRegistrationStatus", "CallbackCredential", "CallbackDelivery", "CallbackRegistration", "Operation", Loading Loading
src/open_exposure_gateway/adapters/database/mappers.py +21 −0 Changes for src/open_exposure_gateway/adapters/database/mappers.py: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ from open_exposure_gateway.adapters.database.sql import ( AppDeploymentRow, AppInstanceRow, AppRegistrationRow, CallbackCredentialRow, CallbackDeliveryRow, CallbackRegistrationRow, OperationRow, Loading @@ -13,6 +14,7 @@ from open_exposure_gateway.domain.models import ( AppDeployment, AppInstance, AppRegistration, CallbackCredential, CallbackDelivery, CallbackRegistration, Operation, Loading Loading @@ -203,6 +205,25 @@ class CallbackRegistrationMapper: ) class CallbackCredentialMapper: @staticmethod def to_domain(row: CallbackCredentialRow) -> CallbackCredential: return CallbackCredential( callback_registration_id=row.callback_registration_id, encrypted_token=row.encrypted_token, expires_at=row.expires_at, created_at=row.created_at, ) @staticmethod def to_row(domain: CallbackCredential) -> CallbackCredentialRow: return CallbackCredentialRow( callback_registration_id=domain.callback_registration_id, encrypted_token=domain.encrypted_token, expires_at=domain.expires_at, ) class CallbackDeliveryMapper: @staticmethod def to_domain(row: CallbackDeliveryRow) -> CallbackDelivery: Loading
src/open_exposure_gateway/adapters/database/repos/callback_credentials.py 0 → 100644 +50 −0 Changes for src/open_exposure_gateway/adapters/database/repos/callback_credentials.py: 50 added lines, 0 removed lines. Original line number Diff line number Diff line from datetime import datetime from typing import Any, cast from uuid import UUID from sqlalchemy import CursorResult, delete, select from sqlalchemy.ext.asyncio import AsyncSession from open_exposure_gateway.adapters.database.mappers import CallbackCredentialMapper from open_exposure_gateway.adapters.database.sql import CallbackCredentialRow from open_exposure_gateway.domain.models import CallbackCredential from open_exposure_gateway.ports.database.callbacks import CallbackCredentialRepository class SqlCallbackCredentialRepository(CallbackCredentialRepository): def __init__(self, session: AsyncSession) -> None: self._session = session async def get_by_registration_id( self, callback_registration_id: UUID ) -> CallbackCredential | None: stmt = select(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == callback_registration_id ) row = await self._session.scalar(stmt) return CallbackCredentialMapper.to_domain(row) if row is not None else None async def save(self, callback_credential: CallbackCredential) -> CallbackCredential: merged = await self._session.merge(CallbackCredentialMapper.to_row(callback_credential)) await self._session.flush() stmt = select(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == merged.callback_registration_id ) row = await self._session.scalar(stmt) if row is None: raise RuntimeError("Saved callback credential could not be reloaded") return CallbackCredentialMapper.to_domain(row) async def delete_by_registration_id(self, callback_registration_id: UUID) -> None: stmt = delete(CallbackCredentialRow).where( CallbackCredentialRow.callback_registration_id == callback_registration_id ) await self._session.execute(stmt) async def delete_expired(self, now: datetime) -> int: stmt = delete(CallbackCredentialRow).where( CallbackCredentialRow.expires_at.is_not(None), CallbackCredentialRow.expires_at <= now, ) result = cast(CursorResult[Any], await self._session.execute(stmt)) return result.rowcount
src/open_exposure_gateway/adapters/database/sql.py +20 −0 Changes for src/open_exposure_gateway/adapters/database/sql.py: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from sqlalchemy import ( ForeignKey, Index, Integer, LargeBinary, MetaData, String, Text, Loading Loading @@ -151,6 +152,25 @@ class CallbackRegistrationRow(Base): ) class CallbackCredentialRow(Base): # Separate table rather than a column on callback_registrations: create_all never # alters an existing table. __tablename__ = "callback_credentials" __table_args__ = (Index("idx_callback_credentials_expires_at", "expires_at"),) callback_registration_id: Mapped[UUID] = mapped_column( ForeignKey("callback_registrations.id"), primary_key=True ) # nonce + AES-GCM ciphertext; see application/callback_secrets.py. encrypted_token: Mapped[bytes] = mapped_column(LargeBinary, nullable=False) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) class CallbackDeliveryRow(Base): __tablename__ = "callback_deliveries" Loading
src/open_exposure_gateway/dependencies.py +8 −0 Changes for src/open_exposure_gateway/dependencies.py: 8 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -14,6 +14,9 @@ from open_exposure_gateway.adapters.database.repos.app_instances import ( from open_exposure_gateway.adapters.database.repos.app_registrations import ( SqlAppRegistrationRepository, ) from open_exposure_gateway.adapters.database.repos.callback_credentials import ( SqlCallbackCredentialRepository, ) from open_exposure_gateway.adapters.database.repos.callback_deliveries import ( SqlCallbackDeliveryRepository, ) Loading Loading @@ -49,6 +52,7 @@ from open_exposure_gateway.core.exceptions import ( ) from open_exposure_gateway.core.state import AppState from open_exposure_gateway.ports.database.callbacks import ( CallbackCredentialRepository, CallbackDeliveryRepository, CallbackRegistrationRepository, ) Loading Loading @@ -215,6 +219,10 @@ def get_callback_delivery_repo(session: SessionDep) -> CallbackDeliveryRepositor return SqlCallbackDeliveryRepository(session) def get_callback_credential_repo(session: SessionDep) -> CallbackCredentialRepository: return SqlCallbackCredentialRepository(session) def get_qod_callback_client(request: Request) -> QodCallbackDeliveryPort: return get_app_state(request=request).qod_callback_client Loading
src/open_exposure_gateway/domain/models/__init__.py +2 −0 Changes for src/open_exposure_gateway/domain/models/__init__.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line from open_exposure_gateway.domain.models.callbacks import ( CallbackCredential, CallbackDelivery, CallbackRegistration, ) Loading @@ -23,6 +24,7 @@ __all__ = [ "AppInstanceState", "AppRegistration", "AppRegistrationStatus", "CallbackCredential", "CallbackDelivery", "CallbackRegistration", "Operation", Loading