Commit 4be16254 authored by George Papathanail's avatar George Papathanail
Browse files

feat(db): add callback_credentials table and repository

parent c46cfee7
Loading
Loading
Loading
Loading
Loading
+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
@@ -4,6 +4,7 @@ from open_exposure_gateway.adapters.database.sql import (
    AppDeploymentRow,
    AppInstanceRow,
    AppRegistrationRow,
    CallbackCredentialRow,
    CallbackDeliveryRow,
    CallbackRegistrationRow,
    OperationRow,
@@ -13,6 +14,7 @@ from open_exposure_gateway.domain.models import (
    AppDeployment,
    AppInstance,
    AppRegistration,
    CallbackCredential,
    CallbackDelivery,
    CallbackRegistration,
    Operation,
@@ -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:
+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
+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
@@ -9,6 +9,7 @@ from sqlalchemy import (
    ForeignKey,
    Index,
    Integer,
    LargeBinary,
    MetaData,
    String,
    Text,
@@ -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"

+8 −0
Changes for src/open_exposure_gateway/dependencies.py: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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,
)
@@ -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,
)
@@ -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

+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,
)
@@ -23,6 +24,7 @@ __all__ = [
    "AppInstanceState",
    "AppRegistration",
    "AppRegistrationStatus",
    "CallbackCredential",
    "CallbackDelivery",
    "CallbackRegistration",
    "Operation",
Loading