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