1use crate::{
2 censor::{CENSORED_PLACEHOLDER, Censor},
3 models::{InsertQueryBuilder, UpdateQueryBuilder},
4 prelude::*,
5};
6use garde::Validate;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow, prelude::Type};
9use std::{
10 collections::{BTreeMap, HashMap},
11 hash::Hash,
12 str::FromStr,
13 sync::{Arc, LazyLock},
14};
15use tokio::sync::Mutex;
16use utoipa::ToSchema;
17
18pub enum DatabaseTransaction<'a> {
19 Mysql(sqlx::Transaction<'a, sqlx::MySql>, sqlx::Pool<sqlx::MySql>),
20 Postgres(
21 sqlx::Transaction<'a, sqlx::Postgres>,
22 sqlx::Pool<sqlx::Postgres>,
23 ),
24 Mongodb(mongodb::Client),
25}
26
27#[derive(Clone)]
28pub enum DatabasePool {
29 Mysql(sqlx::Pool<sqlx::MySql>),
30 Postgres(sqlx::Pool<sqlx::Postgres>),
31 Mongodb(mongodb::Client),
32}
33
34type DatabasePoolValue = (std::time::Instant, DatabasePool);
35static DATABASE_CLIENTS: LazyLock<Arc<Mutex<HashMap<uuid::Uuid, DatabasePoolValue>>>> =
36 LazyLock::new(|| {
37 let clients = Arc::new(Mutex::new(HashMap::<uuid::Uuid, DatabasePoolValue>::new()));
38
39 tokio::spawn({
40 let clients = Arc::clone(&clients);
41 async move {
42 loop {
43 tokio::time::sleep(std::time::Duration::from_mins(1)).await;
44
45 let mut clients = clients.lock().await;
46 let before_len = clients.len();
47 clients.retain(|_, &mut (last_used, _)| {
48 last_used.elapsed() < std::time::Duration::from_mins(5)
49 });
50
51 if clients.len() != before_len {
52 tracing::info!(
53 "cleaned up {} idle database connections",
54 before_len - clients.len()
55 );
56 }
57 }
58 }
59 });
60
61 clients
62 });
63
64#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
65#[serde(rename_all = "snake_case")]
66#[sqlx(type_name = "database_type", rename_all = "SCREAMING_SNAKE_CASE")]
67pub enum DatabaseType {
68 Mysql,
69 Postgres,
70 Mongodb,
71}
72
73impl DatabaseType {
74 pub fn from_url_scheme(scheme: &str) -> Result<Self, anyhow::Error> {
75 match scheme {
76 "mysql" | "mariadb" => Ok(Self::Mysql),
77 "postgres" | "postgresql" => Ok(Self::Postgres),
78 "mongodb" => Ok(Self::Mongodb),
79 _ => Err(anyhow::anyhow!("Unsupported database type: {}", scheme)),
80 }
81 }
82
83 pub const fn default_port(self) -> u16 {
84 match self {
85 DatabaseType::Mysql => 3306,
86 DatabaseType::Postgres => 5432,
87 DatabaseType::Mongodb => 27017,
88 }
89 }
90}
91
92fn validate_connection_string(connection_string: &str, _context: &()) -> Result<(), garde::Error> {
93 if connection_string.trim().is_empty() {
94 return Err(garde::Error::new("connection string cannot be empty"));
95 }
96
97 let url = reqwest::Url::parse(connection_string)
98 .map_err(|err| garde::Error::new(format!("Invalid connection string: {err}")))?;
99
100 DatabaseType::from_url_scheme(url.scheme())
101 .map_err(|err| garde::Error::new(format!("Invalid connection string: {err}")))?;
102
103 if url.host_str().is_none() {
104 return Err(garde::Error::new("Invalid connection string: missing host"));
105 }
106 if url.username().is_empty() {
107 return Err(garde::Error::new(
108 "Invalid connection string: missing username",
109 ));
110 }
111 if url.password().is_none() {
112 return Err(garde::Error::new(
113 "Invalid connection string: missing password",
114 ));
115 }
116
117 Ok(())
118}
119
120#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
121#[serde(tag = "type", rename_all = "snake_case")]
122pub enum DatabaseCredentials {
123 ConnectionString {
124 #[garde(length(chars, min = 1, max = 255), custom(validate_connection_string))]
125 #[schema(min_length = 1, max_length = 255)]
126 connection_string: compact_str::CompactString,
127 },
128 Details {
129 #[garde(length(chars, min = 1, max = 255))]
130 #[schema(min_length = 1, max_length = 255)]
131 host: compact_str::CompactString,
132 #[garde(range(min = 1))]
133 #[schema(minimum = 1)]
134 port: u16,
135 #[garde(length(chars, min = 1, max = 255))]
136 #[schema(min_length = 1, max_length = 255)]
137 username: compact_str::CompactString,
138 #[garde(length(chars, min = 1, max = 255))]
139 #[schema(min_length = 1, max_length = 255)]
140 password: compact_str::CompactString,
141 },
142}
143
144#[derive(ToSchema, Serialize)]
145pub struct ParsedConnectionDetails {
146 pub host: compact_str::CompactString,
147 pub port: u16,
148 pub username: compact_str::CompactString,
149}
150
151impl DatabaseCredentials {
152 pub async fn encrypt(
153 &mut self,
154 database: &crate::database::Database,
155 ) -> Result<(), anyhow::Error> {
156 match self {
157 DatabaseCredentials::ConnectionString { connection_string } => {
158 *connection_string = database.encrypt_base64(connection_string.clone()).await?;
159 }
160 DatabaseCredentials::Details { password, .. } => {
161 *password = database.encrypt_base64(password.clone()).await?;
162 }
163 }
164
165 Ok(())
166 }
167
168 pub async fn decrypt(
169 &mut self,
170 database: &crate::database::Database,
171 ) -> Result<(), anyhow::Error> {
172 match self {
173 DatabaseCredentials::ConnectionString { connection_string } => {
174 if let Some(decrypted) =
175 database.decrypt_base64_optional(&connection_string).await?
176 {
177 *connection_string = decrypted;
178 }
179 }
180 DatabaseCredentials::Details { password, .. } => {
181 if let Some(decrypted) = database.decrypt_base64_optional(&password).await? {
182 *password = decrypted;
183 }
184 }
185 }
186
187 Ok(())
188 }
189
190 pub async fn parse_connection_details(
191 &self,
192 database: &crate::database::Database,
193 ) -> Result<ParsedConnectionDetails, anyhow::Error> {
194 match self {
195 DatabaseCredentials::ConnectionString { connection_string } => {
196 let connection_string = database.decrypt_base64(connection_string).await?;
197 let url = reqwest::Url::parse(connection_string.as_str())?;
198 let database_type = DatabaseType::from_url_scheme(url.scheme())?;
199
200 let host = url
201 .host_str()
202 .ok_or_else(|| anyhow::anyhow!("Invalid host"))?
203 .into();
204 let port = url.port().unwrap_or_else(|| database_type.default_port());
205 let username = url.username().into();
206
207 Ok(ParsedConnectionDetails {
208 host,
209 port,
210 username,
211 })
212 }
213 DatabaseCredentials::Details {
214 host,
215 port,
216 username,
217 ..
218 } => Ok(ParsedConnectionDetails {
219 host: host.clone(),
220 port: *port,
221 username: username.clone(),
222 }),
223 }
224 }
225}
226
227impl Censor for DatabaseCredentials {
228 fn censor(&mut self) {
229 match self {
230 DatabaseCredentials::ConnectionString { connection_string } => {
231 *connection_string = CENSORED_PLACEHOLDER.into();
232 }
233 DatabaseCredentials::Details { password, .. } => {
234 *password = CENSORED_PLACEHOLDER.into();
235 }
236 }
237 }
238}
239
240#[derive(Serialize, Deserialize, Clone)]
241pub struct DatabaseHost {
242 pub uuid: uuid::Uuid,
243
244 pub name: compact_str::CompactString,
245 pub r#type: DatabaseType,
246
247 pub deployment_enabled: bool,
248 pub maintenance_enabled: bool,
249
250 pub public_host: Option<compact_str::CompactString>,
251 pub public_port: Option<i32>,
252
253 pub credentials: DatabaseCredentials,
254
255 pub created: chrono::NaiveDateTime,
256
257 extension_data: super::ModelExtensionData,
258}
259
260impl BaseModel for DatabaseHost {
261 const NAME: &'static str = "database_host";
262
263 fn get_extension_list() -> &'static super::ModelExtensionList {
264 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
265 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
266
267 &EXTENSIONS
268 }
269
270 fn get_extension_data(&self) -> &super::ModelExtensionData {
271 &self.extension_data
272 }
273
274 #[inline]
275 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
276 let prefix = prefix.unwrap_or_default();
277
278 BTreeMap::from([
279 (
280 "database_hosts.uuid",
281 compact_str::format_compact!("{prefix}uuid"),
282 ),
283 (
284 "database_hosts.name",
285 compact_str::format_compact!("{prefix}name"),
286 ),
287 (
288 "database_hosts.type",
289 compact_str::format_compact!("{prefix}type"),
290 ),
291 (
292 "database_hosts.deployment_enabled",
293 compact_str::format_compact!("{prefix}deployment_enabled"),
294 ),
295 (
296 "database_hosts.maintenance_enabled",
297 compact_str::format_compact!("{prefix}maintenance_enabled"),
298 ),
299 (
300 "database_hosts.public_host",
301 compact_str::format_compact!("{prefix}public_host"),
302 ),
303 (
304 "database_hosts.public_port",
305 compact_str::format_compact!("{prefix}public_port"),
306 ),
307 (
308 "database_hosts.credentials",
309 compact_str::format_compact!("{prefix}credentials"),
310 ),
311 (
312 "database_hosts.created",
313 compact_str::format_compact!("{prefix}created"),
314 ),
315 ])
316 }
317
318 #[inline]
319 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
320 let prefix = prefix.unwrap_or_default();
321
322 Ok(Self {
323 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
324 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
325 r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
326 deployment_enabled: row
327 .try_get(compact_str::format_compact!("{prefix}deployment_enabled").as_str())?,
328 maintenance_enabled: row
329 .try_get(compact_str::format_compact!("{prefix}maintenance_enabled").as_str())?,
330 public_host: row
331 .try_get(compact_str::format_compact!("{prefix}public_host").as_str())?,
332 public_port: row
333 .try_get(compact_str::format_compact!("{prefix}public_port").as_str())?,
334 credentials: serde_json::from_value(
335 row.try_get(compact_str::format_compact!("{prefix}credentials").as_str())?,
336 )?,
337 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
338 extension_data: Self::map_extensions(prefix, row)?,
339 })
340 }
341}
342
343impl DatabaseHost {
344 pub async fn get_connection(
345 &mut self,
346 database: &crate::database::Database,
347 ) -> Result<DatabasePool, crate::database::DatabaseError> {
348 let mut clients = DATABASE_CLIENTS.lock().await;
349
350 if let Some((last_used, pool)) = clients.get_mut(&self.uuid) {
351 *last_used = std::time::Instant::now();
352
353 return Ok(pool.clone());
354 }
355
356 drop(clients);
357
358 self.credentials.decrypt(database).await?;
359
360 let pool = match self.r#type {
361 DatabaseType::Mysql => {
362 let options = match &self.credentials {
363 DatabaseCredentials::ConnectionString { connection_string } => {
364 sqlx::mysql::MySqlConnectOptions::from_str(connection_string).map_err(
365 |err| anyhow::anyhow!("failed to parse MySQL connection string: {err}"),
366 )?
367 }
368 DatabaseCredentials::Details {
369 host,
370 port,
371 username,
372 password,
373 } => sqlx::mysql::MySqlConnectOptions::new()
374 .host(host)
375 .port(*port)
376 .username(username)
377 .password(password),
378 };
379
380 let pool = sqlx::Pool::connect_with(options).await?;
381 DatabasePool::Mysql(pool)
382 }
383 DatabaseType::Postgres => {
384 let options = match &self.credentials {
385 DatabaseCredentials::ConnectionString { connection_string } => {
386 sqlx::postgres::PgConnectOptions::from_str(connection_string).map_err(
387 |err| {
388 anyhow::anyhow!("failed to parse Postgres connection string: {err}")
389 },
390 )?
391 }
392 DatabaseCredentials::Details {
393 host,
394 port,
395 username,
396 password,
397 } => sqlx::postgres::PgConnectOptions::new()
398 .host(host)
399 .port(*port)
400 .username(username)
401 .password(password)
402 .database("postgres"),
403 };
404
405 let pool = sqlx::Pool::connect_with(options).await?;
406 DatabasePool::Postgres(pool)
407 }
408 DatabaseType::Mongodb => {
409 let options = match &self.credentials {
410 DatabaseCredentials::ConnectionString { connection_string } => {
411 mongodb::options::ClientOptions::parse(connection_string.as_str())
412 .await
413 .map_err(|err| {
414 anyhow::anyhow!("failed to parse MongoDB connection string: {err}")
415 })?
416 }
417 DatabaseCredentials::Details {
418 host,
419 port,
420 username,
421 password,
422 } => {
423 let mut options = mongodb::options::ClientOptions::default();
424 options.hosts.push(mongodb::options::ServerAddress::Tcp {
425 host: host.to_string(),
426 port: Some(*port),
427 });
428 options.credential = Some(
429 mongodb::options::Credential::builder()
430 .username(username.to_string())
431 .password(password.to_string())
432 .build(),
433 );
434 options
435 }
436 };
437
438 let client = mongodb::Client::with_options(options)?;
439 DatabasePool::Mongodb(client)
440 }
441 };
442
443 DATABASE_CLIENTS
444 .lock()
445 .await
446 .insert(self.uuid, (std::time::Instant::now(), pool.clone()));
447 Ok(pool)
448 }
449
450 pub async fn all_with_pagination(
451 database: &crate::database::Database,
452 page: i64,
453 per_page: i64,
454 search: Option<&str>,
455 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
456 let offset = (page - 1) * per_page;
457
458 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
459 r#"
460 SELECT {}, COUNT(*) OVER() AS total_count
461 FROM database_hosts
462 WHERE ($1 IS NULL OR database_hosts.name ILIKE '%' || $1 || '%')
463 ORDER BY database_hosts.created
464 LIMIT $2 OFFSET $3
465 "#,
466 Self::columns_sql(None)
467 )))
468 .bind(search)
469 .bind(per_page)
470 .bind(offset)
471 .fetch_all(database.read())
472 .await?;
473
474 Ok(super::Pagination {
475 total: rows
476 .first()
477 .map_or(Ok(0), |row| row.try_get("total_count"))?,
478 per_page,
479 page,
480 data: rows
481 .into_iter()
482 .map(|row| Self::map(None, &row))
483 .try_collect_vec()?,
484 })
485 }
486
487 pub async fn by_node_uuid(
488 database: &crate::database::Database,
489 node: &super::node::Node,
490 uuid: uuid::Uuid,
491 ) -> Result<Option<Self>, crate::database::DatabaseError> {
492 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
493 r#"
494 SELECT {}
495 FROM database_hosts
496 WHERE database_hosts.uuid = $3
497 AND (
498 EXISTS (
499 SELECT 1 FROM node_database_hosts
500 WHERE node_database_hosts.database_host_uuid = database_hosts.uuid AND node_database_hosts.node_uuid = $1
501 )
502 OR EXISTS (
503 SELECT 1 FROM location_database_hosts
504 WHERE location_database_hosts.database_host_uuid = database_hosts.uuid AND location_database_hosts.location_uuid = $2
505 )
506 )
507 "#,
508 Self::columns_sql(None)
509 )))
510 .bind(node.uuid)
511 .bind(node.location.uuid)
512 .bind(uuid)
513 .fetch_optional(database.read())
514 .await?;
515
516 row.try_map(|row| Self::map(None, &row))
517 }
518
519 pub async fn all_public_by_node(
520 database: &crate::database::Database,
521 node: &super::node::Node,
522 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
523 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
524 r#"
525 SELECT {}
526 FROM database_hosts
527 WHERE database_hosts.deployment_enabled
528 AND (
529 EXISTS (
530 SELECT 1 FROM node_database_hosts
531 WHERE node_database_hosts.database_host_uuid = database_hosts.uuid AND node_database_hosts.node_uuid = $1
532 )
533 OR EXISTS (
534 SELECT 1 FROM location_database_hosts
535 WHERE location_database_hosts.database_host_uuid = database_hosts.uuid AND location_database_hosts.location_uuid = $2
536 )
537 )
538 ORDER BY database_hosts.created DESC
539 "#,
540 Self::columns_sql(None)
541 )))
542 .bind(node.uuid)
543 .bind(node.location.uuid)
544 .fetch_all(database.read())
545 .await?;
546
547 rows.into_iter()
548 .map(|row| Self::map(None, &row))
549 .try_collect_vec()
550 }
551}
552
553#[async_trait::async_trait]
554impl IntoAdminApiObject for DatabaseHost {
555 type AdminApiObject = AdminApiDatabaseHost;
556 type ExtraArgs<'a> = ();
557
558 async fn into_admin_api_object<'a>(
559 mut self,
560 state: &crate::State,
561 _args: Self::ExtraArgs<'a>,
562 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
563 let api_object = AdminApiDatabaseHost::init_hooks(&self, state).await?;
564 self.credentials.censor();
565
566 let api_object = finish_extendible!(
567 AdminApiDatabaseHost {
568 uuid: self.uuid,
569 name: self.name,
570 r#type: self.r#type,
571 deployment_enabled: self.deployment_enabled,
572 maintenance_enabled: self.maintenance_enabled,
573 public_host: self.public_host,
574 public_port: self.public_port,
575 credentials: self.credentials,
576 created: self.created.and_utc(),
577 },
578 api_object,
579 state
580 )?;
581
582 Ok(api_object)
583 }
584}
585
586#[async_trait::async_trait]
587impl IntoApiObject for DatabaseHost {
588 type ApiObject = ApiDatabaseHost;
589 type ExtraArgs<'a> = ();
590
591 async fn into_api_object<'a>(
592 self,
593 state: &crate::State,
594 _args: Self::ExtraArgs<'a>,
595 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
596 let api_object = ApiDatabaseHost::init_hooks(&self, state).await?;
597 let details = self
598 .credentials
599 .parse_connection_details(&state.database)
600 .await?;
601
602 let api_object = finish_extendible!(
603 ApiDatabaseHost {
604 uuid: self.uuid,
605 name: self.name,
606 maintenance_enabled: self.maintenance_enabled,
607 r#type: self.r#type,
608 host: self.public_host.unwrap_or(details.host),
609 port: self.public_port.unwrap_or(details.port as i32),
610 },
611 api_object,
612 state
613 )?;
614
615 Ok(api_object)
616 }
617}
618
619#[async_trait::async_trait]
620impl ByUuid for DatabaseHost {
621 async fn by_uuid(
622 database: &crate::database::Database,
623 uuid: uuid::Uuid,
624 ) -> Result<Self, crate::database::DatabaseError> {
625 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
626 r#"
627 SELECT {}
628 FROM database_hosts
629 WHERE database_hosts.uuid = $1
630 "#,
631 Self::columns_sql(None)
632 )))
633 .bind(uuid)
634 .fetch_one(database.read())
635 .await?;
636
637 Self::map(None, &row)
638 }
639
640 async fn by_uuid_with_transaction(
641 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
642 uuid: uuid::Uuid,
643 ) -> Result<Self, crate::database::DatabaseError> {
644 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
645 r#"
646 SELECT {}
647 FROM database_hosts
648 WHERE database_hosts.uuid = $1
649 "#,
650 Self::columns_sql(None)
651 )))
652 .bind(uuid)
653 .fetch_one(&mut **transaction)
654 .await?;
655
656 Self::map(None, &row)
657 }
658}
659
660#[derive(ToSchema, Deserialize, Validate)]
661pub struct CreateDatabaseHostOptions {
662 #[garde(length(chars, min = 1, max = 255))]
663 #[schema(min_length = 1, max_length = 255)]
664 pub name: compact_str::CompactString,
665 #[garde(skip)]
666 pub r#type: DatabaseType,
667
668 #[garde(skip)]
669 pub deployment_enabled: bool,
670 #[garde(skip)]
671 pub maintenance_enabled: bool,
672
673 #[garde(length(chars, min = 3, max = 255))]
674 #[schema(min_length = 3, max_length = 255)]
675 pub public_host: Option<compact_str::CompactString>,
676 #[garde(range(min = 1))]
677 #[schema(minimum = 1)]
678 pub public_port: Option<u16>,
679
680 #[garde(dive)]
681 pub credentials: DatabaseCredentials,
682}
683
684#[async_trait::async_trait]
685impl CreatableModel for DatabaseHost {
686 type CreateOptions<'a> = CreateDatabaseHostOptions;
687 type CreateResult = Self;
688
689 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
690 static CREATE_LISTENERS: LazyLock<CreateListenerList<DatabaseHost>> =
691 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
692
693 &CREATE_LISTENERS
694 }
695
696 async fn create_with_transaction(
697 state: &crate::State,
698 mut options: Self::CreateOptions<'_>,
699 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
700 ) -> Result<Self, crate::database::DatabaseError> {
701 options.validate()?;
702
703 let mut query_builder = InsertQueryBuilder::new("database_hosts");
704
705 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
706
707 options.credentials.encrypt(&state.database).await?;
708
709 query_builder
710 .set("name", &options.name)
711 .set("type", options.r#type)
712 .set("deployment_enabled", options.deployment_enabled)
713 .set("maintenance_enabled", options.maintenance_enabled)
714 .set("public_host", &options.public_host)
715 .set("public_port", options.public_port.map(|p| p as i32))
716 .set("credentials", serde_json::to_value(&options.credentials)?);
717
718 let row = query_builder
719 .returning(&Self::columns_sql(None))
720 .fetch_one(&mut **transaction)
721 .await?;
722 let mut database_host = Self::map(None, &row)?;
723
724 Self::run_after_create_handlers(&mut database_host, &options, state, transaction).await?;
725
726 Ok(database_host)
727 }
728}
729
730#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
731pub struct UpdateDatabaseHostOptions {
732 #[garde(length(chars, min = 1, max = 255))]
733 #[schema(min_length = 1, max_length = 255)]
734 pub name: Option<compact_str::CompactString>,
735
736 #[garde(skip)]
737 pub deployment_enabled: Option<bool>,
738 #[garde(skip)]
739 pub maintenance_enabled: Option<bool>,
740
741 #[garde(length(max = 255))]
742 #[schema(max_length = 255)]
743 #[serde(
744 default,
745 skip_serializing_if = "Option::is_none",
746 with = "::serde_with::rust::double_option"
747 )]
748 pub public_host: Option<Option<compact_str::CompactString>>,
749 #[serde(
750 default,
751 skip_serializing_if = "Option::is_none",
752 with = "::serde_with::rust::double_option"
753 )]
754 #[garde(range(min = 1))]
755 #[schema(minimum = 1)]
756 pub public_port: Option<Option<u16>>,
757
758 #[garde(dive)]
759 pub credentials: Option<DatabaseCredentials>,
760}
761
762#[async_trait::async_trait]
763impl UpdatableModel for DatabaseHost {
764 type UpdateOptions = UpdateDatabaseHostOptions;
765
766 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
767 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<DatabaseHost>> =
768 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
769
770 &UPDATE_LISTENERS
771 }
772
773 async fn update_with_transaction(
774 &mut self,
775 state: &crate::State,
776 mut options: Self::UpdateOptions,
777 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
778 ) -> Result<(), crate::database::DatabaseError> {
779 options.validate()?;
780
781 let mut query_builder = UpdateQueryBuilder::new("database_hosts");
782
783 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
784 .await?;
785
786 if let Some(credentials) = &mut options.credentials {
787 credentials.encrypt(&state.database).await?;
788 }
789
790 query_builder
791 .set("name", options.name.as_ref())
792 .set("deployment_enabled", options.deployment_enabled)
793 .set("maintenance_enabled", options.maintenance_enabled)
794 .set("public_host", options.public_host.as_ref())
795 .set(
796 "public_port",
797 options
798 .public_port
799 .as_ref()
800 .map(|p| p.as_ref().map(|port| *port as i32)),
801 )
802 .set(
803 "credentials",
804 options
805 .credentials
806 .as_ref()
807 .map(serde_json::to_value)
808 .transpose()?,
809 )
810 .where_eq("uuid", self.uuid);
811
812 query_builder.execute(&mut **transaction).await?;
813
814 if let Some(name) = options.name {
815 self.name = name;
816 }
817 if let Some(deployment_enabled) = options.deployment_enabled {
818 self.deployment_enabled = deployment_enabled;
819 }
820 if let Some(maintenance_enabled) = options.maintenance_enabled {
821 self.maintenance_enabled = maintenance_enabled;
822 }
823 if let Some(public_host) = options.public_host {
824 self.public_host = public_host;
825 }
826 if let Some(public_port) = options.public_port {
827 self.public_port = public_port.map(|port| port as i32);
828 }
829 if let Some(credentials) = options.credentials {
830 self.credentials = credentials;
831 }
832
833 self.run_after_update_handlers(state, transaction).await?;
834
835 Ok(())
836 }
837}
838
839#[async_trait::async_trait]
840impl DeletableModel for DatabaseHost {
841 type DeleteOptions = ();
842
843 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
844 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<DatabaseHost>> =
845 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
846
847 &DELETE_LISTENERS
848 }
849
850 async fn delete_with_transaction(
851 &self,
852 state: &crate::State,
853 options: Self::DeleteOptions,
854 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
855 ) -> Result<(), anyhow::Error> {
856 self.run_delete_handlers(&options, state, transaction)
857 .await?;
858
859 sqlx::query(
860 r#"
861 DELETE FROM database_hosts
862 WHERE database_hosts.uuid = $1
863 "#,
864 )
865 .bind(self.uuid)
866 .execute(&mut **transaction)
867 .await?;
868
869 self.run_after_delete_handlers(&options, state, transaction)
870 .await?;
871
872 Ok(())
873 }
874}
875
876#[schema_extension_derive::extendible]
877#[init_args(DatabaseHost, crate::State)]
878#[hook_args(crate::State)]
879#[derive(ToSchema, Serialize)]
880#[schema(title = "AdminDatabaseHost")]
881pub struct AdminApiDatabaseHost {
882 pub uuid: uuid::Uuid,
883
884 pub name: compact_str::CompactString,
885 pub deployment_enabled: bool,
886 pub maintenance_enabled: bool,
887 pub r#type: DatabaseType,
888
889 pub public_host: Option<compact_str::CompactString>,
890 pub public_port: Option<i32>,
891
892 pub credentials: DatabaseCredentials,
893
894 pub created: chrono::DateTime<chrono::Utc>,
895}
896
897#[schema_extension_derive::extendible]
898#[init_args(DatabaseHost, crate::State)]
899#[hook_args(crate::State)]
900#[derive(ToSchema, Serialize)]
901#[schema(title = "DatabaseHost")]
902pub struct ApiDatabaseHost {
903 pub uuid: uuid::Uuid,
904
905 pub name: compact_str::CompactString,
906 pub maintenance_enabled: bool,
907 pub r#type: DatabaseType,
908
909 pub host: compact_str::CompactString,
910 pub port: i32,
911}