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