1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow, prelude::Type};
8use std::{
9 collections::BTreeMap,
10 hash::Hash,
11 sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
16#[serde(rename_all = "snake_case")]
17#[sqlx(type_name = "announcement_type", rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum AnnouncementType {
19 Info,
20 Success,
21 Warning,
22 Error,
23}
24
25pub fn validate_title_translations(
26 title_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
27 _context: &(),
28) -> Result<(), garde::Error> {
29 if title_translations.len() > 512 {
30 return Err(garde::Error::new("cannot have more than 512 entries"));
31 }
32
33 for (lang, translation) in title_translations {
34 if lang.len() < 2 || lang.len() > 15 {
35 return Err(garde::Error::new(format!(
36 "language code '{}' must be between 2 and 15 characters",
37 lang
38 )));
39 }
40 if translation.is_empty() || translation.len() > 255 {
41 return Err(garde::Error::new(format!(
42 "translation for language '{}' must be between 1 and 255 characters",
43 lang
44 )));
45 }
46 }
47
48 Ok(())
49}
50
51pub fn validate_content_translations(
52 content_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
53 _context: &(),
54) -> Result<(), garde::Error> {
55 if content_translations.len() > 512 {
56 return Err(garde::Error::new("cannot have more than 512 entries"));
57 }
58
59 for (lang, translation) in content_translations {
60 if lang.len() < 2 || lang.len() > 15 {
61 return Err(garde::Error::new(format!(
62 "language code '{}' must be between 2 and 15 characters",
63 lang
64 )));
65 }
66 if translation.is_empty() || translation.len() > 2048 {
67 return Err(garde::Error::new(format!(
68 "translation for language '{}' must be between 1 and 2048 characters",
69 lang
70 )));
71 }
72 }
73
74 Ok(())
75}
76
77#[derive(Serialize, Deserialize, Clone)]
78pub struct Announcement {
79 pub uuid: uuid::Uuid,
80
81 pub r#type: AnnouncementType,
82 pub enabled: bool,
83 pub enabled_start: Option<chrono::NaiveDateTime>,
84 pub enabled_end: Option<chrono::NaiveDateTime>,
85 pub dismissible: bool,
86 pub dismissible_end: Option<chrono::NaiveDateTime>,
87
88 pub title: compact_str::CompactString,
89 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
90 pub content: compact_str::CompactString,
91 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
92
93 pub locations: Vec<uuid::Uuid>,
94 pub nodes: Vec<uuid::Uuid>,
95 pub backup_configurations: Vec<uuid::Uuid>,
96 pub eggs: Vec<uuid::Uuid>,
97
98 pub created: chrono::NaiveDateTime,
99
100 extension_data: super::ModelExtensionData,
101}
102
103impl BaseModel for Announcement {
104 const NAME: &'static str = "announcement";
105
106 fn get_extension_list() -> &'static super::ModelExtensionList {
107 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
108 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
109
110 &EXTENSIONS
111 }
112
113 fn get_extension_data(&self) -> &super::ModelExtensionData {
114 &self.extension_data
115 }
116
117 #[inline]
118 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
119 let prefix = prefix.unwrap_or_default();
120
121 BTreeMap::from([
122 (
123 "announcements.uuid",
124 compact_str::format_compact!("{prefix}uuid"),
125 ),
126 (
127 "announcements.type",
128 compact_str::format_compact!("{prefix}type"),
129 ),
130 (
131 "announcements.enabled",
132 compact_str::format_compact!("{prefix}enabled"),
133 ),
134 (
135 "announcements.enabled_start",
136 compact_str::format_compact!("{prefix}enabled_start"),
137 ),
138 (
139 "announcements.enabled_end",
140 compact_str::format_compact!("{prefix}enabled_end"),
141 ),
142 (
143 "announcements.dismissible",
144 compact_str::format_compact!("{prefix}dismissible"),
145 ),
146 (
147 "announcements.dismissible_end",
148 compact_str::format_compact!("{prefix}dismissible_end"),
149 ),
150 (
151 "announcements.title",
152 compact_str::format_compact!("{prefix}title"),
153 ),
154 (
155 "announcements.title_translations",
156 compact_str::format_compact!("{prefix}title_translations"),
157 ),
158 (
159 "announcements.content",
160 compact_str::format_compact!("{prefix}content"),
161 ),
162 (
163 "announcements.content_translations",
164 compact_str::format_compact!("{prefix}content_translations"),
165 ),
166 (
167 "announcements.locations",
168 compact_str::format_compact!("{prefix}locations"),
169 ),
170 (
171 "announcements.nodes",
172 compact_str::format_compact!("{prefix}nodes"),
173 ),
174 (
175 "announcements.backup_configurations",
176 compact_str::format_compact!("{prefix}backup_configurations"),
177 ),
178 (
179 "announcements.eggs",
180 compact_str::format_compact!("{prefix}eggs"),
181 ),
182 (
183 "announcements.created",
184 compact_str::format_compact!("{prefix}created"),
185 ),
186 ])
187 }
188
189 #[inline]
190 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
191 let prefix = prefix.unwrap_or_default();
192
193 Ok(Self {
194 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
195 r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
196 enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
197 enabled_start: row
198 .try_get(compact_str::format_compact!("{prefix}enabled_start").as_str())?,
199 enabled_end: row
200 .try_get(compact_str::format_compact!("{prefix}enabled_end").as_str())?,
201 dismissible: row
202 .try_get(compact_str::format_compact!("{prefix}dismissible").as_str())?,
203 dismissible_end: row
204 .try_get(compact_str::format_compact!("{prefix}dismissible_end").as_str())?,
205 title: row.try_get(compact_str::format_compact!("{prefix}title").as_str())?,
206 title_translations: serde_json::from_value(
207 row.try_get(compact_str::format_compact!("{prefix}title_translations").as_str())?,
208 )?,
209 content: row.try_get(compact_str::format_compact!("{prefix}content").as_str())?,
210 content_translations: serde_json::from_value(
211 row.try_get(compact_str::format_compact!("{prefix}content_translations").as_str())?,
212 )?,
213 locations: row.try_get(compact_str::format_compact!("{prefix}locations").as_str())?,
214 nodes: row.try_get(compact_str::format_compact!("{prefix}nodes").as_str())?,
215 backup_configurations: row
216 .try_get(compact_str::format_compact!("{prefix}backup_configurations").as_str())?,
217 eggs: row.try_get(compact_str::format_compact!("{prefix}eggs").as_str())?,
218 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
219 extension_data: Self::map_extensions(prefix, row)?,
220 })
221 }
222}
223
224impl Announcement {
225 pub async fn all_with_pagination(
226 database: &crate::database::Database,
227 page: i64,
228 per_page: i64,
229 search: Option<&str>,
230 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
231 let offset = (page - 1) * per_page;
232
233 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
234 r#"
235 SELECT {}, COUNT(*) OVER() AS total_count
236 FROM announcements
237 WHERE ($1 IS NULL OR announcements.title ILIKE '%' || $1 || '%')
238 ORDER BY announcements.created
239 LIMIT $2 OFFSET $3
240 "#,
241 Self::columns_sql(None)
242 )))
243 .bind(search)
244 .bind(per_page)
245 .bind(offset)
246 .fetch_all(database.read())
247 .await?;
248
249 Ok(super::Pagination {
250 total: rows
251 .first()
252 .map_or(Ok(0), |row| row.try_get("total_count"))?,
253 per_page,
254 page,
255 data: rows
256 .into_iter()
257 .map(|row| Self::map(None, &row))
258 .try_collect_vec()?,
259 })
260 }
261
262 pub async fn all_by_active(
263 database: &crate::database::Database,
264 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
265 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
266 r#"
267 SELECT {}
268 FROM announcements
269 WHERE announcements.enabled = true
270 AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
271 AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
272 AND array_length(announcements.locations, 1) IS NULL
273 AND array_length(announcements.nodes, 1) IS NULL
274 AND array_length(announcements.backup_configurations, 1) IS NULL
275 AND array_length(announcements.eggs, 1) IS NULL
276 ORDER BY announcements.created
277 "#,
278 Self::columns_sql(None)
279 )))
280 .fetch_all(database.read())
281 .await?;
282
283 rows.into_iter()
284 .map(|row| Self::map(None, &row))
285 .try_collect_vec()
286 }
287
288 pub async fn all_by_active_server(
289 database: &crate::database::Database,
290 server: &super::server::Server,
291 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
292 let node = server.node.fetch_cached(database).await?;
293
294 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
295 r#"
296 SELECT {}
297 FROM announcements
298 WHERE announcements.enabled = true
299 AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
300 AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
301 AND (
302 array_length(announcements.locations, 1) IS NOT NULL
303 OR array_length(announcements.nodes, 1) IS NOT NULL
304 OR array_length(announcements.backup_configurations, 1) IS NOT NULL
305 OR array_length(announcements.eggs, 1) IS NOT NULL
306 )
307 AND (
308 (announcements.locations IS NULL OR $1 = ANY(announcements.locations))
309 OR (announcements.nodes IS NULL OR $2 = ANY(announcements.nodes))
310 OR ($3 IS NOT NULL AND (announcements.backup_configurations IS NULL OR $3 = ANY(announcements.backup_configurations)))
311 OR (announcements.eggs IS NULL OR $4 = ANY(announcements.eggs))
312 )
313 ORDER BY announcements.created
314 "#,
315 Self::columns_sql(None)
316 )))
317 .bind(node.location.uuid)
318 .bind(node.uuid)
319 .bind(server.backup_configuration.as_ref().map_or_else(
320 || {
321 node.backup_configuration.as_ref().map_or_else(
322 || node.location.backup_configuration.as_ref().map(|b| b.uuid),
323 |b| Some(b.uuid),
324 )
325 },
326 |b| Some(b.uuid),
327 ))
328 .bind(server.egg.uuid)
329 .fetch_all(database.read())
330 .await?;
331
332 rows.into_iter()
333 .map(|row| Self::map(None, &row))
334 .try_collect_vec()
335 }
336
337 pub async fn cleanup_uuid_arrays(
338 database: &crate::database::Database,
339 ) -> Result<u64, crate::database::DatabaseError> {
340 let result = sqlx::query(
341 "UPDATE announcements
342 SET
343 locations = CASE
344 WHEN EXISTS (
345 SELECT 1 FROM unnest(locations) AS u
346 WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u)
347 )
348 THEN COALESCE(
349 (SELECT array_agg(u) FROM unnest(locations) AS u
350 WHERE EXISTS (SELECT 1 FROM locations WHERE uuid = u)),
351 '{}'::uuid[]
352 )
353 ELSE locations
354 END,
355 nodes = CASE
356 WHEN EXISTS (
357 SELECT 1 FROM unnest(nodes) AS u
358 WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u)
359 )
360 THEN COALESCE(
361 (SELECT array_agg(u) FROM unnest(nodes) AS u
362 WHERE EXISTS (SELECT 1 FROM nodes WHERE uuid = u)),
363 '{}'::uuid[]
364 )
365 ELSE nodes
366 END,
367 backup_configurations = CASE
368 WHEN EXISTS (
369 SELECT 1 FROM unnest(backup_configurations) AS u
370 WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)
371 )
372 THEN COALESCE(
373 (SELECT array_agg(u) FROM unnest(backup_configurations) AS u
374 WHERE EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)),
375 '{}'::uuid[]
376 )
377 ELSE backup_configurations
378 END,
379 eggs = CASE
380 WHEN EXISTS (
381 SELECT 1 FROM unnest(eggs) AS u
382 WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)
383 )
384 THEN COALESCE(
385 (SELECT array_agg(u) FROM unnest(eggs) AS u
386 WHERE EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)),
387 '{}'::uuid[]
388 )
389 ELSE eggs
390 END
391 WHERE
392 EXISTS (SELECT 1 FROM unnest(locations) AS u WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u))
393 OR EXISTS (SELECT 1 FROM unnest(nodes) AS u WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u))
394 OR EXISTS (SELECT 1 FROM unnest(backup_configurations) AS u WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u))
395 OR EXISTS (SELECT 1 FROM unnest(eggs) AS u WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u))",
396 )
397 .execute(database.write())
398 .await?;
399
400 Ok(result.rows_affected())
401 }
402}
403
404#[async_trait::async_trait]
405impl IntoAdminApiObject for Announcement {
406 type AdminApiObject = AdminApiAnnouncement;
407 type ExtraArgs<'a> = ();
408
409 async fn into_admin_api_object<'a>(
410 mut self,
411 state: &crate::State,
412 _args: Self::ExtraArgs<'a>,
413 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
414 let api_object = AdminApiAnnouncement::init_hooks(&self, state).await?;
415
416 let api_object = finish_extendible!(
417 AdminApiAnnouncement {
418 uuid: self.uuid,
419 r#type: self.r#type,
420 enabled: self.enabled,
421 enabled_start: self.enabled_start.map(|dt| dt.and_utc()),
422 enabled_end: self.enabled_end.map(|dt| dt.and_utc()),
423 dismissible: self.dismissible,
424 dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
425 title: self.title,
426 title_translations: self.title_translations,
427 content: self.content,
428 content_translations: self.content_translations,
429 locations: self.locations,
430 nodes: self.nodes,
431 backup_configurations: self.backup_configurations,
432 eggs: self.eggs,
433 created: self.created.and_utc(),
434 },
435 api_object,
436 state
437 )?;
438
439 Ok(api_object)
440 }
441}
442
443#[async_trait::async_trait]
444impl IntoApiObject for Announcement {
445 type ApiObject = ApiAnnouncement;
446 type ExtraArgs<'a> = ();
447
448 async fn into_api_object<'a>(
449 self,
450 state: &crate::State,
451 _args: Self::ExtraArgs<'a>,
452 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
453 let api_object = ApiAnnouncement::init_hooks(&self, state).await?;
454
455 let api_object = finish_extendible!(
456 ApiAnnouncement {
457 uuid: self.uuid,
458 r#type: self.r#type,
459 dismissible: self.dismissible,
460 dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
461 title: self.title,
462 title_translations: self.title_translations,
463 content: self.content,
464 content_translations: self.content_translations,
465 },
466 api_object,
467 state
468 )?;
469
470 Ok(api_object)
471 }
472}
473
474#[async_trait::async_trait]
475impl ByUuid for Announcement {
476 async fn by_uuid(
477 database: &crate::database::Database,
478 uuid: uuid::Uuid,
479 ) -> Result<Self, crate::database::DatabaseError> {
480 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
481 r#"
482 SELECT {}
483 FROM announcements
484 WHERE announcements.uuid = $1
485 "#,
486 Self::columns_sql(None)
487 )))
488 .bind(uuid)
489 .fetch_one(database.read())
490 .await?;
491
492 Self::map(None, &row)
493 }
494
495 async fn by_uuid_with_transaction(
496 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
497 uuid: uuid::Uuid,
498 ) -> Result<Self, crate::database::DatabaseError> {
499 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
500 r#"
501 SELECT {}
502 FROM announcements
503 WHERE announcements.uuid = $1
504 "#,
505 Self::columns_sql(None)
506 )))
507 .bind(uuid)
508 .fetch_one(&mut **transaction)
509 .await?;
510
511 Self::map(None, &row)
512 }
513}
514
515#[derive(ToSchema, Deserialize, Validate)]
516pub struct CreateAnnouncementOptions {
517 #[garde(skip)]
518 pub r#type: AnnouncementType,
519
520 #[garde(skip)]
521 pub enabled: bool,
522 #[garde(skip)]
523 pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
524 #[garde(skip)]
525 pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
526 #[garde(skip)]
527 pub dismissible: bool,
528 #[garde(skip)]
529 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
530
531 #[garde(length(chars, min = 1, max = 255))]
532 #[schema(min_length = 1, max_length = 255)]
533 pub title: compact_str::CompactString,
534 #[garde(custom(validate_title_translations))]
535 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
536 #[garde(length(chars, min = 1, max = 2048))]
537 #[schema(min_length = 1, max_length = 2048)]
538 pub content: compact_str::CompactString,
539 #[garde(custom(validate_content_translations))]
540 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
541
542 #[garde(length(max = 100))]
543 #[schema(max_length = 100)]
544 pub locations: Vec<uuid::Uuid>,
545 #[garde(length(max = 100))]
546 #[schema(max_length = 100)]
547 pub nodes: Vec<uuid::Uuid>,
548 #[garde(length(max = 100))]
549 #[schema(max_length = 100)]
550 pub backup_configurations: Vec<uuid::Uuid>,
551 #[garde(length(max = 100))]
552 #[schema(max_length = 100)]
553 pub eggs: Vec<uuid::Uuid>,
554}
555
556#[async_trait::async_trait]
557impl CreatableModel for Announcement {
558 type CreateOptions<'a> = CreateAnnouncementOptions;
559 type CreateResult = Self;
560
561 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
562 static CREATE_LISTENERS: LazyLock<CreateListenerList<Announcement>> =
563 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
564
565 &CREATE_LISTENERS
566 }
567
568 async fn create_with_transaction(
569 state: &crate::State,
570 mut options: Self::CreateOptions<'_>,
571 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
572 ) -> Result<Self, crate::database::DatabaseError> {
573 options.validate()?;
574
575 let mut query_builder = InsertQueryBuilder::new("announcements");
576
577 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
578
579 query_builder
580 .set("type", options.r#type)
581 .set("enabled", options.enabled)
582 .set("enabled_start", options.enabled_start)
583 .set("enabled_end", options.enabled_end)
584 .set("dismissible", options.dismissible)
585 .set("dismissible_end", options.dismissible_end)
586 .set("title", &options.title)
587 .set(
588 "title_translations",
589 serde_json::to_value(&options.title_translations)?,
590 )
591 .set("content", &options.content)
592 .set(
593 "content_translations",
594 serde_json::to_value(&options.content_translations)?,
595 )
596 .set("locations", &options.locations)
597 .set("nodes", &options.nodes)
598 .set("backup_configurations", &options.backup_configurations)
599 .set("eggs", &options.eggs);
600
601 let row = query_builder
602 .returning(&Self::columns_sql(None))
603 .fetch_one(&mut **transaction)
604 .await?;
605 let mut announcement = Self::map(None, &row)?;
606
607 Self::run_after_create_handlers(&mut announcement, &options, state, transaction).await?;
608
609 Ok(announcement)
610 }
611}
612
613#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
614pub struct UpdateAnnouncementOptions {
615 #[garde(skip)]
616 pub r#type: Option<AnnouncementType>,
617
618 #[garde(skip)]
619 pub enabled: Option<bool>,
620 #[garde(skip)]
621 #[serde(default, with = "::serde_with::rust::double_option")]
622 pub enabled_start: Option<Option<chrono::DateTime<chrono::Utc>>>,
623 #[garde(skip)]
624 #[serde(default, with = "::serde_with::rust::double_option")]
625 pub enabled_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
626 #[garde(skip)]
627 pub dismissible: Option<bool>,
628 #[garde(skip)]
629 #[serde(default, with = "::serde_with::rust::double_option")]
630 pub dismissible_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
631
632 #[garde(length(chars, min = 1, max = 255))]
633 #[schema(min_length = 1, max_length = 255)]
634 pub title: Option<compact_str::CompactString>,
635 #[garde(inner(custom(validate_title_translations)))]
636 pub title_translations:
637 Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
638 #[garde(length(chars, min = 1, max = 2048))]
639 #[schema(min_length = 1, max_length = 2048)]
640 pub content: Option<compact_str::CompactString>,
641 #[garde(inner(custom(validate_content_translations)))]
642 pub content_translations:
643 Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
644
645 #[garde(length(max = 100))]
646 #[schema(max_length = 100)]
647 pub locations: Option<Vec<uuid::Uuid>>,
648 #[garde(length(max = 100))]
649 #[schema(max_length = 100)]
650 pub nodes: Option<Vec<uuid::Uuid>>,
651 #[garde(length(max = 100))]
652 #[schema(max_length = 100)]
653 pub backup_configurations: Option<Vec<uuid::Uuid>>,
654 #[garde(length(max = 100))]
655 #[schema(max_length = 100)]
656 pub eggs: Option<Vec<uuid::Uuid>>,
657}
658
659#[async_trait::async_trait]
660impl UpdatableModel for Announcement {
661 type UpdateOptions = UpdateAnnouncementOptions;
662
663 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
664 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Announcement>> =
665 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
666
667 &UPDATE_LISTENERS
668 }
669
670 async fn update_with_transaction(
671 &mut self,
672 state: &crate::State,
673 mut options: Self::UpdateOptions,
674 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
675 ) -> Result<(), crate::database::DatabaseError> {
676 options.validate()?;
677
678 let mut query_builder = UpdateQueryBuilder::new("announcements");
679
680 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
681 .await?;
682
683 query_builder
684 .set("type", options.r#type)
685 .set("enabled", options.enabled)
686 .set(
687 "enabled_start",
688 options
689 .enabled_start
690 .as_ref()
691 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
692 )
693 .set(
694 "enabled_end",
695 options
696 .enabled_end
697 .as_ref()
698 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
699 )
700 .set("dismissible", options.dismissible)
701 .set(
702 "dismissible_end",
703 options
704 .dismissible_end
705 .as_ref()
706 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
707 )
708 .set("title", options.title.as_ref())
709 .set(
710 "title_translations",
711 options
712 .title_translations
713 .as_ref()
714 .map(serde_json::to_value)
715 .transpose()?,
716 )
717 .set("content", options.content.as_ref())
718 .set(
719 "content_translations",
720 options
721 .content_translations
722 .as_ref()
723 .map(serde_json::to_value)
724 .transpose()?,
725 )
726 .set("locations", options.locations.as_ref())
727 .set("nodes", options.nodes.as_ref())
728 .set(
729 "backup_configurations",
730 options.backup_configurations.as_ref(),
731 )
732 .set("eggs", options.eggs.as_ref())
733 .where_eq("uuid", self.uuid);
734
735 query_builder.execute(&mut **transaction).await?;
736
737 if let Some(r#type) = options.r#type {
738 self.r#type = r#type;
739 }
740 if let Some(enabled) = options.enabled {
741 self.enabled = enabled;
742 }
743 if let Some(enabled_start) = options.enabled_start {
744 self.enabled_start = enabled_start.map(|dt| dt.naive_utc());
745 }
746 if let Some(enabled_end) = options.enabled_end {
747 self.enabled_end = enabled_end.map(|dt| dt.naive_utc());
748 }
749 if let Some(dismissible) = options.dismissible {
750 self.dismissible = dismissible;
751 }
752 if let Some(dismissible_end) = options.dismissible_end {
753 self.dismissible_end = dismissible_end.map(|dt| dt.naive_utc());
754 }
755 if let Some(title) = options.title {
756 self.title = title;
757 }
758 if let Some(title_translations) = options.title_translations {
759 self.title_translations = title_translations;
760 }
761 if let Some(content) = options.content {
762 self.content = content;
763 }
764 if let Some(content_translations) = options.content_translations {
765 self.content_translations = content_translations;
766 }
767 if let Some(locations) = options.locations {
768 self.locations = locations;
769 }
770 if let Some(nodes) = options.nodes {
771 self.nodes = nodes;
772 }
773 if let Some(backup_configurations) = options.backup_configurations {
774 self.backup_configurations = backup_configurations;
775 }
776 if let Some(eggs) = options.eggs {
777 self.eggs = eggs;
778 }
779
780 self.run_after_update_handlers(state, transaction).await?;
781
782 Ok(())
783 }
784}
785
786#[async_trait::async_trait]
787impl DeletableModel for Announcement {
788 type DeleteOptions = ();
789
790 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
791 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Announcement>> =
792 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
793
794 &DELETE_LISTENERS
795 }
796
797 async fn delete_with_transaction(
798 &self,
799 state: &crate::State,
800 options: Self::DeleteOptions,
801 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
802 ) -> Result<(), anyhow::Error> {
803 self.run_delete_handlers(&options, state, transaction)
804 .await?;
805
806 sqlx::query(
807 r#"
808 DELETE FROM announcements
809 WHERE announcements.uuid = $1
810 "#,
811 )
812 .bind(self.uuid)
813 .execute(&mut **transaction)
814 .await?;
815
816 self.run_after_delete_handlers(&options, state, transaction)
817 .await?;
818
819 Ok(())
820 }
821}
822
823#[derive(Validate)]
824pub struct DuplicateAnnouncementOptions {}
825
826#[async_trait::async_trait]
827impl DuplicableModel for Announcement {
828 type DuplicateOptions<'a> = DuplicateAnnouncementOptions;
829
830 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
831 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Announcement>> =
832 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
833
834 &DUPLICATE_LISTENERS
835 }
836
837 async fn duplicate_with_transaction(
838 &self,
839 state: &crate::State,
840 options: Self::DuplicateOptions<'_>,
841 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
842 ) -> Result<Self, crate::database::DatabaseError> {
843 options.validate()?;
844
845 self.run_duplicate_handlers(&options, state, transaction)
846 .await?;
847
848 let mut query_builder = InsertQueryBuilder::new("announcements");
849
850 query_builder
851 .set("type", self.r#type)
852 .set("enabled", self.enabled)
853 .set("enabled_start", self.enabled_start)
854 .set("enabled_end", self.enabled_end)
855 .set("dismissible", self.dismissible)
856 .set("dismissible_end", self.dismissible_end)
857 .set("title", &self.title)
858 .set(
859 "title_translations",
860 serde_json::to_value(&self.title_translations)?,
861 )
862 .set("content", &self.content)
863 .set(
864 "content_translations",
865 serde_json::to_value(&self.content_translations)?,
866 )
867 .set("locations", &self.locations)
868 .set("nodes", &self.nodes)
869 .set("backup_configurations", &self.backup_configurations)
870 .set("eggs", &self.eggs);
871
872 let row = query_builder
873 .returning(&Self::columns_sql(None))
874 .fetch_one(&mut **transaction)
875 .await?;
876 let mut announcement = Self::map(None, &row)?;
877
878 self.run_after_duplicate_handlers(&mut announcement, &options, state, transaction)
879 .await?;
880
881 Ok(announcement)
882 }
883}
884
885#[schema_extension_derive::extendible]
886#[init_args(Announcement, crate::State)]
887#[hook_args(crate::State)]
888#[derive(ToSchema, Serialize)]
889#[schema(title = "AdminAnnouncement")]
890pub struct AdminApiAnnouncement {
891 pub uuid: uuid::Uuid,
892
893 pub r#type: AnnouncementType,
894 pub enabled: bool,
895 pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
896 pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
897 pub dismissible: bool,
898 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
899
900 pub title: compact_str::CompactString,
901 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
902 pub content: compact_str::CompactString,
903 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
904
905 pub locations: Vec<uuid::Uuid>,
906 pub nodes: Vec<uuid::Uuid>,
907 pub backup_configurations: Vec<uuid::Uuid>,
908 pub eggs: Vec<uuid::Uuid>,
909
910 pub created: chrono::DateTime<chrono::Utc>,
911}
912
913#[schema_extension_derive::extendible]
914#[init_args(Announcement, crate::State)]
915#[hook_args(crate::State)]
916#[derive(ToSchema, Serialize)]
917#[schema(title = "ApiAnnouncement")]
918pub struct ApiAnnouncement {
919 pub uuid: uuid::Uuid,
920
921 pub r#type: AnnouncementType,
922 pub dismissible: bool,
923 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
924
925 pub title: compact_str::CompactString,
926 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
927 pub content: compact_str::CompactString,
928 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
929}