shared/models/
node_mount.rs

1use crate::{models::InsertQueryBuilder, prelude::*};
2use garde::Validate;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6    collections::BTreeMap,
7    sync::{Arc, LazyLock},
8};
9use utoipa::ToSchema;
10
11#[derive(Serialize, Deserialize)]
12pub struct NodeMount {
13    pub mount: Fetchable<super::mount::Mount>,
14    pub node: Fetchable<super::node::Node>,
15
16    pub created: chrono::NaiveDateTime,
17}
18
19impl BaseModel for NodeMount {
20    const NAME: &'static str = "node_mount";
21
22    #[inline]
23    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
24        let prefix = prefix.unwrap_or_default();
25
26        BTreeMap::from([
27            (
28                "node_mounts.mount_uuid",
29                compact_str::format_compact!("{prefix}mount_uuid"),
30            ),
31            (
32                "node_mounts.node_uuid",
33                compact_str::format_compact!("{prefix}node_uuid"),
34            ),
35            (
36                "node_mounts.created",
37                compact_str::format_compact!("{prefix}created"),
38            ),
39        ])
40    }
41
42    #[inline]
43    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
44        let prefix = prefix.unwrap_or_default();
45
46        Ok(Self {
47            mount: super::mount::Mount::get_fetchable(
48                row.try_get(compact_str::format_compact!("{prefix}mount_uuid").as_str())?,
49            ),
50            node: super::node::Node::get_fetchable(
51                row.try_get(compact_str::format_compact!("{prefix}node_uuid").as_str())?,
52            ),
53            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
54        })
55    }
56}
57
58impl NodeMount {
59    pub async fn by_node_uuid_mount_uuid(
60        database: &crate::database::Database,
61        node_uuid: uuid::Uuid,
62        mount_uuid: uuid::Uuid,
63    ) -> Result<Option<Self>, crate::database::DatabaseError> {
64        let row = sqlx::query(&format!(
65            r#"
66            SELECT {}
67            FROM node_mounts
68            WHERE node_mounts.node_uuid = $1 AND node_mounts.mount_uuid = $2
69            "#,
70            Self::columns_sql(None)
71        ))
72        .bind(node_uuid)
73        .bind(mount_uuid)
74        .fetch_optional(database.read())
75        .await?;
76
77        row.try_map(|row| Self::map(None, &row))
78    }
79
80    pub async fn by_node_uuid_with_pagination(
81        database: &crate::database::Database,
82        node_uuid: uuid::Uuid,
83        page: i64,
84        per_page: i64,
85        search: Option<&str>,
86    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
87        let offset = (page - 1) * per_page;
88
89        let rows = sqlx::query(&format!(
90            r#"
91            SELECT {}, COUNT(*) OVER() AS total_count
92            FROM node_mounts
93            JOIN mounts ON mounts.uuid = node_mounts.mount_uuid
94            WHERE node_mounts.node_uuid = $1 AND ($2 IS NULL OR mounts.name ILIKE '%' || $2 || '%')
95            ORDER BY node_mounts.created
96            LIMIT $3 OFFSET $4
97            "#,
98            Self::columns_sql(None)
99        ))
100        .bind(node_uuid)
101        .bind(search)
102        .bind(per_page)
103        .bind(offset)
104        .fetch_all(database.read())
105        .await?;
106
107        Ok(super::Pagination {
108            total: rows
109                .first()
110                .map_or(Ok(0), |row| row.try_get("total_count"))?,
111            per_page,
112            page,
113            data: rows
114                .into_iter()
115                .map(|row| Self::map(None, &row))
116                .try_collect_vec()?,
117        })
118    }
119
120    pub async fn by_mount_uuid_with_pagination(
121        database: &crate::database::Database,
122        mount_uuid: uuid::Uuid,
123        page: i64,
124        per_page: i64,
125        search: Option<&str>,
126    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
127        let offset = (page - 1) * per_page;
128
129        let rows = sqlx::query(&format!(
130            r#"
131            SELECT {}, COUNT(*) OVER() AS total_count
132            FROM node_mounts
133            JOIN nodes ON nodes.uuid = node_mounts.node_uuid
134            WHERE node_mounts.mount_uuid = $1 AND ($2 IS NULL OR nodes.name ILIKE '%' || $2 || '%')
135            ORDER BY node_mounts.created
136            LIMIT $3 OFFSET $4
137            "#,
138            Self::columns_sql(None)
139        ))
140        .bind(mount_uuid)
141        .bind(search)
142        .bind(per_page)
143        .bind(offset)
144        .fetch_all(database.read())
145        .await?;
146
147        Ok(super::Pagination {
148            total: rows
149                .first()
150                .map_or(Ok(0), |row| row.try_get("total_count"))?,
151            per_page,
152            page,
153            data: rows
154                .into_iter()
155                .map(|row| Self::map(None, &row))
156                .try_collect_vec()?,
157        })
158    }
159
160    #[inline]
161    pub async fn into_admin_node_api_object(
162        self,
163        database: &crate::database::Database,
164    ) -> Result<AdminApiNodeNodeMount, anyhow::Error> {
165        Ok(AdminApiNodeNodeMount {
166            node: self
167                .node
168                .fetch_cached(database)
169                .await?
170                .into_admin_api_object(database)
171                .await?,
172            created: self.created.and_utc(),
173        })
174    }
175
176    #[inline]
177    pub async fn into_admin_api_object(
178        self,
179        database: &crate::database::Database,
180    ) -> Result<AdminApiNodeMount, anyhow::Error> {
181        Ok(AdminApiNodeMount {
182            mount: self
183                .mount
184                .fetch_cached(database)
185                .await?
186                .into_admin_api_object(),
187            created: self.created.and_utc(),
188        })
189    }
190}
191
192#[derive(ToSchema, Deserialize, Validate)]
193pub struct CreateNodeMountOptions {
194    #[garde(skip)]
195    pub node_uuid: uuid::Uuid,
196    #[garde(skip)]
197    pub mount_uuid: uuid::Uuid,
198}
199
200#[async_trait::async_trait]
201impl CreatableModel for NodeMount {
202    type CreateOptions<'a> = CreateNodeMountOptions;
203    type CreateResult = Self;
204
205    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
206        static CREATE_LISTENERS: LazyLock<CreateListenerList<NodeMount>> =
207            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
208
209        &CREATE_LISTENERS
210    }
211
212    async fn create(
213        state: &crate::State,
214        mut options: Self::CreateOptions<'_>,
215    ) -> Result<Self, crate::database::DatabaseError> {
216        options.validate()?;
217
218        super::mount::Mount::by_uuid_optional_cached(&state.database, options.mount_uuid)
219            .await?
220            .ok_or(crate::database::InvalidRelationError("mount"))?;
221
222        let mut transaction = state.database.write().begin().await?;
223
224        let mut query_builder = InsertQueryBuilder::new("node_mounts");
225
226        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
227            .await?;
228
229        query_builder
230            .set("node_uuid", options.node_uuid)
231            .set("mount_uuid", options.mount_uuid);
232
233        query_builder.execute(&mut *transaction).await?;
234
235        transaction.commit().await?;
236
237        match Self::by_node_uuid_mount_uuid(&state.database, options.node_uuid, options.mount_uuid)
238            .await?
239        {
240            Some(node_mount) => Ok(node_mount),
241            None => Err(sqlx::Error::RowNotFound.into()),
242        }
243    }
244}
245
246#[async_trait::async_trait]
247impl DeletableModel for NodeMount {
248    type DeleteOptions = ();
249
250    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
251        static DELETE_LISTENERS: LazyLock<DeleteListenerList<NodeMount>> =
252            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
253
254        &DELETE_LISTENERS
255    }
256
257    async fn delete(
258        &self,
259        state: &crate::State,
260        options: Self::DeleteOptions,
261    ) -> Result<(), anyhow::Error> {
262        let mut transaction = state.database.write().begin().await?;
263
264        self.run_delete_handlers(&options, state, &mut transaction)
265            .await?;
266
267        sqlx::query(
268            r#"
269            DELETE FROM node_mounts
270            WHERE node_mounts.node_uuid = $1 AND node_mounts.mount_uuid = $2
271            "#,
272        )
273        .bind(self.node.uuid)
274        .bind(self.mount.uuid)
275        .execute(&mut *transaction)
276        .await?;
277
278        transaction.commit().await?;
279
280        Ok(())
281    }
282}
283
284#[derive(ToSchema, Serialize)]
285#[schema(title = "AdminNodeNodeMount")]
286pub struct AdminApiNodeNodeMount {
287    pub node: super::node::AdminApiNode,
288
289    pub created: chrono::DateTime<chrono::Utc>,
290}
291
292#[derive(ToSchema, Serialize)]
293#[schema(title = "AdminNodeMount")]
294pub struct AdminApiNodeMount {
295    pub mount: super::mount::AdminApiMount,
296
297    pub created: chrono::DateTime<chrono::Utc>,
298}