use crate::{
bootstrap::state::AppState,
features::databases::env_switch,
features::databases::{
managed_view::to_view,
shared_db::{self, Engine},
},
features::deploy::shared_net,
store::{self, managed_database::Row},
};
use chrono::Utc;
use shared::{ManagedDatabaseCreateReq, Response as RpResponse, Service};
async fn server(state: &AppState, id: &str) -> Result<(Service, Engine), RpResponse> {
let svc = match store::services::get(&state.db, id).await {
Ok(Some(s)) => s,
Ok(None) => return Err(RpResponse::err("NotFound", "service not found")),
Err(e) => return Err(RpResponse::err("DatabaseError", e.to_string())),
};
if svc.spec.shared.is_none() {
return Err(RpResponse::err(
"NotShared",
"este serviço não é um servidor compartilhado",
));
}
let Some(engine) = Engine::from_kind(svc.spec.db_kind.as_deref()) else {
return Err(RpResponse::err(
"Unsupported",
"motor de banco não suportado",
));
};
Ok((svc, engine))
}
async fn views(state: &AppState, svc: &Service, engine: Engine) -> RpResponse {
match store::managed_database::list(&state.db, &svc.id).await {
Ok(rows) => {
RpResponse::ManagedDatabases(rows.iter().map(|r| to_view(svc, engine, r)).collect())
}
Err(e) => RpResponse::err("DatabaseError", e.to_string()),
}
}
pub async fn list(state: AppState, server_service_id: String) -> RpResponse {
match server(&state, &server_service_id).await {
Ok((svc, engine)) => views(&state, &svc, engine).await,
Err(r) => r,
}
}
pub async fn create(state: AppState, req: ManagedDatabaseCreateReq) -> RpResponse {
let (svc, engine) = match server(&state, &req.server_service_id).await {
Ok(v) => v,
Err(r) => return r,
};
if let Err(e) = shared_db::validate_name(&req.name) {
return RpResponse::err("InvalidName", e.to_string());
}
match store::projects::get(&state.db, &req.project_id).await {
Ok(Some(_)) => {}
Ok(None) => return RpResponse::err("NotFound", "project not found"),
Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
}
let env_var = if req.env_var.trim().is_empty() {
engine.default_env_var().to_string()
} else {
req.env_var.trim().to_string()
};
if !env_var
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return RpResponse::err("InvalidName", "nome de env var inválido");
}
let defined = if req.skip_env {
Ok(false)
} else {
env_switch::is_defined(&state.db, &req.project_id, &env_var).await
};
match defined {
Ok(true) if !req.overwrite_env => {
return RpResponse::err(
"EnvVarExists",
format!(
"o projeto já tem a env var {env_var}; escolha outro nome ou confirme a sobrescrita"
),
);
}
Ok(_) => {}
Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
}
if store::managed_database::list(&state.db, &svc.id)
.await
.is_ok_and(|l| l.iter().any(|r| r.name == req.name))
{
return RpResponse::err(
"AlreadyExists",
"já existe um database com esse nome neste servidor",
);
}
let Some(cid) = shared_net::server_container(&state.docker.inner, &svc).await else {
return RpResponse::err("NotRunning", "o servidor compartilhado não está no ar");
};
let password = match shared_db::generate_password() {
Ok(p) => p,
Err(e) => return RpResponse::err("Internal", e.to_string()),
};
if req.project_id != svc.spec.project_id {
if let Err(e) =
shared_net::connect_to_project(&state.db, &state.docker, &svc, &cid, &req.project_id)
.await
{
return RpResponse::err("NetworkError", e.to_string());
}
let _ = store::shared_access::grant(&state.db, &svc.id, &req.project_id).await;
}
if let Err(e) = shared_db::wait_ready(engine, &cid, 30).await {
return RpResponse::err("NotReady", e.to_string());
}
let script = shared_db::create_script(
engine,
&req.name,
&password,
req.connection_limit,
req.statement_timeout_ms,
);
if let Err(e) = shared_db::exec(engine, &cid, &script).await {
return RpResponse::err("ProvisionError", e.to_string());
}
let row = Row {
id: String::new(),
server_service_id: svc.id.clone(),
project_id: req.project_id.clone(),
name: req.name.clone(),
password,
env_var: env_var.clone(),
connection_limit: req.connection_limit,
statement_timeout_ms: req.statement_timeout_ms,
created_at: Utc::now(),
};
let row = match store::managed_database::insert(&state.db, &row).await {
Ok(r) => r,
Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
};
let url = to_view(&svc, engine, &row).connection_url;
if !req.skip_env
&& let Err(e) = env_switch::switch(&state.db, &req.project_id, &env_var, &url).await
{
return RpResponse::err("DatabaseError", e.to_string());
}
views(&state, &svc, engine).await
}
pub async fn delete(state: AppState, id: String) -> RpResponse {
let row = match store::managed_database::get(&state.db, &id).await {
Ok(Some(r)) => r,
Ok(None) => return RpResponse::err("NotFound", "database not found"),
Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
};
let (svc, engine) = match server(&state, &row.server_service_id).await {
Ok(v) => v,
Err(r) => return r,
};
let Some(cid) = shared_net::server_container(&state.docker.inner, &svc).await else {
return RpResponse::err("NotRunning", "o servidor compartilhado não está no ar");
};
if let Err(e) = shared_db::exec(engine, &cid, &shared_db::drop_script(engine, &row.name)).await
{
return RpResponse::err("ProvisionError", e.to_string());
}
if let Err(e) = store::managed_database::delete(&state.db, &id).await {
return RpResponse::err("DatabaseError", e.to_string());
}
views(&state, &svc, engine).await
}
pub async fn list_all(state: AppState) -> RpResponse {
let servers = match store::services::list_all(&state.db).await {
Ok(v) => v,
Err(e) => return RpResponse::err("DatabaseError", e.to_string()),
};
let mut out = vec![];
for svc in servers.iter().filter(|s| s.spec.shared.is_some()) {
let Some(engine) = Engine::from_kind(svc.spec.db_kind.as_deref()) else {
continue;
};
if let Ok(rows) = store::managed_database::list(&state.db, &svc.id).await {
out.extend(rows.iter().map(|r| to_view(svc, engine, r)));
}
}
RpResponse::ManagedDatabases(out)
}