"""Durable operation metadata for jobs, retries, idempotency, and audit.""" from __future__ import annotations from datetime import datetime, timezone from enum import Enum from uuid import uuid4 from sqlalchemy import JSON, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column from ..db import Base def _id(prefix: str) -> str: return f"{prefix}_{uuid4().hex}" def _utc_now() -> datetime: return datetime.now(timezone.utc) class JobStatus(str, Enum): QUEUED = "queued" RUNNING = "running" SUCCEEDED = "succeeded" FAILED = "failed" CANCELLED = "cancelled" class Job(Base): """Durable, tenant-owned unit of asynchronous work.""" __tablename__ = "jobs" __table_args__ = ( CheckConstraint( "status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')", name="ck_jobs_status", ), Index("ix_jobs_org_status_created", "organization_id", "status", "created_at"), Index("ix_jobs_org_owner", "organization_id", "owner_user_id"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("job")) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) owner_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True ) project_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) graph_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) operation: Mapped[str] = mapped_column(String(120), nullable=False) status: Mapped[JobStatus] = mapped_column( String(32), nullable=False, default=JobStatus.QUEUED.value, server_default=text("'queued'") ) progress: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default=text("0")) message: Mapped[str] = mapped_column(Text, nullable=False, default="", server_default=text("''")) result: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) progress_detail: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) job_metadata: Mapped[dict | list | None] = mapped_column("metadata", JSON, nullable=True) error_code: Mapped[str | None] = mapped_column(String(120), nullable=True) result_ref: Mapped[str | None] = mapped_column(String(512), nullable=True) idempotency_key: Mapped[str | None] = mapped_column(String(128), nullable=True) attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default=text("0")) settings_version: Mapped[str | None] = mapped_column(String(128), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, onupdate=_utc_now, server_default=func.now(), nullable=False ) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class IdempotencyRecord(Base): """Request fingerprint and replayable response for retry-safe mutations.""" __tablename__ = "idempotency_records" __table_args__ = ( UniqueConstraint("organization_id", "user_id", "key", name="uq_idempotency_org_user_key"), Index("ix_idempotency_expiry", "expires_at"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("idem")) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) key: Mapped[str] = mapped_column(String(128), nullable=False) request_hash: Mapped[str] = mapped_column(String(64), nullable=False) status: Mapped[str] = mapped_column( String(32), nullable=False, default="reserved", server_default=text("'reserved'") ) response_status: Mapped[int | None] = mapped_column(Integer, nullable=True) response_body: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class AuditLog(Base): """Tenant-scoped redacted audit event; details must never contain secrets.""" __tablename__ = "audit_logs" __table_args__ = ( Index("ix_audit_org_created", "organization_id", "created_at"), Index("ix_audit_org_target", "organization_id", "target_type", "target_id"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("audit")) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) actor_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True ) action: Mapped[str] = mapped_column(String(160), nullable=False) target_type: Mapped[str] = mapped_column(String(80), nullable=False) target_id: Mapped[str | None] = mapped_column(String(160), nullable=True) details: Mapped[dict | list | None] = mapped_column("metadata", JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False )