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}