shared/models/
nest.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 Nest {
16    pub uuid: uuid::Uuid,
17
18    pub name: compact_str::CompactString,
19    pub description: Option<compact_str::CompactString>,
20    pub author: compact_str::CompactString,
21
22    pub created: chrono::NaiveDateTime,
23}
24
25impl BaseModel for Nest {
26    const NAME: &'static str = "nest";
27
28    #[inline]
29    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
30        let prefix = prefix.unwrap_or_default();
31
32        BTreeMap::from([
33            ("nests.uuid", compact_str::format_compact!("{prefix}uuid")),
34            ("nests.name", compact_str::format_compact!("{prefix}name")),
35            (
36                "nests.description",
37                compact_str::format_compact!("{prefix}description"),
38            ),
39            (
40                "nests.author",
41                compact_str::format_compact!("{prefix}author"),
42            ),
43            (
44                "nests.created",
45                compact_str::format_compact!("{prefix}created"),
46            ),
47        ])
48    }
49
50    #[inline]
51    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
52        let prefix = prefix.unwrap_or_default();
53
54        Ok(Self {
55            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
56            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
57            description: row
58                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
59            author: row.try_get(compact_str::format_compact!("{prefix}author").as_str())?,
60            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
61        })
62    }
63}
64
65impl Nest {
66    pub async fn all_with_pagination(
67        database: &crate::database::Database,
68        page: i64,
69        per_page: i64,
70        search: Option<&str>,
71    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
72        let offset = (page - 1) * per_page;
73
74        let rows = sqlx::query(&format!(
75            r#"
76            SELECT {}, COUNT(*) OVER() AS total_count
77            FROM nests
78            WHERE ($1 IS NULL OR nests.name ILIKE '%' || $1 || '%')
79            ORDER BY nests.created
80            LIMIT $2 OFFSET $3
81            "#,
82            Self::columns_sql(None)
83        ))
84        .bind(search)
85        .bind(per_page)
86        .bind(offset)
87        .fetch_all(database.read())
88        .await?;
89
90        Ok(super::Pagination {
91            total: rows
92                .first()
93                .map_or(Ok(0), |row| row.try_get("total_count"))?,
94            per_page,
95            page,
96            data: rows
97                .into_iter()
98                .map(|row| Self::map(None, &row))
99                .try_collect_vec()?,
100        })
101    }
102
103    pub async fn by_name(
104        database: &crate::database::Database,
105        name: &str,
106    ) -> Result<Option<Self>, crate::database::DatabaseError> {
107        let row = sqlx::query(&format!(
108            r#"
109            SELECT {}
110            FROM nests
111            WHERE nests.name = $1
112            "#,
113            Self::columns_sql(None)
114        ))
115        .bind(name)
116        .fetch_optional(database.read())
117        .await?;
118
119        row.try_map(|row| Self::map(None, &row))
120    }
121
122    #[inline]
123    pub fn into_admin_api_object(self) -> AdminApiNest {
124        AdminApiNest {
125            uuid: self.uuid,
126            name: self.name,
127            description: self.description,
128            author: self.author,
129            created: self.created.and_utc(),
130        }
131    }
132}
133
134#[async_trait::async_trait]
135impl ByUuid for Nest {
136    async fn by_uuid(
137        database: &crate::database::Database,
138        uuid: uuid::Uuid,
139    ) -> Result<Self, crate::database::DatabaseError> {
140        let row = sqlx::query(&format!(
141            r#"
142            SELECT {}
143            FROM nests
144            WHERE nests.uuid = $1
145            "#,
146            Self::columns_sql(None)
147        ))
148        .bind(uuid)
149        .fetch_one(database.read())
150        .await?;
151
152        Self::map(None, &row)
153    }
154}
155
156#[derive(ToSchema, Deserialize, Validate)]
157pub struct CreateNestOptions {
158    #[garde(length(chars, min = 2, max = 255))]
159    #[schema(min_length = 2, max_length = 255)]
160    pub author: compact_str::CompactString,
161    #[garde(length(chars, min = 3, max = 255))]
162    #[schema(min_length = 3, max_length = 255)]
163    pub name: compact_str::CompactString,
164    #[garde(length(chars, min = 1, max = 1024))]
165    #[schema(min_length = 1, max_length = 1024)]
166    pub description: Option<compact_str::CompactString>,
167}
168
169#[async_trait::async_trait]
170impl CreatableModel for Nest {
171    type CreateOptions<'a> = CreateNestOptions;
172    type CreateResult = Self;
173
174    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
175        static CREATE_LISTENERS: LazyLock<CreateListenerList<Nest>> =
176            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
177
178        &CREATE_LISTENERS
179    }
180
181    async fn create(
182        state: &crate::State,
183        mut options: Self::CreateOptions<'_>,
184    ) -> Result<Self, crate::database::DatabaseError> {
185        options.validate()?;
186
187        let mut transaction = state.database.write().begin().await?;
188
189        let mut query_builder = InsertQueryBuilder::new("nests");
190
191        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
192            .await?;
193
194        query_builder
195            .set("author", &options.author)
196            .set("name", &options.name)
197            .set("description", &options.description);
198
199        let row = query_builder
200            .returning(&Self::columns_sql(None))
201            .fetch_one(&mut *transaction)
202            .await?;
203        let nest = Self::map(None, &row)?;
204
205        transaction.commit().await?;
206
207        Ok(nest)
208    }
209}
210
211#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
212pub struct UpdateNestOptions {
213    #[garde(length(chars, min = 2, max = 255))]
214    #[schema(min_length = 2, max_length = 255)]
215    pub author: Option<compact_str::CompactString>,
216    #[garde(length(chars, min = 3, max = 255))]
217    #[schema(min_length = 3, max_length = 255)]
218    pub name: Option<compact_str::CompactString>,
219    #[garde(length(chars, min = 1, max = 1024))]
220    #[schema(min_length = 1, max_length = 1024)]
221    #[serde(
222        default,
223        skip_serializing_if = "Option::is_none",
224        with = "::serde_with::rust::double_option"
225    )]
226    pub description: Option<Option<compact_str::CompactString>>,
227}
228
229#[async_trait::async_trait]
230impl UpdatableModel for Nest {
231    type UpdateOptions = UpdateNestOptions;
232
233    fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
234        static UPDATE_LISTENERS: LazyLock<UpdateListenerList<Nest>> =
235            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
236
237        &UPDATE_LISTENERS
238    }
239
240    async fn update(
241        &mut self,
242        state: &crate::State,
243        mut options: Self::UpdateOptions,
244    ) -> Result<(), crate::database::DatabaseError> {
245        options.validate()?;
246
247        let mut transaction = state.database.write().begin().await?;
248
249        let mut query_builder = UpdateQueryBuilder::new("nests");
250
251        Self::run_update_handlers(
252            self,
253            &mut options,
254            &mut query_builder,
255            state,
256            &mut transaction,
257        )
258        .await?;
259
260        query_builder
261            .set("author", options.author.as_ref())
262            .set("name", options.name.as_ref())
263            .set(
264                "description",
265                options.description.as_ref().map(|d| d.as_ref()),
266            )
267            .where_eq("uuid", self.uuid);
268
269        query_builder.execute(&mut *transaction).await?;
270
271        if let Some(author) = options.author {
272            self.author = author;
273        }
274        if let Some(name) = options.name {
275            self.name = name;
276        }
277        if let Some(description) = options.description {
278            self.description = description;
279        }
280
281        transaction.commit().await?;
282
283        Ok(())
284    }
285}
286
287#[async_trait::async_trait]
288impl DeletableModel for Nest {
289    type DeleteOptions = ();
290
291    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
292        static DELETE_LISTENERS: LazyLock<DeleteListenerList<Nest>> =
293            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
294
295        &DELETE_LISTENERS
296    }
297
298    async fn delete(
299        &self,
300        state: &crate::State,
301        options: Self::DeleteOptions,
302    ) -> Result<(), anyhow::Error> {
303        let mut transaction = state.database.write().begin().await?;
304
305        self.run_delete_handlers(&options, state, &mut transaction)
306            .await?;
307
308        sqlx::query(
309            r#"
310            DELETE FROM nests
311            WHERE nests.uuid = $1
312            "#,
313        )
314        .bind(self.uuid)
315        .execute(&mut *transaction)
316        .await?;
317
318        transaction.commit().await?;
319
320        Ok(())
321    }
322}
323
324#[derive(ToSchema, Serialize)]
325#[schema(title = "Nest")]
326pub struct AdminApiNest {
327    pub uuid: uuid::Uuid,
328
329    pub name: compact_str::CompactString,
330    pub description: Option<compact_str::CompactString>,
331    pub author: compact_str::CompactString,
332
333    pub created: chrono::DateTime<chrono::Utc>,
334}