1use crate::{models::InsertQueryBuilder, prelude::*, storage::StorageUrlRetriever};
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 ServerDevice {
13 pub device: Fetchable<super::device::Device>,
14 pub server: Option<Fetchable<super::server::Server>>,
15
16 pub created: Option<chrono::NaiveDateTime>,
17
18 extension_data: super::ModelExtensionData,
19}
20
21impl BaseModel for ServerDevice {
22 const NAME: &'static str = "server_device";
23
24 fn get_extension_list() -> &'static super::ModelExtensionList {
25 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
26 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
27
28 &EXTENSIONS
29 }
30
31 fn get_extension_data(&self) -> &super::ModelExtensionData {
32 &self.extension_data
33 }
34
35 #[inline]
36 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
37 let prefix = prefix.unwrap_or_default();
38
39 BTreeMap::from([
40 (
41 "server_devices.device_uuid",
42 compact_str::format_compact!("{prefix}device_uuid"),
43 ),
44 (
45 "server_devices.server_uuid",
46 compact_str::format_compact!("{prefix}server_uuid"),
47 ),
48 (
49 "server_devices.created",
50 compact_str::format_compact!("{prefix}created"),
51 ),
52 ])
53 }
54
55 #[inline]
56 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
57 let prefix = prefix.unwrap_or_default();
58
59 Ok(Self {
60 device: super::device::Device::get_fetchable(
61 row.try_get(compact_str::format_compact!("{prefix}device_uuid").as_str())
62 .or_else(|_| row.try_get("alt_device_uuid"))?,
63 ),
64 server: super::server::Server::get_fetchable_from_row(
65 row,
66 compact_str::format_compact!("{prefix}server_uuid"),
67 ),
68 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
69 extension_data: Self::map_extensions(prefix, row)?,
70 })
71 }
72}
73
74impl ServerDevice {
75 pub async fn by_server_uuid_device_uuid(
76 database: &crate::database::Database,
77 server_uuid: uuid::Uuid,
78 device_uuid: uuid::Uuid,
79 ) -> Result<Option<Self>, crate::database::DatabaseError> {
80 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
81 r#"
82 SELECT {}
83 FROM server_devices
84 WHERE server_devices.server_uuid = $1 AND server_devices.device_uuid = $2
85 "#,
86 Self::columns_sql(None)
87 )))
88 .bind(server_uuid)
89 .bind(device_uuid)
90 .fetch_optional(database.read())
91 .await?;
92
93 row.try_map(|row| Self::map(None, &row))
94 }
95
96 pub async fn by_server_uuid_with_pagination(
97 database: &crate::database::Database,
98 server_uuid: uuid::Uuid,
99 page: i64,
100 per_page: i64,
101 search: Option<&str>,
102 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
103 super::PaginatedQueryBuilder::new(
104 Self::columns_sql(None),
105 format!(
106 r#"
107 FROM server_devices
108 JOIN devices ON devices.uuid = server_devices.device_uuid
109 WHERE server_devices.server_uuid = $1 AND {search}
110 "#,
111 search = super::search_sql(2, &["devices.name"], &["devices.uuid"]),
112 ),
113 "server_devices.device_uuid ASC",
114 )
115 .bind(server_uuid)
116 .bind(search)
117 .fetch(database, page, per_page)
118 .await
119 }
120
121 pub async fn available_by_server_with_pagination(
122 database: &crate::database::Database,
123 server: &super::server::Server,
124 page: i64,
125 per_page: i64,
126 search: Option<&str>,
127 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
128 super::PaginatedQueryBuilder::new(
129 format!("{}, devices.uuid AS alt_device_uuid", Self::columns_sql(None)),
130 format!(
131 r#"
132 FROM devices
133 JOIN node_devices ON devices.uuid = node_devices.device_uuid AND node_devices.node_uuid = $1
134 JOIN nest_egg_devices ON devices.uuid = nest_egg_devices.device_uuid AND nest_egg_devices.egg_uuid = $2
135 LEFT JOIN server_devices ON server_devices.device_uuid = devices.uuid AND server_devices.server_uuid = $3
136 WHERE {search}
137 "#,
138 search = super::search_sql(4, &["devices.name"], &["devices.uuid"]),
139 ),
140 "devices.created, devices.uuid",
141 )
142 .bind(server.node.uuid)
143 .bind(server.egg.uuid)
144 .bind(server.uuid)
145 .bind(search)
146 .fetch(database, page, per_page)
147 .await
148 }
149
150 pub async fn attachable_by_server_with_pagination(
151 database: &crate::database::Database,
152 server: &super::server::Server,
153 page: i64,
154 per_page: i64,
155 search: Option<&str>,
156 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
157 super::PaginatedQueryBuilder::new(
158 format!("{}, devices.uuid AS alt_device_uuid", Self::columns_sql(None)),
159 format!(
160 r#"
161 FROM devices
162 JOIN node_devices ON devices.uuid = node_devices.device_uuid AND node_devices.node_uuid = $1
163 JOIN nest_egg_devices ON devices.uuid = nest_egg_devices.device_uuid AND nest_egg_devices.egg_uuid = $2
164 LEFT JOIN server_devices ON server_devices.device_uuid = devices.uuid AND server_devices.server_uuid = $3
165 WHERE devices.user_attachable = TRUE AND {search}
166 "#,
167 search = super::search_sql(4, &["devices.name"], &["devices.uuid"]),
168 ),
169 "devices.created, devices.uuid",
170 )
171 .bind(server.node.uuid)
172 .bind(server.egg.uuid)
173 .bind(server.uuid)
174 .bind(search)
175 .fetch(database, page, per_page)
176 .await
177 }
178
179 pub async fn by_device_uuid_with_pagination(
180 database: &crate::database::Database,
181 device_uuid: uuid::Uuid,
182 page: i64,
183 per_page: i64,
184 search: Option<&str>,
185 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
186 super::PaginatedQueryBuilder::new(
187 Self::columns_sql(None),
188 format!(
189 r#"
190 FROM server_devices
191 JOIN servers ON servers.uuid = server_devices.server_uuid
192 WHERE server_devices.device_uuid = $1 AND {search}
193 "#,
194 search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
195 ),
196 "server_devices.device_uuid ASC",
197 )
198 .bind(device_uuid)
199 .bind(search)
200 .fetch(database, page, per_page)
201 .await
202 }
203
204 pub async fn into_admin_server_api_object(
205 self,
206 state: &crate::State,
207 storage_url_retriever: &StorageUrlRetriever<'_>,
208 ) -> Result<AdminApiServerServerDevice, crate::database::DatabaseError> {
209 let created = match self.created {
210 Some(created) => created,
211 None => {
212 return Err(crate::database::DatabaseError::Any(anyhow::anyhow!(
213 "This device does not have a server attached"
214 )));
215 }
216 };
217 let server = match self.server {
218 Some(server) => server.fetch_cached(&state.database).await?,
219 None => {
220 return Err(crate::database::DatabaseError::Any(anyhow::anyhow!(
221 "This device does not have a server attached"
222 )));
223 }
224 };
225
226 Ok(AdminApiServerServerDevice {
227 server: server
228 .into_admin_api_object(state, storage_url_retriever)
229 .await?,
230 created: created.and_utc(),
231 })
232 }
233}
234
235#[async_trait::async_trait]
236impl IntoApiObject for ServerDevice {
237 type ApiObject = ApiServerDevice;
238 type ExtraArgs<'a> = ();
239
240 async fn into_api_object<'a>(
241 self,
242 state: &crate::State,
243 _args: Self::ExtraArgs<'a>,
244 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
245 let api_object = ApiServerDevice::init_hooks(&self, state).await?;
246 let device = self.device.fetch_cached(&state.database).await?;
247
248 let api_object = finish_extendible!(
249 ApiServerDevice {
250 uuid: device.uuid,
251 name: device.name,
252 description: device.description,
253 target: device.target,
254 permissions: device.permissions,
255 created: self.created.map(|dt| dt.and_utc()),
256 },
257 api_object,
258 state
259 )?;
260
261 Ok(api_object)
262 }
263}
264
265#[async_trait::async_trait]
266impl IntoAdminApiObject for ServerDevice {
267 type AdminApiObject = AdminApiServerDevice;
268 type ExtraArgs<'a> = ();
269
270 async fn into_admin_api_object<'a>(
271 self,
272 state: &crate::State,
273 _args: Self::ExtraArgs<'a>,
274 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
275 let api_object = AdminApiServerDevice::init_hooks(&self, state).await?;
276 let device = self.device.fetch_cached(&state.database).await?;
277
278 let api_object = finish_extendible!(
279 AdminApiServerDevice {
280 device: device.into_admin_api_object(state, ()).await?,
281 created: self.created.map(|dt| dt.and_utc()),
282 },
283 api_object,
284 state
285 )?;
286
287 Ok(api_object)
288 }
289}
290
291#[derive(Validate)]
292pub struct CreateServerDeviceOptions {
293 #[garde(skip)]
294 pub server_uuid: uuid::Uuid,
295 #[garde(skip)]
296 pub device_uuid: uuid::Uuid,
297}
298
299#[async_trait::async_trait]
300impl CreatableModel for ServerDevice {
301 type CreateOptions<'a> = CreateServerDeviceOptions;
302 type CreateResult = Self;
303
304 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
305 static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerDevice>> =
306 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
307
308 &CREATE_LISTENERS
309 }
310
311 async fn create_with_transaction(
312 state: &crate::State,
313 mut options: Self::CreateOptions<'_>,
314 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
315 ) -> Result<Self, crate::database::DatabaseError> {
316 options.validate()?;
317
318 super::device::Device::by_uuid_optional_cached(&state.database, options.device_uuid)
319 .await?
320 .ok_or(crate::database::InvalidRelationError("device"))?;
321
322 let mut query_builder = InsertQueryBuilder::new("server_devices");
323
324 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
325
326 query_builder
327 .set("server_uuid", options.server_uuid)
328 .set("device_uuid", options.device_uuid);
329
330 let row = query_builder
331 .returning(&Self::columns_sql(None))
332 .fetch_one(&mut **transaction)
333 .await?;
334 let mut server_device = Self::map(None, &row)?;
335
336 Self::run_after_create_handlers(&mut server_device, &options, state, transaction).await?;
337
338 Ok(server_device)
339 }
340}
341
342#[async_trait::async_trait]
343impl DeletableModel for ServerDevice {
344 type DeleteOptions = ();
345
346 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
347 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerDevice>> =
348 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
349
350 &DELETE_LISTENERS
351 }
352
353 async fn delete_with_transaction(
354 &self,
355 state: &crate::State,
356 options: Self::DeleteOptions,
357 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
358 ) -> Result<(), anyhow::Error> {
359 let server_uuid = match &self.server {
360 Some(server) => server.uuid,
361 None => {
362 return Err(anyhow::anyhow!(
363 "This server device does not have a server attached, cannot delete"
364 ));
365 }
366 };
367
368 self.run_delete_handlers(&options, state, transaction)
369 .await?;
370
371 sqlx::query(
372 r#"
373 DELETE FROM server_devices
374 WHERE server_devices.server_uuid = $1 AND server_devices.device_uuid = $2
375 "#,
376 )
377 .bind(server_uuid)
378 .bind(self.device.uuid)
379 .execute(&mut **transaction)
380 .await?;
381
382 self.run_after_delete_handlers(&options, state, transaction)
383 .await?;
384
385 Ok(())
386 }
387}
388
389#[schema_extension_derive::extendible]
390#[init_args(ServerDevice, crate::State)]
391#[hook_args(crate::State)]
392#[derive(ToSchema, Serialize)]
393#[schema(title = "ServerDevice")]
394pub struct ApiServerDevice {
395 pub uuid: uuid::Uuid,
396
397 pub name: compact_str::CompactString,
398 pub description: Option<compact_str::CompactString>,
399
400 pub target: compact_str::CompactString,
401 pub permissions: compact_str::CompactString,
402
403 pub created: Option<chrono::DateTime<chrono::Utc>>,
404}
405
406#[derive(ToSchema, Serialize)]
407#[schema(title = "AdminServerServerDevice")]
408pub struct AdminApiServerServerDevice {
409 pub server: super::server::AdminApiServer,
410
411 pub created: chrono::DateTime<chrono::Utc>,
412}
413
414#[schema_extension_derive::extendible]
415#[init_args(ServerDevice, crate::State)]
416#[hook_args(crate::State)]
417#[derive(ToSchema, Serialize)]
418#[schema(title = "AdminServerDevice")]
419pub struct AdminApiServerDevice {
420 pub device: super::device::AdminApiDevice,
421
422 pub created: Option<chrono::DateTime<chrono::Utc>>,
423}