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}