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