156 lines
4.6 KiB
Rust
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}"),
|
|
}
|
|
}
|