Files
govoplan-calendar/src/govoplan_calendar/backend/db/models.py

220 lines
12 KiB
Python

from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from govoplan_core.db.base import Base, TimestampMixin
def new_uuid() -> str:
return str(uuid.uuid4())
class CalendarCollection(Base, TimestampMixin):
__tablename__ = "calendar_collections"
__table_args__ = (
Index(
"uq_calendar_collections_active_slug",
"tenant_id",
"slug",
unique=True,
sqlite_where=text("deleted_at IS NULL"),
postgresql_where=text("deleted_at IS NULL"),
),
Index("ix_calendar_collections_owner", "tenant_id", "owner_type", "owner_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)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
timezone: Mapped[str] = mapped_column(String(100), default="UTC", nullable=False)
color: Mapped[str | None] = mapped_column(String(20))
owner_type: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True)
owner_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
visibility: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True)
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
events: Mapped[list["CalendarEvent"]] = relationship(back_populates="calendar", cascade="all, delete-orphan")
class CalendarEvent(Base, TimestampMixin):
__tablename__ = "calendar_events"
__table_args__ = (
UniqueConstraint("calendar_id", "uid", "recurrence_id", name="uq_calendar_events_calendar_uid_recurrence"),
Index("ix_calendar_events_range", "tenant_id", "calendar_id", "start_at", "end_at"),
Index("ix_calendar_events_tenant_uid", "tenant_id", "uid"),
Index("ix_calendar_events_tenant_status", "tenant_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)
calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True)
uid: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
recurrence_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
sequence: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
summary: Mapped[str] = mapped_column(String(500), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
location: Mapped[str | None] = mapped_column(String(500))
status: Mapped[str] = mapped_column(String(40), default="CONFIRMED", nullable=False, index=True)
transparency: Mapped[str] = mapped_column(String(20), default="OPAQUE", nullable=False)
classification: Mapped[str] = mapped_column(String(20), default="PUBLIC", nullable=False)
start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
end_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
duration_seconds: Mapped[int | None] = mapped_column(Integer)
all_day: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
timezone: Mapped[str | None] = mapped_column(String(100))
organizer: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
attendees: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
categories: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
rrule: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
rdate: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
exdate: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
reminders: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
attachments: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
related_to: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
source_kind: Mapped[str] = mapped_column(String(30), default="local", nullable=False, index=True)
source_href: Mapped[str | None] = mapped_column(String(1000))
etag: Mapped[str | None] = mapped_column(String(255), index=True)
icalendar: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
raw_ics: Mapped[str | None] = mapped_column(Text)
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
updated_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
calendar: Mapped[CalendarCollection] = relationship(back_populates="events")
class CalendarSyncSource(Base, TimestampMixin):
__tablename__ = "calendar_sync_sources"
__table_args__ = (
Index("ix_calendar_sync_sources_calendar", "tenant_id", "calendar_id", "source_kind"),
Index(
"uq_calendar_sync_sources_active_url",
"tenant_id",
"source_kind",
"collection_url",
unique=True,
sqlite_where=text("deleted_at IS NULL"),
postgresql_where=text("deleted_at IS NULL"),
),
)
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)
calendar_id: Mapped[str] = mapped_column(ForeignKey("calendar_collections.id", ondelete="CASCADE"), nullable=False, index=True)
source_kind: Mapped[str] = mapped_column(String(30), default="caldav", nullable=False, index=True)
collection_url: Mapped[str] = mapped_column(String(1000), nullable=False)
display_name: Mapped[str | None] = mapped_column(String(255))
auth_type: Mapped[str] = mapped_column(String(30), default="none", nullable=False)
username: Mapped[str | None] = mapped_column(String(255))
credential_ref: Mapped[str | None] = mapped_column(String(255))
sync_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
sync_interval_seconds: Mapped[int] = mapped_column(Integer, default=900, nullable=False)
sync_direction: Mapped[str] = mapped_column(String(30), default="two_way", nullable=False)
conflict_policy: Mapped[str] = mapped_column(String(30), default="etag", nullable=False)
sync_token: Mapped[str | None] = mapped_column(Text)
ctag: Mapped[str | None] = mapped_column(String(255))
last_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
next_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
last_status: Mapped[str | None] = mapped_column(String(30))
last_error: Mapped[str | None] = mapped_column(Text)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
calendar: Mapped[CalendarCollection] = relationship()
class CalendarSyncCredential(Base, TimestampMixin):
__tablename__ = "calendar_sync_credentials"
__table_args__ = (
Index("ix_calendar_sync_credentials_tenant_kind", "tenant_id", "credential_kind"),
)
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)
credential_kind: Mapped[str] = mapped_column(String(30), nullable=False, index=True)
label: Mapped[str | None] = mapped_column(String(255))
secret_encrypted: Mapped[str | None] = mapped_column(Text)
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
class CalendarOutboxOperation(Base, TimestampMixin):
"""Durable desired state for one external CalDAV resource.
Rows are inserted in the same transaction as the local event mutation. A
dispatcher leases and commits the row before performing network I/O, so a
worker crash can be detected and reconciled without repeating a blind
write.
"""
__tablename__ = "calendar_outbox_operations"
__table_args__ = (
UniqueConstraint("idempotency_key", name="uq_calendar_outbox_operations_idempotency_key"),
Index("ix_calendar_outbox_due", "status", "available_at", "created_at"),
Index("ix_calendar_outbox_tenant_status", "tenant_id", "status"),
Index("ix_calendar_outbox_resource", "source_id", "resource_href", "created_at"),
Index("ix_calendar_outbox_lease", "status", "lease_expires_at"),
)
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)
source_id: Mapped[str] = mapped_column(
ForeignKey("calendar_sync_sources.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
event_id: Mapped[str | None] = mapped_column(
ForeignKey("calendar_events.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
operation_kind: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
resource_href: Mapped[str] = mapped_column(String(1000), nullable=False)
payload_ics: Mapped[str | None] = mapped_column(Text)
payload_fingerprint: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
expected_etag: Mapped[str | None] = mapped_column(String(255))
idempotency_key: Mapped[str] = mapped_column(String(64), nullable=False)
status: Mapped[str] = mapped_column(String(30), default="pending", nullable=False, index=True)
attempt_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
max_attempts: Mapped[int] = mapped_column(Integer, default=8, nullable=False)
available_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
index=True,
)
last_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
lease_token: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
lease_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
reconciled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
remote_etag: Mapped[str | None] = mapped_column(String(255))
last_error: Mapped[str | None] = mapped_column(Text)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
source: Mapped[CalendarSyncSource] = relationship()
event: Mapped[CalendarEvent | None] = relationship()
__all__ = [
"CalendarCollection",
"CalendarEvent",
"CalendarOutboxOperation",
"CalendarSyncCredential",
"CalendarSyncSource",
"new_uuid",
]