shared/models/
location.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct Location {
16    pub uuid: uuid::Uuid,
17    pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
18
19    pub name: compact_str::CompactString,
20    pub description: Option<compact_str::CompactString>,
21
22    pub created: chrono::NaiveDateTime,
23}
24
25impl BaseModel for Location {
26    const NAME: &'static str = "location";
27
28    #[inline]
29    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
30        let prefix = prefix.unwrap_or_default();
31
32        BTreeMap::from([
33            (
34                "locations.uuid",
35                compact_str::format_compact!("{prefix}uuid"),
36            ),
37            (
38                "locations.backup_configuration_uuid",
39                compact_str::format_compact!("{prefix}location_backup_configuration_uuid"),
40            ),
41            (
42                "locations.name",
43                compact_str::format_compact!("{prefix}name"),
44            ),
45            (
46                "locations.description",
47                compact_str::format_compact!("{prefix}description"),
48            ),
49            (
50                "locations.created",
51                compact_str::format_compact!("{prefix}created"),
52            ),
53        ])
54    }
55
56    #[inline]
57    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
58        let prefix = prefix.unwrap_or_default();
59
60        Ok(Self {
61            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
62            backup_configuration:
63                super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
64                    row,
65                    compact_str::format_compact!("{prefix}location_backup_configuration_uuid"),
66                ),
67            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
68            description: row
69                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
70            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
71        })
72    }
73}
74
75impl Location {
76    pub async fn by_backup_configuration_uuid_with_pagination(
77        database: &crate::database::Database,
78        backup_configuration_uuid: uuid::Uuid,
79        page: i64,
80        per_page: i64,
81        search: Option<&str>,
82    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
83        let offset = (page - 1) * per_page;
84
85        let rows = sqlx::query(&format!(
86            r#"
87            SELECT {}, COUNT(*) OVER() AS total_count
88            FROM locations
89            WHERE locations.backup_configuration_uuid = $1 AND ($2 IS NULL OR locations.name ILIKE '%' || $2 || '%')
90            ORDER BY locations.created
91            LIMIT $3 OFFSET $4
92            "#,
93            Self::columns_sql(None)
94        ))
95        .bind(backup_configuration_uuid)
96        .bind(search)
97        .bind(per_page)
98        .bind(offset)
99        .fetch_all(database.read())
100        .await?;
101
102        Ok(super::Pagination {
103            total: rows
104                .first()
105                .map_or(Ok(0), |row| row.try_get("total_count"))?,
106            per_page,
107            page,
108            data: rows
109                .into_iter()
110                .map(|row| Self::map(None, &row))
111                .try_collect_vec()?,
112        })
113    }
114
115    pub async fn all_with_pagination(
116        database: &crate::database::Database,
117        page: i64,
118        per_page: i64,
119        search: Option<&str>,
120    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
121        let offset = (page - 1) * per_page;
122
123        let rows = sqlx::query(&format!(
124            r#"
125            SELECT {}, COUNT(*) OVER() AS total_count
126            FROM locations
127            WHERE $1 IS NULL OR locations.name ILIKE '%' || $1 || '%'
128            ORDER BY locations.created
129            LIMIT $2 OFFSET $3
130            "#,
131            Self::columns_sql(None)
132        ))
133        .bind(search)
134        .bind(per_page)
135        .bind(offset)
136        .fetch_all(database.read())
137        .await?;
138
139        Ok(super::Pagination {
140            total: rows
141                .first()
142                .map_or(Ok(0), |row| row.try_get("total_count"))?,
143            per_page,
144            page,
145            data: rows
146                .into_iter()
147                .map(|row| Self::map(None, &row))
148                .try_collect_vec()?,
149        })
150    }
151
152    #[inline]
153    pub async fn into_admin_api_object(
154        self,
155        database: &crate::database::Database,
156    ) -> AdminApiLocation {
157        AdminApiLocation {
158            uuid: self.uuid,
159            backup_configuration: if let Some(backup_configuration) = self.backup_configuration {
160                if let Ok(backup_configuration) = backup_configuration.fetch_cached(database).await
161                {
162                    backup_configuration
163                        .into_admin_api_object(database)
164                        .await
165                        .ok()
166                } else {
167                    None
168                }
169            } else {
170                None
171            },
172            name: self.name,
173            description: self.description,
174            created: self.created.and_utc(),
175        }
176    }
177}
178
179#[async_trait::async_trait]
180impl ByUuid for Location {
181    async fn by_uuid(
182        database: &crate::database::Database,
183        uuid: uuid::Uuid,
184    ) -> Result<Self, crate::database::DatabaseError> {
185        let row = sqlx::query(&format!(
186            r#"
187            SELECT {}
188            FROM locations
189            WHERE locations.uuid = $1
190            "#,
191            Self::columns_sql(None)
192        ))
193        .bind(uuid)
194        .fetch_one(database.read())
195        .await?;
196
197        Self::map(None, &row)
198    }
199}
200
201#[derive(ToSchema, Deserialize, Validate)]
202pub struct CreateLocationOptions {
203    #[garde(skip)]
204    pub backup_configuration_uuid: Option<uuid::Uuid>,
205    #[garde(length(chars, min = 3, max = 255))]
206    #[schema(min_length = 3, max_length = 255)]
207    pub name: compact_str::CompactString,
208    #[garde(length(chars, min = 1, max = 1024))]
209    #[schema(min_length = 1, max_length = 1024)]
210    pub description: Option<compact_str::CompactString>,
211}
212
213#[async_trait::async_trait]
214impl CreatableModel for Location {
215    type CreateOptions<'a> = CreateLocationOptions;
216    type CreateResult = Self;
217
218    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
219        static CREATE_LISTENERS: LazyLock<CreateListenerList<Location>> =
220            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
221
222        &CREATE_LISTENERS
223    }
224
225    async fn create(
226        state: &crate::State,
227        mut options: Self::CreateOptions<'_>,
228    ) -> Result<Self, crate::database::DatabaseError> {
229        options.validate()?;
230
231        if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
232            super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
233                &state.database,
234                *backup_configuration_uuid,
235            )
236            .await?
237            .ok_or(crate::database::InvalidRelationError(
238                "backup_configuration",
239            ))?;
240        }
241
242        let mut transaction = state.database.write().begin().await?;
243
244        let mut query_builder = InsertQueryBuilder::new("locations");
245
246        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
247            .await?;
248
249        query_builder
250            .set(
251                "backup_configuration_uuid",
252                options.backup_configuration_uuid,
253            )
254            .set("name", &options.name)
255            .set("description", &options.description);
256
257        let row = query_builder
258            .returning(&Self::columns_sql(None))
259            .fetch_one(&mut *transaction)
260            .await?;
261        let location = Self::map(None, &row)?;
262
263        transaction.commit().await?;
264
265        Ok(location)
266    }
267}
268
269#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
270pub struct UpdateLocationOptions {
271    #[garde(skip)]
272    #[serde(
273        default,
274        skip_serializing_if = "Option::is_none",
275        with = "::serde_with::rust::double_option"
276    )]
277    pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
278    #[garde(length(chars, min = 3, max = 255))]
279    #[schema(min_length = 3, max_length = 255)]
280    pub name: Option<compact_str::CompactString>,
281    #[garde(length(chars, min = 1, max = 1024))]
282    #[schema(min_length = 1, max_length = 1024)]
283    #[serde(
284        default,
285        skip_serializing_if = "Option::is_none",
286        with = "::serde_with::rust::double_option"
287    )]
288    pub description: Option<Option<compact_str::CompactString>>,
289}
290
291#[async_trait::async_trait]
292impl UpdatableModel for Location {
293    type UpdateOptions = UpdateLocationOptions;
294
295    fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
296        static UPDATE_LISTENERS: LazyLock<UpdateListenerList<Location>> =
297            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
298
299        &UPDATE_LISTENERS
300    }
301
302    async fn update(
303        &mut self,
304        state: &crate::State,
305        mut options: Self::UpdateOptions,
306    ) -> Result<(), crate::database::DatabaseError> {
307        options.validate()?;
308
309        let backup_configuration =
310            if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
311                match backup_configuration_uuid {
312                    Some(uuid) => {
313                        super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
314                            &state.database,
315                            *uuid,
316                        )
317                        .await?
318                        .ok_or(crate::database::InvalidRelationError(
319                            "backup_configuration",
320                        ))?;
321
322                        Some(Some(
323                            super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
324                        ))
325                    }
326                    None => Some(None),
327                }
328            } else {
329                None
330            };
331
332        let mut transaction = state.database.write().begin().await?;
333
334        let mut query_builder = UpdateQueryBuilder::new("locations");
335
336        Self::run_update_handlers(
337            self,
338            &mut options,
339            &mut query_builder,
340            state,
341            &mut transaction,
342        )
343        .await?;
344
345        query_builder
346            .set(
347                "backup_configuration_uuid",
348                options
349                    .backup_configuration_uuid
350                    .as_ref()
351                    .map(|u| u.as_ref()),
352            )
353            .set("name", options.name.as_ref())
354            .set(
355                "description",
356                options.description.as_ref().map(|d| d.as_ref()),
357            )
358            .where_eq("uuid", self.uuid);
359
360        query_builder.execute(&mut *transaction).await?;
361
362        if let Some(backup_configuration) = backup_configuration {
363            self.backup_configuration = backup_configuration;
364        }
365        if let Some(name) = options.name {
366            self.name = name;
367        }
368        if let Some(description) = options.description {
369            self.description = description;
370        }
371
372        transaction.commit().await?;
373
374        Ok(())
375    }
376}
377
378#[async_trait::async_trait]
379impl DeletableModel for Location {
380    type DeleteOptions = ();
381
382    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
383        static DELETE_LISTENERS: LazyLock<DeleteListenerList<Location>> =
384            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
385
386        &DELETE_LISTENERS
387    }
388
389    async fn delete(
390        &self,
391        state: &crate::State,
392        options: Self::DeleteOptions,
393    ) -> Result<(), anyhow::Error> {
394        let mut transaction = state.database.write().begin().await?;
395
396        self.run_delete_handlers(&options, state, &mut transaction)
397            .await?;
398
399        sqlx::query(
400            r#"
401            DELETE FROM locations
402            WHERE locations.uuid = $1
403            "#,
404        )
405        .bind(self.uuid)
406        .execute(&mut *transaction)
407        .await?;
408
409        transaction.commit().await?;
410
411        Ok(())
412    }
413}
414
415#[derive(ToSchema, Serialize)]
416#[schema(title = "Location")]
417pub struct AdminApiLocation {
418    pub uuid: uuid::Uuid,
419    pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
420
421    pub name: compact_str::CompactString,
422    pub description: Option<compact_str::CompactString>,
423
424    pub created: chrono::DateTime<chrono::Utc>,
425}