Skip to main content

shared/models/
node_device.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 NodeDevice {
13    pub device: Fetchable<super::device::Device>,
14    pub node: Fetchable<super::node::Node>,
15
16    pub created: chrono::NaiveDateTime,
17
18    extension_data: super::ModelExtensionData,
19}
20
21impl BaseModel for NodeDevice {
22    const NAME: &'static str = "node_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                "node_devices.device_uuid",
42                compact_str::format_compact!("{prefix}device_uuid"),
43            ),
44            (
45                "node_devices.node_uuid",
46                compact_str::format_compact!("{prefix}node_uuid"),
47            ),
48            (
49                "node_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            ),
63            node: super::node::Node::get_fetchable(
64                row.try_get(compact_str::format_compact!("{prefix}node_uuid").as_str())?,
65            ),
66            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
67            extension_data: Self::map_extensions(prefix, row)?,
68        })
69    }
70}
71
72impl NodeDevice {
73    pub async fn by_node_uuid_device_uuid(
74        database: &crate::database::Database,
75        node_uuid: uuid::Uuid,
76        device_uuid: uuid::Uuid,
77    ) -> Result<Option<Self>, crate::database::DatabaseError> {
78        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
79            r#"
80            SELECT {}
81            FROM node_devices
82            WHERE node_devices.node_uuid = $1 AND node_devices.device_uuid = $2
83            "#,
84            Self::columns_sql(None)
85        )))
86        .bind(node_uuid)
87        .bind(device_uuid)
88        .fetch_optional(database.read())
89        .await?;
90
91        row.try_map(|row| Self::map(None, &row))
92    }
93
94    pub async fn by_node_uuid_with_pagination(
95        database: &crate::database::Database,
96        node_uuid: uuid::Uuid,
97        page: i64,
98        per_page: i64,
99        search: Option<&str>,
100    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
101        super::PaginatedQueryBuilder::new(
102            Self::columns_sql(None),
103            format!(
104                r#"
105                FROM node_devices
106                JOIN devices ON devices.uuid = node_devices.device_uuid
107                WHERE node_devices.node_uuid = $1 AND {search}
108                "#,
109                search = super::search_sql(2, &["devices.name"], &["devices.uuid"]),
110            ),
111            "node_devices.created, node_devices.node_uuid, node_devices.device_uuid",
112        )
113        .bind(node_uuid)
114        .bind(search)
115        .fetch(database, page, per_page)
116        .await
117    }
118
119    pub async fn by_device_uuid_with_pagination(
120        database: &crate::database::Database,
121        device_uuid: uuid::Uuid,
122        page: i64,
123        per_page: i64,
124        search: Option<&str>,
125    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
126        super::PaginatedQueryBuilder::new(
127            Self::columns_sql(None),
128            format!(
129                r#"
130                FROM node_devices
131                JOIN nodes ON nodes.uuid = node_devices.node_uuid
132                WHERE node_devices.device_uuid = $1 AND {search}
133                "#,
134                search = super::search_sql(2, &["nodes.name"], &["nodes.uuid"]),
135            ),
136            "node_devices.created, node_devices.node_uuid, node_devices.device_uuid",
137        )
138        .bind(device_uuid)
139        .bind(search)
140        .fetch(database, page, per_page)
141        .await
142    }
143
144    #[inline]
145    pub async fn into_admin_node_api_object(
146        self,
147        state: &crate::State,
148        _args: (),
149    ) -> Result<AdminApiNodeNodeDevice, crate::database::DatabaseError> {
150        Ok(AdminApiNodeNodeDevice {
151            node: self
152                .node
153                .fetch_cached(&state.database)
154                .await?
155                .into_admin_api_object(state, ())
156                .await?,
157            created: self.created.and_utc(),
158        })
159    }
160}
161
162#[async_trait::async_trait]
163impl IntoAdminApiObject for NodeDevice {
164    type AdminApiObject = AdminApiNodeDevice;
165    type ExtraArgs<'a> = ();
166
167    async fn into_admin_api_object<'a>(
168        self,
169        state: &crate::State,
170        _args: Self::ExtraArgs<'a>,
171    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
172        let api_object = AdminApiNodeDevice::init_hooks(&self, state).await?;
173
174        let api_object = finish_extendible!(
175            AdminApiNodeDevice {
176                device: self
177                    .device
178                    .fetch_cached(&state.database)
179                    .await?
180                    .into_admin_api_object(state, ())
181                    .await?,
182                created: self.created.and_utc(),
183            },
184            api_object,
185            state
186        )?;
187
188        Ok(api_object)
189    }
190}
191
192#[derive(ToSchema, Deserialize, Validate)]
193pub struct CreateNodeDeviceOptions {
194    #[garde(skip)]
195    pub node_uuid: uuid::Uuid,
196    #[garde(skip)]
197    pub device_uuid: uuid::Uuid,
198}
199
200#[async_trait::async_trait]
201impl CreatableModel for NodeDevice {
202    type CreateOptions<'a> = CreateNodeDeviceOptions;
203    type CreateResult = Self;
204
205    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
206        static CREATE_LISTENERS: LazyLock<CreateListenerList<NodeDevice>> =
207            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
208
209        &CREATE_LISTENERS
210    }
211
212    async fn create_with_transaction(
213        state: &crate::State,
214        mut options: Self::CreateOptions<'_>,
215        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
216    ) -> Result<Self, crate::database::DatabaseError> {
217        options.validate()?;
218
219        super::device::Device::by_uuid_optional_cached(&state.database, options.device_uuid)
220            .await?
221            .ok_or(crate::database::InvalidRelationError("device"))?;
222
223        let mut query_builder = InsertQueryBuilder::new("node_devices");
224
225        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
226
227        query_builder
228            .set("node_uuid", options.node_uuid)
229            .set("device_uuid", options.device_uuid);
230
231        let row = query_builder
232            .returning(&Self::columns_sql(None))
233            .fetch_one(&mut **transaction)
234            .await?;
235        let mut node_device = Self::map(None, &row)?;
236
237        Self::run_after_create_handlers(&mut node_device, &options, state, transaction).await?;
238
239        Ok(node_device)
240    }
241}
242
243#[async_trait::async_trait]
244impl DeletableModel for NodeDevice {
245    type DeleteOptions = ();
246
247    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
248        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<NodeDevice>> =
249            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
250
251        &DELETE_LISTENERS
252    }
253
254    async fn delete_with_transaction(
255        &self,
256        state: &crate::State,
257        options: Self::DeleteOptions,
258        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
259    ) -> Result<(), anyhow::Error> {
260        self.run_delete_handlers(&options, state, transaction)
261            .await?;
262
263        sqlx::query(
264            r#"
265            DELETE FROM node_devices
266            WHERE node_devices.node_uuid = $1 AND node_devices.device_uuid = $2
267            "#,
268        )
269        .bind(self.node.uuid)
270        .bind(self.device.uuid)
271        .execute(&mut **transaction)
272        .await?;
273
274        self.run_after_delete_handlers(&options, state, transaction)
275            .await?;
276
277        Ok(())
278    }
279}
280
281#[derive(ToSchema, Serialize)]
282#[schema(title = "AdminNodeNodeDevice")]
283pub struct AdminApiNodeNodeDevice {
284    pub node: super::node::AdminApiNode,
285
286    pub created: chrono::DateTime<chrono::Utc>,
287}
288
289#[schema_extension_derive::extendible]
290#[init_args(NodeDevice, crate::State)]
291#[hook_args(crate::State)]
292#[derive(ToSchema, Serialize)]
293#[schema(title = "AdminNodeDevice")]
294pub struct AdminApiNodeDevice {
295    pub device: super::device::AdminApiDevice,
296
297    pub created: chrono::DateTime<chrono::Utc>,
298}