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