Skip to main content

shared/models/
device.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 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}