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        super::PaginatedQueryBuilder::new(
114            Self::columns_sql(None),
115            format!(
116                r#"
117                FROM user_command_snippets
118                WHERE user_command_snippets.user_uuid = $1 AND {search}
119                "#,
120                search = super::search_sql(
121                    2,
122                    &["user_command_snippets.name"],
123                    &["user_command_snippets.uuid"]
124                ),
125            ),
126            "user_command_snippets.created, user_command_snippets.uuid",
127        )
128        .bind(user_uuid)
129        .bind(search)
130        .fetch(database, page, per_page)
131        .await
132    }
133
134    pub async fn all_by_user_uuid_nest_egg_uuid(
135        database: &crate::database::Database,
136        user_uuid: uuid::Uuid,
137        nest_egg_uuid: uuid::Uuid,
138    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
139        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
140            r#"
141            SELECT {}
142            FROM user_command_snippets
143            WHERE user_command_snippets.user_uuid = $1 AND ($2 = ANY(user_command_snippets.eggs) OR user_command_snippets.eggs = '{{}}'::uuid[])
144            ORDER BY user_command_snippets.created, user_command_snippets.uuid
145            "#,
146            Self::columns_sql(None)
147        )))
148        .bind(user_uuid)
149        .bind(nest_egg_uuid)
150        .fetch_all(database.read())
151        .await?;
152
153        rows.into_iter()
154            .map(|row| Self::map(None, &row))
155            .try_collect_vec()
156    }
157
158    pub async fn count_by_user_uuid(
159        database: &crate::database::Database,
160        user_uuid: uuid::Uuid,
161    ) -> Result<i64, sqlx::Error> {
162        sqlx::query_scalar(
163            r#"
164            SELECT COUNT(*)
165            FROM user_command_snippets
166            WHERE user_command_snippets.user_uuid = $1
167            "#,
168        )
169        .bind(user_uuid)
170        .fetch_one(database.read())
171        .await
172    }
173
174    pub async fn cleanup_uuid_arrays(
175        database: &crate::database::Database,
176    ) -> Result<u64, crate::database::DatabaseError> {
177        let result = sqlx::query(
178            "UPDATE user_command_snippets
179            SET eggs = COALESCE(
180                (SELECT array_agg(u) FROM unnest(eggs) AS u
181                WHERE EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)),
182                '{}'::uuid[]
183            )
184            WHERE EXISTS (
185                SELECT 1 FROM unnest(eggs) AS u
186                WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)
187            )",
188        )
189        .execute(database.write())
190        .await?;
191
192        Ok(result.rows_affected())
193    }
194}
195
196#[async_trait::async_trait]
197impl IntoApiObject for UserCommandSnippet {
198    type ApiObject = ApiUserCommandSnippet;
199    type ExtraArgs<'a> = ();
200
201    async fn into_api_object<'a>(
202        self,
203        state: &crate::State,
204        _args: Self::ExtraArgs<'a>,
205    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
206        let api_object = ApiUserCommandSnippet::init_hooks(&self, state).await?;
207
208        let api_object = finish_extendible!(
209            ApiUserCommandSnippet {
210                uuid: self.uuid,
211                name: self.name,
212                eggs: self.eggs,
213                command: self.command,
214                created: self.created.and_utc(),
215            },
216            api_object,
217            state
218        )?;
219
220        Ok(api_object)
221    }
222}
223
224#[derive(ToSchema, Deserialize, Validate)]
225pub struct CreateUserCommandSnippetOptions {
226    #[garde(skip)]
227    pub user_uuid: uuid::Uuid,
228
229    #[garde(length(chars, min = 1, max = 31))]
230    #[schema(min_length = 1, max_length = 31)]
231    pub name: compact_str::CompactString,
232
233    #[garde(length(max = 100))]
234    #[schema(max_length = 100)]
235    pub eggs: Vec<uuid::Uuid>,
236    #[garde(length(chars, min = 1, max = 1024))]
237    #[schema(min_length = 1, max_length = 1024)]
238    pub command: compact_str::CompactString,
239}
240
241#[async_trait::async_trait]
242impl CreatableModel for UserCommandSnippet {
243    type CreateOptions<'a> = CreateUserCommandSnippetOptions;
244    type CreateResult = Self;
245
246    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
247        static CREATE_LISTENERS: LazyLock<CreateListenerList<UserCommandSnippet>> =
248            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
249
250        &CREATE_LISTENERS
251    }
252
253    async fn create_with_transaction(
254        state: &crate::State,
255        mut options: Self::CreateOptions<'_>,
256        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
257    ) -> Result<Self, crate::database::DatabaseError> {
258        options.validate()?;
259
260        let mut query_builder = InsertQueryBuilder::new("user_command_snippets");
261
262        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
263
264        query_builder
265            .set("user_uuid", options.user_uuid)
266            .set("name", &options.name)
267            .set("eggs", &options.eggs)
268            .set("command", &options.command);
269
270        let row = query_builder
271            .returning(&Self::columns_sql(None))
272            .fetch_one(&mut **transaction)
273            .await?;
274        let mut user_command_snippet = Self::map(None, &row)?;
275
276        Self::run_after_create_handlers(&mut user_command_snippet, &options, state, transaction)
277            .await?;
278
279        Ok(user_command_snippet)
280    }
281}
282
283#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
284pub struct UpdateUserCommandSnippetOptions {
285    #[garde(length(chars, min = 1, max = 31))]
286    #[schema(min_length = 1, max_length = 31, value_type = String)]
287    pub name: Option<compact_str::CompactString>,
288
289    #[garde(length(max = 100))]
290    #[schema(max_length = 100)]
291    pub eggs: Option<Vec<uuid::Uuid>>,
292    #[garde(length(chars, min = 1, max = 1024))]
293    #[schema(min_length = 1, max_length = 1024)]
294    pub command: Option<compact_str::CompactString>,
295}
296
297#[async_trait::async_trait]
298impl UpdatableModel for UserCommandSnippet {
299    type UpdateOptions = UpdateUserCommandSnippetOptions;
300
301    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
302        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<UserCommandSnippet>> =
303            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
304
305        &UPDATE_LISTENERS
306    }
307
308    async fn update_with_transaction(
309        &mut self,
310        state: &crate::State,
311        mut options: Self::UpdateOptions,
312        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
313    ) -> Result<(), crate::database::DatabaseError> {
314        options.validate()?;
315
316        let mut query_builder = UpdateQueryBuilder::new("user_command_snippets");
317
318        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
319            .await?;
320
321        query_builder
322            .set("name", options.name.as_ref())
323            .set("eggs", options.eggs.as_ref())
324            .set("command", options.command.as_ref())
325            .where_eq("uuid", self.uuid);
326
327        query_builder.execute(&mut **transaction).await?;
328
329        if let Some(name) = options.name {
330            self.name = name;
331        }
332        if let Some(eggs) = options.eggs {
333            self.eggs = eggs;
334        }
335        if let Some(command) = options.command {
336            self.command = command;
337        }
338
339        self.run_after_update_handlers(state, transaction).await?;
340
341        Ok(())
342    }
343}
344
345#[async_trait::async_trait]
346impl DeletableModel for UserCommandSnippet {
347    type DeleteOptions = ();
348
349    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
350        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<UserCommandSnippet>> =
351            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
352
353        &DELETE_LISTENERS
354    }
355
356    async fn delete_with_transaction(
357        &self,
358        state: &crate::State,
359        options: Self::DeleteOptions,
360        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
361    ) -> Result<(), anyhow::Error> {
362        self.run_delete_handlers(&options, state, transaction)
363            .await?;
364
365        sqlx::query(
366            r#"
367            DELETE FROM user_command_snippets
368            WHERE user_command_snippets.uuid = $1
369            "#,
370        )
371        .bind(self.uuid)
372        .execute(&mut **transaction)
373        .await?;
374
375        self.run_after_delete_handlers(&options, state, transaction)
376            .await?;
377
378        Ok(())
379    }
380}
381
382#[derive(Validate)]
383pub struct DuplicateUserCommandSnippetOptions {
384    #[garde(skip)]
385    pub user_uuid: uuid::Uuid,
386    #[garde(length(chars, min = 1, max = 31))]
387    pub name: compact_str::CompactString,
388}
389
390#[async_trait::async_trait]
391impl DuplicableModel for UserCommandSnippet {
392    type DuplicateOptions<'a> = DuplicateUserCommandSnippetOptions;
393    type DuplicateResult = Self;
394
395    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
396        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<UserCommandSnippet>> =
397            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
398
399        &DUPLICATE_LISTENERS
400    }
401
402    async fn duplicate_with_transaction(
403        &self,
404        state: &crate::State,
405        options: Self::DuplicateOptions<'_>,
406        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
407    ) -> Result<Self, crate::database::DatabaseError> {
408        options.validate()?;
409
410        self.run_duplicate_handlers(&options, state, transaction)
411            .await?;
412
413        let mut query_builder = InsertQueryBuilder::new("user_command_snippets");
414
415        query_builder
416            .set("user_uuid", options.user_uuid)
417            .set("name", &options.name)
418            .set("eggs", &self.eggs)
419            .set("command", &self.command);
420
421        let row = query_builder
422            .returning(&Self::columns_sql(None))
423            .fetch_one(&mut **transaction)
424            .await?;
425        let mut user_command_snippet = Self::map(None, &row)?;
426
427        self.run_after_duplicate_handlers(&mut user_command_snippet, &options, state, transaction)
428            .await?;
429
430        Ok(user_command_snippet)
431    }
432}
433
434#[schema_extension_derive::extendible]
435#[init_args(UserCommandSnippet, crate::State)]
436#[hook_args(crate::State)]
437#[derive(ToSchema, Serialize)]
438#[schema(title = "UserCommandSnippet")]
439pub struct ApiUserCommandSnippet {
440    pub uuid: uuid::Uuid,
441
442    pub name: compact_str::CompactString,
443
444    pub eggs: Vec<uuid::Uuid>,
445    pub command: compact_str::CompactString,
446
447    pub created: chrono::DateTime<chrono::Utc>,
448}