use std::sync::Arc;
use async_trait::async_trait;
use serde::Serialize;
use tokio::sync::RwLock;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TenantEventKind {
Created,
ConfigChanged,
Suspended,
Resumed,
Deleted,
}
impl TenantEventKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Created => "created",
Self::ConfigChanged => "config_changed",
Self::Suspended => "suspended",
Self::Resumed => "resumed",
Self::Deleted => "deleted",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TenantEvent {
pub tenant_key: String,
pub event: TenantEventKind,
pub actor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<serde_json::Value>,
pub occurred_at: String,
}
#[async_trait]
pub trait TenantAuditLog: Send + Sync {
async fn record(
&self,
tenant_key: &str,
event: TenantEventKind,
actor: Option<&str>,
payload: Option<serde_json::Value>,
) -> fraiseql_error::Result<()>;
async fn events_for(
&self,
tenant_key: &str,
limit: usize,
offset: usize,
) -> fraiseql_error::Result<Vec<TenantEvent>>;
}
pub struct InMemoryAuditLog {
events: RwLock<Vec<TenantEvent>>,
}
impl InMemoryAuditLog {
#[must_use]
pub fn new() -> Self {
Self {
events: RwLock::new(Vec::new()),
}
}
}
impl Default for InMemoryAuditLog {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TenantAuditLog for InMemoryAuditLog {
async fn record(
&self,
tenant_key: &str,
event: TenantEventKind,
actor: Option<&str>,
payload: Option<serde_json::Value>,
) -> fraiseql_error::Result<()> {
let entry = TenantEvent {
tenant_key: tenant_key.to_string(),
event,
actor: actor.map(ToString::to_string),
payload,
occurred_at: chrono::Utc::now().to_rfc3339(),
};
self.events.write().await.push(entry);
Ok(())
}
async fn events_for(
&self,
tenant_key: &str,
limit: usize,
offset: usize,
) -> fraiseql_error::Result<Vec<TenantEvent>> {
let events = self.events.read().await;
let matching: Vec<TenantEvent> = events
.iter()
.rev() .filter(|e| e.tenant_key == tenant_key)
.skip(offset)
.take(limit)
.cloned()
.collect();
Ok(matching)
}
}
pub type AuditLogHandle = Arc<dyn TenantAuditLog>;
pub const TENANT_EVENTS_DDL: &str = "\
CREATE TABLE IF NOT EXISTS _fraiseql_tenant_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_key TEXT NOT NULL,
event TEXT NOT NULL,
actor TEXT,
payload JSONB,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tenant_events_key
ON _fraiseql_tenant_events (tenant_key, occurred_at);
";