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