shared/models/
location_database_host.rs

1use crate::{models::InsertQueryBuilder, prelude::*};
2use garde::Validate;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6    collections::BTreeMap,
7    sync::{Arc, LazyLock},
8};
9use utoipa::ToSchema;
10
11#[derive(Serialize, Deserialize, Clone)]
12pub struct LocationDatabaseHost {
13    pub location: Fetchable<super::location::Location>,
14    pub database_host: super::database_host::DatabaseHost,
15
16    pub created: chrono::NaiveDateTime,
17}
18
19impl BaseModel for LocationDatabaseHost {
20    const NAME: &'static str = "location_database_host";
21
22    #[inline]
23    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
24        let prefix = prefix.unwrap_or_default();
25
26        let mut columns = BTreeMap::from([
27            (
28                "location_database_hosts.location_uuid",
29                compact_str::format_compact!("{prefix}location_uuid"),
30            ),
31            (
32                "location_database_hosts.created",
33                compact_str::format_compact!("{prefix}created"),
34            ),
35        ]);
36
37        columns.extend(super::database_host::DatabaseHost::columns(Some(
38            "database_host_",
39        )));
40
41        columns
42    }
43
44    #[inline]
45    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
46        let prefix = prefix.unwrap_or_default();
47
48        Ok(Self {
49            location: super::location::Location::get_fetchable(
50                row.try_get(compact_str::format_compact!("{prefix}location_uuid").as_str())?,
51            ),
52            database_host: super::database_host::DatabaseHost::map(Some("database_host_"), row)?,
53            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
54        })
55    }
56}
57
58impl LocationDatabaseHost {
59    pub async fn by_location_uuid_database_host_uuid(
60        database: &crate::database::Database,
61        location_uuid: uuid::Uuid,
62        database_host_uuid: uuid::Uuid,
63    ) -> Result<Option<Self>, crate::database::DatabaseError> {
64        let row = sqlx::query(&format!(
65            r#"
66            SELECT {}
67            FROM location_database_hosts
68            JOIN database_hosts ON location_database_hosts.database_host_uuid = database_hosts.uuid
69            WHERE location_database_hosts.location_uuid = $1 AND location_database_hosts.database_host_uuid = $2
70            "#,
71            Self::columns_sql(None)
72        ))
73        .bind(location_uuid)
74        .bind(database_host_uuid)
75        .fetch_optional(database.read())
76        .await?;
77
78        row.try_map(|row| Self::map(None, &row))
79    }
80
81    pub async fn by_location_uuid_with_pagination(
82        database: &crate::database::Database,
83        location_uuid: uuid::Uuid,
84        page: i64,
85        per_page: i64,
86        search: Option<&str>,
87    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
88        let offset = (page - 1) * per_page;
89
90        let rows = sqlx::query(&format!(
91            r#"
92            SELECT {}, COUNT(*) OVER() AS total_count
93            FROM location_database_hosts
94            JOIN database_hosts ON location_database_hosts.database_host_uuid = database_hosts.uuid
95            WHERE location_database_hosts.location_uuid = $1 AND ($2 IS NULL OR database_hosts.name ILIKE '%' || $2 || '%')
96            ORDER BY location_database_hosts.created
97            LIMIT $3 OFFSET $4
98            "#,
99            Self::columns_sql(None)
100        ))
101        .bind(location_uuid)
102        .bind(search)
103        .bind(per_page)
104        .bind(offset)
105        .fetch_all(database.read())
106        .await?;
107
108        Ok(super::Pagination {
109            total: rows
110                .first()
111                .map_or(Ok(0), |row| row.try_get("total_count"))?,
112            per_page,
113            page,
114            data: rows
115                .into_iter()
116                .map(|row| Self::map(None, &row))
117                .try_collect_vec()?,
118        })
119    }
120
121    pub async fn all_public_by_location_uuid(
122        database: &crate::database::Database,
123        location_uuid: uuid::Uuid,
124    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
125        let rows = sqlx::query(&format!(
126            r#"
127            SELECT {}
128            FROM location_database_hosts
129            JOIN database_hosts ON location_database_hosts.database_host_uuid = database_hosts.uuid
130            WHERE location_database_hosts.location_uuid = $1 AND database_hosts.deployment_enabled
131            ORDER BY location_database_hosts.created DESC
132            "#,
133            Self::columns_sql(None)
134        ))
135        .bind(location_uuid)
136        .fetch_all(database.read())
137        .await?;
138
139        rows.into_iter()
140            .map(|row| Self::map(None, &row))
141            .try_collect_vec()
142    }
143
144    #[inline]
145    pub fn into_admin_api_object(self) -> AdminApiLocationDatabaseHost {
146        AdminApiLocationDatabaseHost {
147            database_host: self.database_host.into_admin_api_object(),
148            created: self.created.and_utc(),
149        }
150    }
151}
152
153#[derive(ToSchema, Deserialize, Validate)]
154pub struct CreateLocationDatabaseHostOptions {
155    #[garde(skip)]
156    pub location_uuid: uuid::Uuid,
157    #[garde(skip)]
158    pub database_host_uuid: uuid::Uuid,
159}
160
161#[async_trait::async_trait]
162impl CreatableModel for LocationDatabaseHost {
163    type CreateOptions<'a> = CreateLocationDatabaseHostOptions;
164    type CreateResult = Self;
165
166    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
167        static CREATE_LISTENERS: LazyLock<CreateListenerList<LocationDatabaseHost>> =
168            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
169
170        &CREATE_LISTENERS
171    }
172
173    async fn create(
174        state: &crate::State,
175        mut options: Self::CreateOptions<'_>,
176    ) -> Result<Self, crate::database::DatabaseError> {
177        options.validate()?;
178
179        super::database_host::DatabaseHost::by_uuid_optional_cached(
180            &state.database,
181            options.database_host_uuid,
182        )
183        .await?
184        .ok_or(crate::database::InvalidRelationError("database_host"))?;
185
186        let mut transaction = state.database.write().begin().await?;
187
188        let mut query_builder = InsertQueryBuilder::new("location_database_hosts");
189
190        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
191            .await?;
192
193        query_builder
194            .set("location_uuid", options.location_uuid)
195            .set("database_host_uuid", options.database_host_uuid);
196
197        query_builder.execute(&mut *transaction).await?;
198
199        transaction.commit().await?;
200
201        match Self::by_location_uuid_database_host_uuid(
202            &state.database,
203            options.location_uuid,
204            options.database_host_uuid,
205        )
206        .await?
207        {
208            Some(location_database_host) => Ok(location_database_host),
209            None => Err(sqlx::Error::RowNotFound.into()),
210        }
211    }
212}
213
214#[async_trait::async_trait]
215impl DeletableModel for LocationDatabaseHost {
216    type DeleteOptions = ();
217
218    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
219        static DELETE_LISTENERS: LazyLock<DeleteListenerList<LocationDatabaseHost>> =
220            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
221
222        &DELETE_LISTENERS
223    }
224
225    async fn delete(
226        &self,
227        state: &crate::State,
228        options: Self::DeleteOptions,
229    ) -> Result<(), anyhow::Error> {
230        let mut transaction = state.database.write().begin().await?;
231
232        self.run_delete_handlers(&options, state, &mut transaction)
233            .await?;
234
235        sqlx::query(
236            r#"
237            DELETE FROM location_database_hosts
238            WHERE location_database_hosts.location_uuid = $1 AND location_database_hosts.database_host_uuid = $2
239            "#,
240        )
241        .bind(self.location.uuid)
242        .bind(self.database_host.uuid)
243        .execute(&mut *transaction)
244        .await?;
245
246        transaction.commit().await?;
247
248        Ok(())
249    }
250}
251
252#[derive(ToSchema, Serialize)]
253#[schema(title = "LocationDatabaseHost")]
254pub struct AdminApiLocationDatabaseHost {
255    pub database_host: super::database_host::AdminApiDatabaseHost,
256
257    pub created: chrono::DateTime<chrono::Utc>,
258}