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