Skip to main content

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