Files
papercrate/backend/src/tenants.rs
T
2025-10-29 22:19:54 +01:00

156 lines
4.6 KiB
Rust

use diesel::{
dsl::{exists, select},
pg::PgConnection,
prelude::*,
sql_types::Text,
};
use serde_json::json;
use uuid::Uuid;
use crate::{
db::PgPool,
error::{AppError, AppResult},
jobs::{enqueue_job, JOB_PROVISION_TENANT},
models::{Tenant, TenantStatus},
schema::tenants::dsl,
};
pub struct TenantRepository;
impl TenantRepository {
pub fn get_by_id(conn: &mut PgConnection, tenant_id: Uuid) -> AppResult<Tenant> {
dsl::tenants.find(tenant_id).first(conn).map_err(Into::into)
}
pub fn get_by_name(conn: &mut PgConnection, name: &str) -> AppResult<Tenant> {
dsl::tenants
.filter(dsl::name.eq(name))
.first(conn)
.map_err(Into::into)
}
}
#[derive(Clone)]
pub struct TenantService {
pool: PgPool,
}
impl TenantService {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
pub fn get_by_id(&self, tenant_id: Uuid) -> AppResult<Tenant> {
let tenant = self.load(|conn| TenantRepository::get_by_id(conn, tenant_id))?;
Ok(tenant)
}
pub fn get_by_name(&self, name: &str) -> AppResult<Tenant> {
let name_owned = name.to_owned();
let tenant = self.load(|conn| TenantRepository::get_by_name(conn, &name_owned))?;
Ok(tenant)
}
pub fn tenant_id_for_name(&self, name: &str) -> AppResult<Uuid> {
Ok(self.get_by_name(name)?.id)
}
pub fn create_tenant(
&self,
name: &str,
storage_root: Option<&str>,
quickwit_index: Option<&str>,
status: TenantStatus,
initial_members: &[Uuid],
created_by: Option<Uuid>,
) -> AppResult<Tenant> {
let name = name.trim();
if name.is_empty() {
return Err(AppError::bad_request("tenant name must not be empty"));
}
let mut conn = self.pool.get().map_err(|err| {
tracing::error!(error = ?err, "database pool error");
AppError::internal("database pool error")
})?;
let exists: bool =
select(exists(dsl::tenants.filter(dsl::name.eq(name)))).get_result(&mut conn)?;
if exists {
return Err(AppError::conflict(format!(
"tenant '{}' already exists",
name
)));
}
let id = Uuid::new_v4();
let storage_root = normalize_storage_root(storage_root, id);
let quickwit_index = normalize_quickwit_index(quickwit_index, id);
diesel::insert_into(dsl::tenants)
.values((
dsl::id.eq(id),
dsl::name.eq(name),
dsl::storage_root.eq(Some(storage_root.clone())),
dsl::quickwit_index.eq(Some(quickwit_index.clone())),
dsl::config.eq(json!({})),
dsl::status.eq(status),
dsl::created_by.eq(created_by),
))
.execute(&mut conn)?;
if status == TenantStatus::Creating {
let payload = json!({
"members": initial_members,
});
enqueue_job(&mut conn, id, JOB_PROVISION_TENANT, payload, None).map_err(|err| {
tracing::error!(error = ?err, tenant_id = %id, "failed to enqueue tenant provisioning job");
AppError::internal("failed to enqueue tenant provisioning job")
})?;
}
TenantRepository::get_by_id(&mut conn, id)
}
fn load<F>(&self, loader: F) -> AppResult<Tenant>
where
F: FnOnce(&mut PgConnection) -> AppResult<Tenant>,
{
let mut conn = self.pool.get().map_err(|err| {
tracing::error!(error = ?err, "database pool error");
AppError::internal("database pool error")
})?;
let tenant = loader(&mut conn)?;
Ok(tenant)
}
}
pub fn apply_tenant_guc(conn: &mut PgConnection, tenant_id: Uuid) -> AppResult<()> {
diesel::sql_query("SELECT set_config('papercrate.tenant_id', $1, true)")
.bind::<Text, _>(tenant_id.to_string())
.execute(conn)
.map(|_| ())
.map_err(AppError::from)
}
fn normalize_storage_root(raw: Option<&str>, tenant_id: Uuid) -> String {
match raw.map(str::trim) {
Some(root) if !root.is_empty() => {
let mut owned = root.to_owned();
if !owned.ends_with('/') {
owned.push('/');
}
owned
}
_ => format!("tenants/{tenant_id}/"),
}
}
fn normalize_quickwit_index(raw: Option<&str>, tenant_id: Uuid) -> String {
match raw.map(str::trim) {
Some(value) if !value.is_empty() => value.to_owned(),
_ => format!("documents-{tenant_id}"),
}
}