1use crate::{
2 models::{
3 InsertQueryBuilder, UpdateQueryBuilder,
4 database_host::{DatabaseTransaction, DatabaseType},
5 },
6 prelude::*,
7};
8use garde::Validate;
9use rand::distr::SampleString;
10use serde::{Deserialize, Serialize};
11use sqlx::{Row, postgres::PgRow};
12use std::{
13 collections::BTreeMap,
14 sync::{Arc, LazyLock},
15};
16use utoipa::ToSchema;
17
18#[derive(Serialize, Deserialize, Clone)]
19pub struct ServerDatabase {
20 pub uuid: uuid::Uuid,
21 pub server: Fetchable<super::server::Server>,
22 pub database_host: super::database_host::DatabaseHost,
23
24 pub name: compact_str::CompactString,
25 pub locked: bool,
26
27 pub username: compact_str::CompactString,
28 pub password: Vec<u8>,
29
30 pub created: chrono::NaiveDateTime,
31
32 extension_data: super::ModelExtensionData,
33}
34
35impl BaseModel for ServerDatabase {
36 const NAME: &'static str = "server_database";
37
38 fn get_extension_list() -> &'static super::ModelExtensionList {
39 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
40 LazyLock::new(|| std::sync::RwLock::new(Vec::new()));
41
42 &EXTENSIONS
43 }
44
45 fn get_extension_data(&self) -> &super::ModelExtensionData {
46 &self.extension_data
47 }
48
49 #[inline]
50 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
51 let prefix = prefix.unwrap_or_default();
52
53 let mut columns = BTreeMap::from([
54 (
55 "server_databases.uuid",
56 compact_str::format_compact!("{prefix}uuid"),
57 ),
58 (
59 "server_databases.server_uuid",
60 compact_str::format_compact!("{prefix}server_uuid"),
61 ),
62 (
63 "server_databases.name",
64 compact_str::format_compact!("{prefix}name"),
65 ),
66 (
67 "server_databases.locked",
68 compact_str::format_compact!("{prefix}locked"),
69 ),
70 (
71 "server_databases.username",
72 compact_str::format_compact!("{prefix}username"),
73 ),
74 (
75 "server_databases.password",
76 compact_str::format_compact!("{prefix}password"),
77 ),
78 (
79 "server_databases.created",
80 compact_str::format_compact!("{prefix}created"),
81 ),
82 ]);
83
84 columns.extend(super::database_host::DatabaseHost::base_columns(Some(
85 "database_host_",
86 )));
87
88 columns
89 }
90
91 #[inline]
92 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
93 let prefix = prefix.unwrap_or_default();
94
95 Ok(Self {
96 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
97 server: super::server::Server::get_fetchable(
98 row.try_get(compact_str::format_compact!("{prefix}server_uuid").as_str())?,
99 ),
100 database_host: super::database_host::DatabaseHost::map(Some("database_host_"), row)?,
101 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
102 locked: row.try_get(compact_str::format_compact!("{prefix}locked").as_str())?,
103 username: row.try_get(compact_str::format_compact!("{prefix}username").as_str())?,
104 password: row.try_get(compact_str::format_compact!("{prefix}password").as_str())?,
105 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
106 extension_data: Self::map_extensions(prefix, row)?,
107 })
108 }
109}
110
111impl ServerDatabase {
112 pub async fn by_server_uuid_uuid(
113 database: &crate::database::Database,
114 server_uuid: uuid::Uuid,
115 uuid: uuid::Uuid,
116 ) -> Result<Option<Self>, crate::database::DatabaseError> {
117 let row = sqlx::query(&format!(
118 r#"
119 SELECT {}
120 FROM server_databases
121 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
122 WHERE server_databases.server_uuid = $1 AND server_databases.uuid = $2
123 "#,
124 Self::columns_sql(None)
125 ))
126 .bind(server_uuid)
127 .bind(uuid)
128 .fetch_optional(database.read())
129 .await?;
130
131 row.try_map(|row| Self::map(None, &row))
132 }
133
134 pub async fn by_database_host_uuid_with_pagination(
135 database: &crate::database::Database,
136 database_host_uuid: uuid::Uuid,
137 page: i64,
138 per_page: i64,
139 search: Option<&str>,
140 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
141 let offset = (page - 1) * per_page;
142
143 let rows = sqlx::query(&format!(
144 r#"
145 SELECT {}, COUNT(*) OVER() AS total_count
146 FROM server_databases
147 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
148 WHERE server_databases.database_host_uuid = $1 AND ($2 IS NULL OR server_databases.name ILIKE '%' || $2 || '%')
149 ORDER BY server_databases.created
150 LIMIT $3 OFFSET $4
151 "#,
152 Self::columns_sql(None)
153 ))
154 .bind(database_host_uuid)
155 .bind(search)
156 .bind(per_page)
157 .bind(offset)
158 .fetch_all(database.read())
159 .await?;
160
161 Ok(super::Pagination {
162 total: rows
163 .first()
164 .map_or(Ok(0), |row| row.try_get("total_count"))?,
165 per_page,
166 page,
167 data: rows
168 .into_iter()
169 .map(|row| Self::map(None, &row))
170 .try_collect_vec()?,
171 })
172 }
173
174 pub async fn by_server_uuid_with_pagination(
175 database: &crate::database::Database,
176 server_uuid: uuid::Uuid,
177 page: i64,
178 per_page: i64,
179 search: Option<&str>,
180 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
181 let offset = (page - 1) * per_page;
182
183 let rows = sqlx::query(&format!(
184 r#"
185 SELECT {}, COUNT(*) OVER() AS total_count
186 FROM server_databases
187 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
188 WHERE server_databases.server_uuid = $1 AND ($2 IS NULL OR server_databases.name ILIKE '%' || $2 || '%')
189 ORDER BY server_databases.created
190 LIMIT $3 OFFSET $4
191 "#,
192 Self::columns_sql(None)
193 ))
194 .bind(server_uuid)
195 .bind(search)
196 .bind(per_page)
197 .bind(offset)
198 .fetch_all(database.read())
199 .await?;
200
201 Ok(super::Pagination {
202 total: rows
203 .first()
204 .map_or(Ok(0), |row| row.try_get("total_count"))?,
205 per_page,
206 page,
207 data: rows
208 .into_iter()
209 .map(|row| Self::map(None, &row))
210 .try_collect_vec()?,
211 })
212 }
213
214 pub async fn all_by_server_uuid(
215 database: &crate::database::Database,
216 server_uuid: uuid::Uuid,
217 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
218 let rows = sqlx::query(&format!(
219 r#"
220 SELECT {}
221 FROM server_databases
222 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
223 WHERE server_databases.server_uuid = $1
224 "#,
225 Self::columns_sql(None)
226 ))
227 .bind(server_uuid)
228 .fetch_all(database.read())
229 .await?;
230
231 rows.into_iter()
232 .map(|row| Self::map(None, &row))
233 .try_collect_vec()
234 }
235
236 pub async fn count_by_server_uuid(
237 database: &crate::database::Database,
238 server_uuid: uuid::Uuid,
239 ) -> Result<i64, sqlx::Error> {
240 sqlx::query_scalar(
241 r#"
242 SELECT COUNT(*)
243 FROM server_databases
244 WHERE server_databases.server_uuid = $1
245 "#,
246 )
247 .bind(server_uuid)
248 .fetch_one(database.read())
249 .await
250 }
251
252 pub async fn count_by_database_host_uuid(
253 database: &crate::database::Database,
254 database_host_uuid: uuid::Uuid,
255 ) -> Result<i64, sqlx::Error> {
256 sqlx::query_scalar(
257 r#"
258 SELECT COUNT(*)
259 FROM server_databases
260 WHERE server_databases.database_host_uuid = $1
261 "#,
262 )
263 .bind(database_host_uuid)
264 .fetch_one(database.read())
265 .await
266 }
267
268 pub async fn rotate_password(
269 &mut self,
270 database: &crate::database::Database,
271 ) -> Result<String, anyhow::Error> {
272 let new_password = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 24);
273
274 match self.database_host.get_connection(database).await? {
275 crate::models::database_host::DatabasePool::Mysql(pool) => {
276 sqlx::query(&format!(
277 "ALTER USER '{}'@'%' IDENTIFIED BY '{}'",
278 self.username, new_password
279 ))
280 .execute(&pool)
281 .await?;
282 }
283 crate::models::database_host::DatabasePool::Postgres(pool) => {
284 sqlx::query(&format!(
285 "ALTER USER \"{}\" WITH PASSWORD '{}'",
286 self.username, new_password
287 ))
288 .execute(&pool)
289 .await?;
290 }
291 crate::models::database_host::DatabasePool::Mongodb(client) => {
292 let cmd = mongodb::bson::doc! {
293 "updateUser": self.username.to_string(),
294 "pwd": &new_password
295 };
296 client.database(&self.name).run_command(cmd).await?;
297 }
298 }
299
300 sqlx::query(
301 r#"
302 UPDATE server_databases
303 SET password = $1
304 WHERE server_databases.uuid = $2
305 "#,
306 )
307 .bind(database.encrypt(new_password.clone()).await?)
308 .bind(self.uuid)
309 .execute(database.write())
310 .await?;
311
312 Ok(new_password)
313 }
314
315 pub async fn get_size(
316 &mut self,
317 database: &crate::database::Database,
318 ) -> Result<i64, crate::database::DatabaseError> {
319 match self.database_host.get_connection(database).await? {
320 crate::models::database_host::DatabasePool::Mysql(pool) => {
321 let row = sqlx::query(
322 "SELECT CAST(SUM(data_length + index_length) AS INTEGER) FROM information_schema.tables WHERE table_schema = ?",
323 )
324 .bind(&self.name)
325 .fetch_one(&pool)
326 .await?;
327
328 Ok(row.get::<Option<i64>, _>(0).unwrap_or(0))
329 }
330 crate::models::database_host::DatabasePool::Postgres(pool) => {
331 let row = sqlx::query("SELECT pg_database_size($1)")
332 .bind(&self.name)
333 .fetch_one(&pool)
334 .await?;
335
336 Ok(row.get::<Option<i64>, _>(0).unwrap_or(0))
337 }
338 crate::models::database_host::DatabasePool::Mongodb(client) => {
339 let cmd = mongodb::bson::doc! { "dbStats": 1, "scale": 1 };
340 let stats = client.database(&self.name).run_command(cmd).await?;
341
342 let size = match stats.get("dataSize") {
343 Some(mongodb::bson::Bson::Int32(i)) => *i as i64,
344 Some(mongodb::bson::Bson::Int64(i)) => *i,
345 Some(mongodb::bson::Bson::Double(f)) => *f as i64,
346 _ => 0,
347 };
348
349 Ok(size)
350 }
351 }
352 }
353
354 pub async fn recreate(
355 &mut self,
356 database: &crate::database::Database,
357 ) -> Result<(), anyhow::Error> {
358 let mut run_recreate = async || {
359 match self.database_host.get_connection(database).await? {
360 crate::models::database_host::DatabasePool::Mysql(pool) => {
361 sqlx::query(&format!("DROP DATABASE IF EXISTS `{}`", self.name))
362 .execute(&pool)
363 .await?;
364 sqlx::query(&format!("CREATE DATABASE `{}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;", self.name))
365 .execute(&pool)
366 .await?;
367 }
368 crate::models::database_host::DatabasePool::Postgres(pool) => {
369 sqlx::query(&format!("DROP DATABASE IF EXISTS \"{}\"", self.name))
370 .execute(&pool)
371 .await?;
372 sqlx::query(&format!(
373 "CREATE DATABASE \"{}\" WITH OWNER \"{}\" ENCODING 'UTF8'",
374 self.name, self.username
375 ))
376 .execute(&pool)
377 .await?;
378 }
379 crate::models::database_host::DatabasePool::Mongodb(client) => {
380 let db = client.database(&self.name);
381
382 let drop_user_cmd =
383 mongodb::bson::doc! { "dropUser": self.username.to_string() };
384 let _ = db.run_command(drop_user_cmd).await;
385
386 db.drop().await?;
387
388 let password = database.decrypt(self.password.clone()).await?;
389 let cmd = mongodb::bson::doc! {
390 "createUser": self.username.to_string(),
391 "pwd": password.into_string(),
392 "roles": [
393 { "role": "readWrite", "db": self.name.to_string() }
394 ]
395 };
396 db.run_command(cmd).await?;
397 }
398 }
399
400 Ok::<(), anyhow::Error>(())
401 };
402
403 if let Err(err) = run_recreate().await {
404 if err
405 .downcast_ref::<sqlx::Error>()
406 .and_then(|e| e.as_database_error())
407 .is_some_and(|e| e.message().contains("is being accessed"))
408 {
409 return Err(crate::response::DisplayError::new(
410 "this database is being accessed, unable to recreate.",
411 )
412 .into());
413 }
414
415 return Err(err);
416 }
417
418 Ok(())
419 }
420}
421
422#[async_trait::async_trait]
423impl IntoAdminApiObject for ServerDatabase {
424 type AdminApiObject = AdminApiServerDatabase;
425 type ExtraArgs<'a> = &'a crate::storage::StorageUrlRetriever<'a>;
426
427 async fn into_admin_api_object<'a>(
428 self,
429 state: &crate::State,
430 storage_url_retriever: Self::ExtraArgs<'a>,
431 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
432 let api_object = AdminApiServerDatabase::init_hooks(&self, state).await?;
433
434 let details = self
435 .database_host
436 .credentials
437 .parse_connection_details(&state.database)
438 .await?;
439
440 let api_object = finish_extendible!(
441 AdminApiServerDatabase {
442 uuid: self.uuid,
443 server: self
444 .server
445 .fetch_cached(&state.database)
446 .await?
447 .into_admin_api_object(state, storage_url_retriever)
448 .await?,
449 r#type: self.database_host.r#type,
450 host: self.database_host.public_host.unwrap_or(details.host),
451 port: self
452 .database_host
453 .public_port
454 .unwrap_or(details.port as i32),
455 name: self.name,
456 is_locked: self.locked,
457 username: self.username,
458 password: state.database.decrypt(self.password).await?,
459 created: self.created.and_utc(),
460 },
461 api_object,
462 state
463 )?;
464
465 Ok(api_object)
466 }
467}
468
469#[async_trait::async_trait]
470impl IntoApiObject for ServerDatabase {
471 type ApiObject = ApiServerDatabase;
472 type ExtraArgs<'a> = bool;
473
474 async fn into_api_object<'a>(
475 self,
476 state: &crate::State,
477 show_password: Self::ExtraArgs<'a>,
478 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
479 let api_object = ApiServerDatabase::init_hooks(&self, state).await?;
480
481 let mut username = self.username;
482 let space_idx = username.find(' ');
483
484 if let Some(space_idx) = space_idx {
485 username.truncate(space_idx);
486 }
487
488 let details = self
489 .database_host
490 .credentials
491 .parse_connection_details(&state.database)
492 .await?;
493
494 let api_object = finish_extendible!(
495 ApiServerDatabase {
496 uuid: self.uuid,
497 r#type: self.database_host.r#type,
498 host: self.database_host.public_host.unwrap_or(details.host),
499 port: self
500 .database_host
501 .public_port
502 .unwrap_or(details.port as i32),
503 name: self.name,
504 is_locked: self.locked,
505 username,
506 password: if show_password {
507 Some(state.database.decrypt(self.password).await?)
508 } else {
509 None
510 },
511 created: self.created.and_utc(),
512 },
513 api_object,
514 state
515 )?;
516
517 Ok(api_object)
518 }
519}
520
521#[async_trait::async_trait]
522impl ByUuid for ServerDatabase {
523 async fn by_uuid(
524 database: &crate::database::Database,
525 uuid: uuid::Uuid,
526 ) -> Result<Self, crate::database::DatabaseError> {
527 let row = sqlx::query(&format!(
528 r#"
529 SELECT {}
530 FROM server_databases
531 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
532 WHERE server_databases.uuid = $1
533 "#,
534 Self::columns_sql(None)
535 ))
536 .bind(uuid)
537 .fetch_one(database.read())
538 .await?;
539
540 Self::map(None, &row)
541 }
542
543 async fn by_uuid_with_transaction(
544 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
545 uuid: uuid::Uuid,
546 ) -> Result<Self, crate::database::DatabaseError> {
547 let row = sqlx::query(&format!(
548 r#"
549 SELECT {}
550 FROM server_databases
551 JOIN database_hosts ON database_hosts.uuid = server_databases.database_host_uuid
552 WHERE server_databases.uuid = $1
553 "#,
554 Self::columns_sql(None)
555 ))
556 .bind(uuid)
557 .fetch_one(&mut **transaction)
558 .await?;
559
560 Self::map(None, &row)
561 }
562}
563
564#[derive(Validate)]
565pub struct CreateServerDatabaseOptions<'a> {
566 #[garde(skip)]
567 pub server: &'a super::server::Server,
568 #[garde(skip)]
569 pub database_host: &'a super::database_host::DatabaseHost,
570
571 #[garde(length(chars, min = 3, max = 31), pattern("^[a-zA-Z0-9_]+$"))]
572 pub name: compact_str::CompactString,
573}
574
575#[async_trait::async_trait]
576impl CreatableModel for ServerDatabase {
577 type CreateOptions<'a> = CreateServerDatabaseOptions<'a>;
578 type CreateResult = Self;
579
580 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
581 static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerDatabase>> =
582 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
583
584 &CREATE_LISTENERS
585 }
586
587 async fn create_with_transaction(
588 state: &crate::State,
589 mut options: Self::CreateOptions<'_>,
590 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
591 ) -> Result<Self, crate::database::DatabaseError> {
592 options.validate()?;
593
594 let server_id = format!("{:08x}", options.server.uuid_short);
595 let name = format!("s{}_{}", server_id, options.name);
596 let username = format!(
597 "u{}_{}",
598 server_id,
599 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 10)
600 );
601 let password = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 24);
602
603 let db_transaction: DatabaseTransaction = match options
604 .database_host
605 .clone()
606 .get_connection(&state.database)
607 .await?
608 {
609 crate::models::database_host::DatabasePool::Mysql(pool) => {
610 let mut transaction = pool.begin().await?;
611
612 sqlx::query(&format!(
613 "CREATE USER IF NOT EXISTS '{username}'@'%' IDENTIFIED BY '{password}'"
614 ))
615 .execute(&mut *transaction)
616 .await?;
617 sqlx::query(&format!("CREATE DATABASE IF NOT EXISTS `{name}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"))
618 .execute(&mut *transaction)
619 .await?;
620 sqlx::query(&format!(
621 "GRANT ALL PRIVILEGES ON `{name}`.* TO '{username}'@'%' WITH GRANT OPTION"
622 ))
623 .execute(&mut *transaction)
624 .await?;
625
626 DatabaseTransaction::Mysql(transaction)
627 }
628 crate::models::database_host::DatabasePool::Postgres(pool) => {
629 let transaction = pool.begin().await?;
630
631 sqlx::query(&format!(
632 "CREATE USER \"{username}\" WITH PASSWORD '{password}'"
633 ))
634 .execute(&pool)
635 .await?;
636 sqlx::query(&format!(
637 "CREATE DATABASE \"{name}\" WITH OWNER \"{username}\" ENCODING 'UTF8'"
638 ))
639 .execute(&pool)
640 .await?;
641
642 DatabaseTransaction::Postgres(transaction, pool)
643 }
644 crate::models::database_host::DatabasePool::Mongodb(client) => {
645 let db = client.database(&name);
646 let cmd = mongodb::bson::doc! {
647 "createUser": &username,
648 "pwd": &password,
649 "roles": [
650 { "role": "readWrite", "db": &name }
651 ]
652 };
653 db.run_command(cmd).await?;
654
655 DatabaseTransaction::Mongodb(client)
656 }
657 };
658
659 let mut query_builder = InsertQueryBuilder::new("server_databases");
660
661 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
662
663 query_builder
664 .set("server_uuid", options.server.uuid)
665 .set("database_host_uuid", options.database_host.uuid)
666 .set("name", &name)
667 .set("username", &username)
668 .set("password", state.database.encrypt(password.clone()).await?);
669
670 let row = match query_builder
671 .returning("uuid")
672 .fetch_one(&mut **transaction)
673 .await
674 {
675 Ok(row) => row,
676 Err(err) => {
677 match db_transaction {
678 DatabaseTransaction::Mysql(db_tx) => {
679 db_tx.rollback().await?;
680 }
681 DatabaseTransaction::Postgres(db_tx, pool) => {
682 db_tx.rollback().await?;
683
684 let drop_database = format!("DROP DATABASE IF EXISTS \"{name}\"");
685 let drop_user = format!("DROP USER IF EXISTS \"{username}\"");
686
687 let (_, _) = tokio::join!(
688 sqlx::query(&drop_database).execute(&pool),
689 sqlx::query(&drop_user).execute(&pool)
690 );
691 }
692 DatabaseTransaction::Mongodb(client) => {
693 let _ = client.database(&name).drop().await;
694 }
695 }
696
697 return Err(err.into());
698 }
699 };
700
701 let uuid: uuid::Uuid = row.try_get("uuid")?;
702
703 match match db_transaction {
704 DatabaseTransaction::Mysql(db_tx) => db_tx.commit().await,
705 DatabaseTransaction::Postgres(db_tx, _) => db_tx.commit().await,
706 DatabaseTransaction::Mongodb(_) => Ok(()),
707 } {
708 Ok(_) => {}
709 Err(err) => {
710 sqlx::query(
711 r#"
712 DELETE FROM server_databases
713 WHERE server_databases.uuid = $1
714 "#,
715 )
716 .bind(uuid)
717 .execute(&mut **transaction)
718 .await
719 .ok();
720
721 return Err(err.into());
722 }
723 }
724
725 let mut result = Self::by_uuid_with_transaction(transaction, uuid).await?;
726
727 Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
728
729 Ok(result)
730 }
731}
732
733#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
734pub struct UpdateServerDatabaseOptions {
735 #[garde(skip)]
736 pub locked: Option<bool>,
737}
738
739#[async_trait::async_trait]
740impl UpdatableModel for ServerDatabase {
741 type UpdateOptions = UpdateServerDatabaseOptions;
742
743 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
744 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerDatabase>> =
745 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
746
747 &UPDATE_LISTENERS
748 }
749
750 async fn update_with_transaction(
751 &mut self,
752 state: &crate::State,
753 mut options: Self::UpdateOptions,
754 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
755 ) -> Result<(), crate::database::DatabaseError> {
756 options.validate()?;
757
758 let mut query_builder = UpdateQueryBuilder::new("server_databases");
759
760 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
761 .await?;
762
763 query_builder
764 .set("locked", options.locked)
765 .where_eq("uuid", self.uuid);
766
767 query_builder.execute(&mut **transaction).await?;
768
769 if let Some(locked) = options.locked {
770 self.locked = locked;
771 }
772
773 self.run_after_update_handlers(state, transaction).await?;
774
775 Ok(())
776 }
777}
778
779#[derive(Clone, Default)]
780pub struct DeleteServerDatabaseOptions {
781 pub force: bool,
782}
783
784#[async_trait::async_trait]
785impl DeletableModel for ServerDatabase {
786 type DeleteOptions = DeleteServerDatabaseOptions;
787
788 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
789 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerDatabase>> =
790 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
791
792 &DELETE_LISTENERS
793 }
794
795 async fn delete_with_transaction(
796 &self,
797 state: &crate::State,
798 options: Self::DeleteOptions,
799 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
800 ) -> Result<(), anyhow::Error> {
801 self.run_delete_handlers(&options, state, transaction)
802 .await?;
803
804 sqlx::query(
805 r#"
806 DELETE FROM server_databases
807 WHERE server_databases.uuid = $1
808 "#,
809 )
810 .bind(self.uuid)
811 .execute(&mut **transaction)
812 .await?;
813
814 self.run_after_delete_handlers(&options, state, transaction)
815 .await?;
816
817 Ok(())
818 }
819
820 async fn delete(
821 &self,
822 state: &crate::State,
823 options: Self::DeleteOptions,
824 ) -> Result<(), anyhow::Error> {
825 if self.name.contains(|c| ['"', '\'', '`'].contains(&c))
826 || self.username.contains(|c| ['"', '\'', '`'].contains(&c))
827 {
828 return Err(anyhow::anyhow!(
829 "unable to delete database with escape characters"
830 ));
831 }
832
833 let connection = self
834 .database_host
835 .clone()
836 .get_connection(&state.database)
837 .await?;
838 let database_name = self.name.clone();
839 let database_username = self.username.trim_end().to_string();
840 let self_clone = self.clone();
841 let state_clone = state.clone();
842
843 tokio::spawn(async move {
844 let run_delete = async || {
845 match connection {
846 crate::models::database_host::DatabasePool::Mysql(pool) => {
847 sqlx::query(&format!("DROP DATABASE IF EXISTS `{}`", database_name))
848 .execute(&pool)
849 .await?;
850 sqlx::query(&format!("DROP USER IF EXISTS '{}'@'%'", database_username))
851 .execute(&pool)
852 .await?;
853 }
854 crate::models::database_host::DatabasePool::Postgres(pool) => {
855 sqlx::query(&format!("DROP DATABASE IF EXISTS \"{}\"", database_name))
856 .execute(&pool)
857 .await?;
858 sqlx::query(&format!("DROP USER IF EXISTS \"{}\"", database_username))
859 .execute(&pool)
860 .await?;
861 }
862 crate::models::database_host::DatabasePool::Mongodb(client) => {
863 let db = client.database(&database_name);
864
865 db.run_command(mongodb::bson::doc! { "dropUser": &database_username })
866 .await?;
867
868 db.drop().await?;
869 }
870 }
871
872 Ok::<_, anyhow::Error>(())
873 };
874
875 if let Err(err) = run_delete().await
876 && !options.force
877 {
878 if err
879 .downcast_ref::<sqlx::Error>()
880 .and_then(|e| e.as_database_error())
881 .is_some_and(|e| e.message().contains("is being accessed"))
882 {
883 return Err(crate::response::DisplayError::new(
884 "this database is being accessed, unable to delete.",
885 )
886 .into());
887 }
888
889 return Err(err);
890 }
891
892 let mut transaction = state_clone.database.write().begin().await?;
893 self_clone
894 .delete_with_transaction(&state_clone, options, &mut transaction)
895 .await?;
896 transaction.commit().await?;
897
898 Ok(())
899 })
900 .await?
901 }
902}
903
904#[schema_extension_derive::extendible]
905#[init_args(ServerDatabase, crate::State)]
906#[hook_args(crate::State)]
907#[derive(ToSchema, Serialize)]
908#[schema(title = "AdminServerDatabase")]
909pub struct AdminApiServerDatabase {
910 pub uuid: uuid::Uuid,
911 pub server: super::server::AdminApiServer,
912
913 pub r#type: DatabaseType,
914 pub host: compact_str::CompactString,
915 pub port: i32,
916
917 pub name: compact_str::CompactString,
918 pub is_locked: bool,
919
920 pub username: compact_str::CompactString,
921 pub password: compact_str::CompactString,
922
923 pub created: chrono::DateTime<chrono::Utc>,
924}
925
926#[schema_extension_derive::extendible]
927#[init_args(ServerDatabase, crate::State)]
928#[hook_args(crate::State)]
929#[derive(ToSchema, Serialize)]
930#[schema(title = "ServerDatabase")]
931pub struct ApiServerDatabase {
932 pub uuid: uuid::Uuid,
933
934 pub r#type: DatabaseType,
935 pub host: compact_str::CompactString,
936 pub port: i32,
937
938 pub name: compact_str::CompactString,
939 pub is_locked: bool,
940
941 pub username: compact_str::CompactString,
942 pub password: Option<compact_str::CompactString>,
943
944 pub created: chrono::DateTime<chrono::Utc>,
945}