Skip to main content

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