Implement destructive CalDAV move saga

This commit is contained in:
2026-08-02 17:30:24 +02:00
parent 56d7b2108d
commit a125b666da
16 changed files with 2384 additions and 36 deletions
@@ -256,9 +256,63 @@ class CalendarOutboxOperation(Base, TimestampMixin):
event: Mapped[CalendarEvent | None] = relationship()
class CalendarMigrationBatch(Base, TimestampMixin):
"""Durable destructive remote-move saga and its authorization evidence."""
__tablename__ = "calendar_migration_batches"
__table_args__ = (
Index("ix_calendar_migration_batches_tenant_status", "tenant_id", "status", "created_at"),
Index("ix_calendar_migration_batches_calendars", "tenant_id", "source_calendar_id", "target_calendar_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
migration_kind: Mapped[str] = mapped_column(String(30), default="remote_move", nullable=False)
status: Mapped[str] = mapped_column(String(30), default="active", nullable=False, index=True)
phase: Mapped[str] = mapped_column(String(40), default="copying_destination", nullable=False, index=True)
source_calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="RESTRICT"), nullable=False, index=True)
target_calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="RESTRICT"), nullable=False, index=True)
source_sync_source_id: Mapped[str] = mapped_column(ForeignKey("calendar_sync_sources.id", ondelete="RESTRICT"), nullable=False, index=True)
target_sync_source_id: Mapped[str] = mapped_column(ForeignKey("calendar_sync_sources.id", ondelete="RESTRICT"), nullable=False, index=True)
total_resources: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
total_events: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
created_by_api_key_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
authorization_evidence: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
cancellation_evidence: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
last_error: Mapped[str | None] = mapped_column(Text)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
resources: Mapped[list["CalendarMigrationResource"]] = relationship(back_populates="batch", cascade="all, delete-orphan")
class CalendarMigrationResource(Base, TimestampMixin):
__tablename__ = "calendar_migration_resources"
__table_args__ = (
UniqueConstraint("batch_id", "source_href", name="uq_calendar_migration_resources_batch_href"),
Index("ix_calendar_migration_resources_batch_status", "batch_id", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
batch_id: Mapped[str] = mapped_column(ForeignKey("calendar_migration_batches.id", ondelete="CASCADE"), nullable=False, index=True)
source_href: Mapped[str] = mapped_column(String(1000), nullable=False)
source_expected_etag: Mapped[str] = mapped_column(String(255), nullable=False)
destination_href: Mapped[str] = mapped_column(String(1000), nullable=False)
event_ids: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
status: Mapped[str] = mapped_column(String(30), default="copy_pending", nullable=False, index=True)
destination_operation_id: Mapped[str | None] = mapped_column(ForeignKey("calendar_outbox_operations.id", ondelete="SET NULL"), nullable=True, unique=True)
source_delete_operation_id: Mapped[str | None] = mapped_column(ForeignKey("calendar_outbox_operations.id", ondelete="SET NULL"), nullable=True, unique=True)
last_error: Mapped[str | None] = mapped_column(Text)
batch: Mapped[CalendarMigrationBatch] = relationship(back_populates="resources")
__all__ = [
"CalendarCollection",
"CalendarEvent",
"CalendarMigrationBatch",
"CalendarMigrationResource",
"CalendarOutboxOperation",
"CalendarSyncCredential",
"CalendarSyncSource",