Files
caravel/backend/src/repo.rs
T
2026-03-03 10:20:36 -08:00

237 lines
7.4 KiB
Rust

use anyhow::Result;
use sqlx::{Row, Sqlite, SqlitePool, Transaction};
use crate::models::{
Invoice, InvoiceItem, NewInvoice, NewInvoiceItem, NewTenant, Relay, Tenant,
};
fn relay_from_row(row: sqlx::sqlite::SqliteRow) -> Relay {
let config_json: Option<String> = row.get("config");
let config = config_json.and_then(|s| serde_json::from_str(&s).ok());
Relay {
id: row.get("id"),
tenant: row.get("tenant"),
name: row.get("name"),
subdomain: row.get("subdomain"),
icon: row.get("icon"),
description: row.get("description"),
plan: row.get("plan"),
status: row.get("status"),
config,
}
}
#[derive(Clone)]
pub struct Repo {
pool: SqlitePool,
}
impl Repo {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
pub async fn create_tenant(&self, tenant: &NewTenant) -> Result<()> {
sqlx::query("INSERT INTO tenants (pubkey, status, tenant_nwc_url) VALUES (?, ?, ?)")
.bind(&tenant.pubkey)
.bind(&tenant.status)
.bind(&tenant.tenant_nwc_url)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn create_tenant_if_missing(&self, tenant: &NewTenant) -> Result<()> {
sqlx::query(
"INSERT OR IGNORE INTO tenants (pubkey, status, tenant_nwc_url) VALUES (?, ?, ?)",
)
.bind(&tenant.pubkey)
.bind(&tenant.status)
.bind(&tenant.tenant_nwc_url)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_tenant(&self, pubkey: &str) -> Result<Option<Tenant>> {
let tenant = sqlx::query_as::<_, Tenant>(
"SELECT pubkey, status, tenant_nwc_url FROM tenants WHERE pubkey = ?",
)
.bind(pubkey)
.fetch_optional(&self.pool)
.await?;
Ok(tenant)
}
pub async fn list_tenants(&self) -> Result<Vec<Tenant>> {
let tenants = sqlx::query_as::<_, Tenant>(
"SELECT pubkey, status, tenant_nwc_url FROM tenants ORDER BY pubkey",
)
.fetch_all(&self.pool)
.await?;
Ok(tenants)
}
pub async fn update_tenant_status(&self, pubkey: &str, status: &str) -> Result<()> {
sqlx::query("UPDATE tenants SET status = ? WHERE pubkey = ?")
.bind(status)
.bind(pubkey)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn update_tenant_nwc_url(&self, pubkey: &str, tenant_nwc_url: &str) -> Result<()> {
sqlx::query("UPDATE tenants SET tenant_nwc_url = ? WHERE pubkey = ?")
.bind(tenant_nwc_url)
.bind(pubkey)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn upsert_relay(&self, relay: &Relay) -> Result<()> {
let config_json = relay.config.as_ref().map(serde_json::to_string).transpose()?;
sqlx::query(
"INSERT INTO relays (id, tenant, name, subdomain, icon, description, plan, status, config)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
subdomain = excluded.subdomain,
icon = excluded.icon,
description = excluded.description,
plan = excluded.plan,
status = excluded.status,
config = excluded.config",
)
.bind(&relay.id)
.bind(&relay.tenant)
.bind(&relay.name)
.bind(&relay.subdomain)
.bind(&relay.icon)
.bind(&relay.description)
.bind(&relay.plan)
.bind(&relay.status)
.bind(config_json)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn update_relay_status(&self, id: &str, status: &str) -> Result<()> {
sqlx::query("UPDATE relays SET status = ? WHERE id = ?")
.bind(status)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn suspend_relays_for_tenant(&self, tenant: &str) -> Result<()> {
sqlx::query(
"UPDATE relays SET status = 'suspended' WHERE tenant = ? AND status = 'active'",
)
.bind(tenant)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_relay(&self, id: &str) -> Result<Option<Relay>> {
let row = sqlx::query(
"SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays WHERE id = ?",
)
.bind(id)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(relay_from_row))
}
pub async fn list_relays_by_tenant(&self, tenant: &str) -> Result<Vec<Relay>> {
let rows = sqlx::query(
"SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays WHERE tenant = ? ORDER BY name",
)
.bind(tenant)
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(relay_from_row).collect())
}
pub async fn list_relays(&self) -> Result<Vec<Relay>> {
let rows = sqlx::query(
"SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays ORDER BY name",
)
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(relay_from_row).collect())
}
pub async fn create_invoice_with_items(
&self,
invoice: &NewInvoice,
items: &[NewInvoiceItem],
) -> Result<()> {
let mut tx: Transaction<'_, Sqlite> = self.pool.begin().await?;
sqlx::query(
"INSERT INTO invoices (id, tenant, amount, status, created_at, invoice)
VALUES (?, ?, ?, ?, ?, ?)",
)
.bind(&invoice.id)
.bind(&invoice.tenant)
.bind(invoice.amount)
.bind(&invoice.status)
.bind(&invoice.created_at)
.bind(&invoice.invoice)
.execute(&mut *tx)
.await?;
for item in items {
sqlx::query(
"INSERT INTO invoice_items (id, invoice, relay, amount, period_start, period_end)
VALUES (?, ?, ?, ?, ?, ?)",
)
.bind(&item.id)
.bind(&item.invoice)
.bind(&item.relay)
.bind(item.amount)
.bind(&item.period_start)
.bind(&item.period_end)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub async fn list_invoices_by_tenant(&self, tenant: &str) -> Result<Vec<Invoice>> {
let invoices = sqlx::query_as::<_, Invoice>(
"SELECT id, tenant, amount, status, created_at, invoice FROM invoices WHERE tenant = ? ORDER BY created_at DESC",
)
.bind(tenant)
.fetch_all(&self.pool)
.await?;
Ok(invoices)
}
pub async fn list_invoice_items(&self, invoice_id: &str) -> Result<Vec<InvoiceItem>> {
let items = sqlx::query_as::<_, InvoiceItem>(
"SELECT id, invoice, relay, amount, period_start, period_end FROM invoice_items WHERE invoice = ?",
)
.bind(invoice_id)
.fetch_all(&self.pool)
.await?;
Ok(items)
}
pub async fn update_invoice_status(&self, id: &str, status: &str) -> Result<()> {
sqlx::query("UPDATE invoices SET status = ? WHERE id = ?")
.bind(status)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
}