Skip to main content

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