keepsake-sqlx 1.0.0

SQLx adapter for keepsake lifecycle storage
Documentation
use chrono::{DateTime, Utc};
use keepsake::{AuditEvent, ExpiryPolicy, Keepsake};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[cfg(feature = "postgres")]
use sqlx::{Row, postgres::PgRow};

/// Keyset cursor for active relation membership scans.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MembershipCursor {
    /// Last seen subject kind.
    pub subject_kind: String,
    /// Last seen subject id.
    pub subject_id: String,
    /// Last seen keepsake id.
    pub keepsake_id: Uuid,
}

impl MembershipCursor {
    /// Creates a cursor positioned after a returned keepsake.
    #[must_use]
    pub fn after(keepsake: &Keepsake) -> Self {
        Self {
            subject_kind: keepsake.subject().kind().to_owned(),
            subject_id: keepsake.subject().id().to_owned(),
            keepsake_id: keepsake.id(),
        }
    }
}

/// A persisted audit event together with its stable storage id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditEventRecord {
    /// Monotonic audit row id, stable for keyset pagination.
    pub id: i64,
    /// Reconstructed audit event.
    pub event: AuditEvent,
}

/// Keyset cursor for audit event reads in stable `(occurred_at, id)` order.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditCursor {
    /// Last seen occurrence time.
    pub occurred_at: DateTime<Utc>,
    /// Last seen audit row id.
    pub id: i64,
}

impl AuditCursor {
    /// Creates a cursor positioned after a returned audit event.
    #[must_use]
    pub const fn after(record: &AuditEventRecord) -> Self {
        Self {
            occurred_at: record.event.at,
            id: record.id,
        }
    }
}

/// Keyset cursor for audit outbox export in stable id order.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditOutboxCursor {
    /// Last seen outbox row id.
    pub id: i64,
}

impl AuditOutboxCursor {
    /// Creates a cursor positioned after a returned outbox record.
    #[must_use]
    pub const fn after(record: &AuditOutboxRecord) -> Self {
        Self { id: record.id }
    }
}

/// Audit outbox row for external delivery workers.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditOutboxRecord {
    /// Monotonic outbox row id.
    pub id: i64,
    /// Source audit event id.
    pub audit_event_id: i64,
    /// Stable event type label.
    pub event_type: String,
    /// Serialized [`AuditEvent`] payload.
    pub payload: AuditEvent,
    /// Worker that currently owns the lease, when claimed.
    pub claimed_by: Option<String>,
    /// Lease expiry timestamp, when claimed.
    pub claimed_until: Option<DateTime<Utc>>,
    /// Delivery acknowledgement timestamp.
    pub delivered_at: Option<DateTime<Utc>>,
}

/// Result of an apply operation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedKeepsake {
    /// Created keepsake, or the existing active keepsake for duplicate applies.
    pub keepsake: Keepsake,
    /// Whether a duplicate active keepsake was prevented.
    pub duplicate_prevented: bool,
}

/// Due timed expiry candidate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "postgres", derive(sqlx::FromRow))]
pub struct TimedExpiryCandidate {
    /// Keepsake id.
    pub keepsake_id: Uuid,
    /// Relation id.
    pub relation_id: Uuid,
    /// Subject kind.
    pub subject_kind: String,
    /// Subject id.
    pub subject_id: String,
    /// Due timestamp.
    pub due_at: DateTime<Utc>,
}

/// Due fulfillment expiry candidate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FulfilledExpiryCandidate {
    /// Keepsake id.
    pub keepsake_id: Uuid,
    /// Relation id.
    pub relation_id: Uuid,
    /// Subject kind.
    pub subject_kind: String,
    /// Subject id.
    pub subject_id: String,
    /// Copied expiry policy.
    pub expiry_policy: ExpiryPolicy,
}

#[cfg(feature = "postgres")]
impl<'row> sqlx::FromRow<'row, PgRow> for FulfilledExpiryCandidate {
    fn from_row(row: &'row PgRow) -> Result<Self, sqlx::Error> {
        let expiry_policy = serde_json::from_value(row.try_get("expiry_policy")?)
            .map_err(|error| sqlx::Error::Decode(Box::new(error)))?;
        Ok(Self {
            keepsake_id: row.try_get("keepsake_id")?,
            relation_id: row.try_get("relation_id")?,
            subject_kind: row.try_get("subject_kind")?,
            subject_id: row.try_get("subject_id")?,
            expiry_policy,
        })
    }
}