use anyhow::{Context, Result};
use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime};
use tokio_postgres::NoTls;
pub struct CursorRow {
pub source_name: String,
pub value: Option<serde_json::Value>,
pub version: i64,
pub updated_at: String,
pub age_seconds: f64,
}
pub struct SourceCursorReader {
pool: Pool,
}
impl SourceCursorReader {
pub fn connect(db_url: &str) -> Result<Self> {
if !db_url.starts_with("postgres") {
anyhow::bail!(
"source cursor reads require a PostgreSQL connection URL (postgres://…); got: \
{db_url}"
);
}
let mut cfg = Config::new();
cfg.url = Some(db_url.to_string());
cfg.manager = Some(ManagerConfig {
recycling_method: RecyclingMethod::Fast,
});
cfg.pool = Some(deadpool_postgres::PoolConfig::new(2));
let pool = cfg
.create_pool(Some(Runtime::Tokio1), NoTls)
.context("failed to create PostgreSQL connection pool for source cursor reads")?;
Ok(Self { pool })
}
pub async fn load_cursors(&self) -> Result<Vec<CursorRow>> {
let client = self.pool.get().await.context("failed to acquire DB connection")?;
let rows = client
.query(
"SELECT source_name, cursor_value, version, updated_at::text AS updated_at_text, \
EXTRACT(EPOCH FROM (now() - updated_at))::float8 AS age_seconds \
FROM _fraiseql_source_cursor",
&[],
)
.await
.context(
"failed to read _fraiseql_source_cursor (is the source subsystem initialized?)",
)?;
Ok(rows
.iter()
.map(|row| CursorRow {
source_name: row.get("source_name"),
value: row.get("cursor_value"),
version: row.get("version"),
updated_at: row.get("updated_at_text"),
age_seconds: row.get("age_seconds"),
})
.collect())
}
}