Skip to main content

shared/models/
server_database.rs

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}