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 Device {
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 permissions: compact_str::CompactString,
25 pub user_attachable: bool,
26
27 pub created: chrono::NaiveDateTime,
28
29 extension_data: super::ModelExtensionData,
30}
31
32impl BaseModel for Device {
33 const NAME: &'static str = "device";
34
35 fn get_extension_list() -> &'static super::ModelExtensionList {
36 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
37 LazyLock::new(|| parking_lot::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 ("devices.uuid", compact_str::format_compact!("{prefix}uuid")),
52 ("devices.name", compact_str::format_compact!("{prefix}name")),
53 (
54 "devices.description",
55 compact_str::format_compact!("{prefix}description"),
56 ),
57 (
58 "devices.source",
59 compact_str::format_compact!("{prefix}source"),
60 ),
61 (
62 "devices.target",
63 compact_str::format_compact!("{prefix}target"),
64 ),
65 (
66 "devices.permissions",
67 compact_str::format_compact!("{prefix}permissions"),
68 ),
69 (
70 "devices.user_attachable",
71 compact_str::format_compact!("{prefix}user_attachable"),
72 ),
73 (
74 "devices.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 permissions: row
92 .try_get(compact_str::format_compact!("{prefix}permissions").as_str())?,
93 user_attachable: row
94 .try_get(compact_str::format_compact!("{prefix}user_attachable").as_str())?,
95 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
96 extension_data: Self::map_extensions(prefix, row)?,
97 })
98 }
99
100 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
101 vec![compact_str::format_compact!(
102 "{}::{}",
103 Self::NAME,
104 self.uuid
105 )]
106 }
107}
108
109impl Device {
110 pub async fn by_node_uuid_egg_uuid_uuid(
111 database: &crate::database::Database,
112 node_uuid: uuid::Uuid,
113 egg_uuid: uuid::Uuid,
114 uuid: uuid::Uuid,
115 ) -> Result<Option<Self>, crate::database::DatabaseError> {
116 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
117 r#"
118 SELECT {}
119 FROM devices
120 JOIN node_devices ON devices.uuid = node_devices.device_uuid
121 JOIN nest_egg_devices ON devices.uuid = nest_egg_devices.device_uuid
122 WHERE node_devices.node_uuid = $1 AND nest_egg_devices.egg_uuid = $2 AND devices.uuid = $3
123 "#,
124 Self::columns_sql(None)
125 )))
126 .bind(node_uuid)
127 .bind(egg_uuid)
128 .bind(uuid)
129 .fetch_optional(database.read())
130 .await?;
131
132 row.try_map(|row| Self::map(None, &row))
133 }
134
135 pub async fn all_with_pagination(
136 database: &crate::database::Database,
137 page: i64,
138 per_page: i64,
139 search: Option<&str>,
140 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
141 super::PaginatedQueryBuilder::new(
142 Self::columns_sql(None),
143 format!(
144 r#"
145 FROM devices
146 WHERE {search}
147 "#,
148 search = super::search_sql(1, &["devices.name"], &["devices.uuid"]),
149 ),
150 "devices.created, devices.uuid",
151 )
152 .bind(search)
153 .fetch(database, page, per_page)
154 .await
155 }
156}
157
158#[async_trait::async_trait]
159impl IntoAdminApiObject for Device {
160 type AdminApiObject = AdminApiDevice;
161 type ExtraArgs<'a> = ();
162
163 async fn into_admin_api_object<'a>(
164 self,
165 state: &crate::State,
166 _args: Self::ExtraArgs<'a>,
167 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
168 let api_object = AdminApiDevice::init_hooks(&self, state).await?;
169
170 let api_object = finish_extendible!(
171 AdminApiDevice {
172 uuid: self.uuid,
173 name: self.name,
174 description: self.description,
175 source: self.source,
176 target: self.target,
177 permissions: self.permissions,
178 user_attachable: self.user_attachable,
179 created: self.created.and_utc(),
180 },
181 api_object,
182 state
183 )?;
184
185 Ok(api_object)
186 }
187}
188
189#[async_trait::async_trait]
190impl ByUuid for Device {
191 async fn by_uuid(
192 database: &crate::database::Database,
193 uuid: uuid::Uuid,
194 ) -> Result<Self, crate::database::DatabaseError> {
195 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
196 r#"
197 SELECT {}
198 FROM devices
199 WHERE devices.uuid = $1
200 "#,
201 Self::columns_sql(None)
202 )))
203 .bind(uuid)
204 .fetch_one(database.read())
205 .await?;
206
207 Self::map(None, &row)
208 }
209
210 async fn by_uuid_with_transaction(
211 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
212 uuid: uuid::Uuid,
213 ) -> Result<Self, crate::database::DatabaseError> {
214 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
215 r#"
216 SELECT {}
217 FROM devices
218 WHERE devices.uuid = $1
219 "#,
220 Self::columns_sql(None)
221 )))
222 .bind(uuid)
223 .fetch_one(&mut **transaction)
224 .await?;
225
226 Self::map(None, &row)
227 }
228}
229
230#[derive(ToSchema, Deserialize, Validate)]
231pub struct CreateDeviceOptions {
232 #[garde(length(chars, min = 1, max = 255))]
233 #[schema(min_length = 1, max_length = 255)]
234 pub name: compact_str::CompactString,
235 #[garde(length(chars, min = 1, max = 1024))]
236 #[schema(min_length = 1, max_length = 1024)]
237 pub description: Option<compact_str::CompactString>,
238 #[garde(length(chars, min = 1, max = 255))]
239 #[schema(min_length = 1, max_length = 255)]
240 pub source: compact_str::CompactString,
241 #[garde(length(chars, min = 1, max = 255))]
242 #[schema(min_length = 1, max_length = 255)]
243 pub target: compact_str::CompactString,
244 #[garde(length(chars, min = 1, max = 255), pattern("^[rwm]+$"))]
245 #[schema(min_length = 1, max_length = 255, pattern = "^[rwm]+$")]
246 pub permissions: compact_str::CompactString,
247 #[garde(skip)]
248 pub user_attachable: bool,
249}
250
251#[async_trait::async_trait]
252impl CreatableModel for Device {
253 type CreateOptions<'a> = CreateDeviceOptions;
254 type CreateResult = Self;
255
256 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
257 static CREATE_LISTENERS: LazyLock<CreateListenerList<Device>> =
258 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
259
260 &CREATE_LISTENERS
261 }
262
263 async fn create_with_transaction(
264 state: &crate::State,
265 mut options: Self::CreateOptions<'_>,
266 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
267 ) -> Result<Self, crate::database::DatabaseError> {
268 options.validate()?;
269
270 let mut query_builder = InsertQueryBuilder::new("devices");
271
272 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
273
274 query_builder
275 .set("name", &options.name)
276 .set("description", &options.description)
277 .set("source", &options.source)
278 .set("target", &options.target)
279 .set("permissions", &options.permissions)
280 .set("user_attachable", options.user_attachable);
281
282 let row = query_builder
283 .returning(&Self::columns_sql(None))
284 .fetch_one(&mut **transaction)
285 .await?;
286 let mut device = Self::map(None, &row)?;
287
288 Self::run_after_create_handlers(&mut device, &options, state, transaction).await?;
289
290 Ok(device)
291 }
292}
293
294#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
295pub struct UpdateDeviceOptions {
296 #[garde(length(chars, min = 1, max = 255))]
297 #[schema(min_length = 1, max_length = 255)]
298 pub name: Option<compact_str::CompactString>,
299 #[garde(length(chars, min = 1, max = 1024))]
300 #[schema(min_length = 1, max_length = 1024)]
301 #[serde(
302 default,
303 skip_serializing_if = "Option::is_none",
304 with = "::serde_with::rust::double_option"
305 )]
306 pub description: Option<Option<compact_str::CompactString>>,
307 #[garde(length(chars, min = 1, max = 255))]
308 #[schema(min_length = 1, max_length = 255)]
309 pub source: Option<compact_str::CompactString>,
310 #[garde(length(chars, min = 1, max = 255))]
311 #[schema(min_length = 1, max_length = 255)]
312 pub target: Option<compact_str::CompactString>,
313 #[garde(length(chars, min = 1, max = 255), pattern("^[rwm]+$"))]
314 #[schema(min_length = 1, max_length = 255, pattern = "^[rwm]+$")]
315 pub permissions: Option<compact_str::CompactString>,
316 #[garde(skip)]
317 pub user_attachable: Option<bool>,
318}
319
320#[async_trait::async_trait]
321impl UpdatableModel for Device {
322 type UpdateOptions = UpdateDeviceOptions;
323
324 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
325 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Device>> =
326 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
327
328 &UPDATE_LISTENERS
329 }
330
331 async fn update_with_transaction(
332 &mut self,
333 state: &crate::State,
334 mut options: Self::UpdateOptions,
335 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
336 ) -> Result<(), crate::database::DatabaseError> {
337 options.validate()?;
338
339 let mut query_builder = UpdateQueryBuilder::new("devices");
340
341 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
342 .await?;
343
344 query_builder
345 .set("name", options.name.as_ref())
346 .set(
347 "description",
348 options.description.as_ref().map(|d| d.as_ref()),
349 )
350 .set("source", options.source.as_ref())
351 .set("target", options.target.as_ref())
352 .set("permissions", options.permissions.as_ref())
353 .set("user_attachable", options.user_attachable)
354 .where_eq("uuid", self.uuid);
355
356 query_builder.execute(&mut **transaction).await?;
357
358 if let Some(name) = options.name {
359 self.name = name;
360 }
361 if let Some(description) = options.description {
362 self.description = description;
363 }
364 if let Some(source) = options.source {
365 self.source = source;
366 }
367 if let Some(target) = options.target {
368 self.target = target;
369 }
370 if let Some(permissions) = options.permissions {
371 self.permissions = permissions;
372 }
373 if let Some(user_attachable) = options.user_attachable {
374 self.user_attachable = user_attachable;
375 }
376
377 self.run_after_update_handlers(state, transaction).await?;
378
379 Ok(())
380 }
381}
382
383#[async_trait::async_trait]
384impl DeletableModel for Device {
385 type DeleteOptions = ();
386
387 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
388 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Device>> =
389 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
390
391 &DELETE_LISTENERS
392 }
393
394 async fn delete_with_transaction(
395 &self,
396 state: &crate::State,
397 options: Self::DeleteOptions,
398 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
399 ) -> Result<(), anyhow::Error> {
400 self.run_delete_handlers(&options, state, transaction)
401 .await?;
402
403 sqlx::query(
404 r#"
405 DELETE FROM devices
406 WHERE devices.uuid = $1
407 "#,
408 )
409 .bind(self.uuid)
410 .execute(&mut **transaction)
411 .await?;
412
413 self.run_after_delete_handlers(&options, state, transaction)
414 .await?;
415
416 Ok(())
417 }
418}
419
420#[derive(Validate)]
421pub struct DuplicateDeviceOptions {
422 #[garde(length(chars, min = 1, max = 255))]
423 pub name: compact_str::CompactString,
424 #[garde(length(chars, min = 1, max = 255))]
425 pub source: compact_str::CompactString,
426 #[garde(length(chars, min = 1, max = 255))]
427 pub target: compact_str::CompactString,
428}
429
430#[async_trait::async_trait]
431impl DuplicableModel for Device {
432 type DuplicateOptions<'a> = DuplicateDeviceOptions;
433 type DuplicateResult = Self;
434
435 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
436 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Device>> =
437 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
438
439 &DUPLICATE_LISTENERS
440 }
441
442 async fn duplicate_with_transaction(
443 &self,
444 state: &crate::State,
445 options: Self::DuplicateOptions<'_>,
446 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
447 ) -> Result<Self, crate::database::DatabaseError> {
448 options.validate()?;
449
450 self.run_duplicate_handlers(&options, state, transaction)
451 .await?;
452
453 let mut query_builder = InsertQueryBuilder::new("devices");
454
455 query_builder
456 .set("name", &options.name)
457 .set("description", &self.description)
458 .set("source", &options.source)
459 .set("target", &options.target)
460 .set("permissions", &self.permissions)
461 .set("user_attachable", self.user_attachable);
462
463 let row = query_builder
464 .returning(&Self::columns_sql(None))
465 .fetch_one(&mut **transaction)
466 .await?;
467 let mut device = Self::map(None, &row)?;
468
469 self.run_after_duplicate_handlers(&mut device, &options, state, transaction)
470 .await?;
471
472 Ok(device)
473 }
474}
475
476#[schema_extension_derive::extendible]
477#[init_args(Device, crate::State)]
478#[hook_args(crate::State)]
479#[derive(ToSchema, Serialize)]
480#[schema(title = "Device")]
481pub struct AdminApiDevice {
482 pub uuid: uuid::Uuid,
483
484 pub name: compact_str::CompactString,
485 pub description: Option<compact_str::CompactString>,
486
487 pub source: compact_str::CompactString,
488 pub target: compact_str::CompactString,
489
490 pub permissions: compact_str::CompactString,
491 pub user_attachable: bool,
492
493 pub created: chrono::DateTime<chrono::Utc>,
494}