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 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
224 vec![
225 compact_str::format_compact!("{}::{}", Self::NAME, self.uuid),
226 compact_str::CompactString::const_new("announcements::active"),
227 ]
228 }
229}
230
231impl Announcement {
232 pub async fn all_with_pagination(
233 database: &crate::database::Database,
234 page: i64,
235 per_page: i64,
236 search: Option<&str>,
237 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
238 super::PaginatedQueryBuilder::new(
239 Self::columns_sql(None),
240 format!(
241 r#"
242 FROM announcements
243 WHERE {search}
244 "#,
245 search = super::search_sql(1, &["announcements.title"], &["announcements.uuid"]),
246 ),
247 "announcements.created, announcements.uuid",
248 )
249 .bind(search)
250 .fetch(database, page, per_page)
251 .await
252 }
253
254 pub async fn all_by_active(
255 database: &crate::database::Database,
256 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
257 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
258 r#"
259 SELECT {}
260 FROM announcements
261 WHERE announcements.enabled = true
262 AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
263 AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
264 AND array_length(announcements.locations, 1) IS NULL
265 AND array_length(announcements.nodes, 1) IS NULL
266 AND array_length(announcements.backup_configurations, 1) IS NULL
267 AND array_length(announcements.eggs, 1) IS NULL
268 ORDER BY announcements.created, announcements.uuid
269 "#,
270 Self::columns_sql(None)
271 )))
272 .fetch_all(database.read())
273 .await?;
274
275 rows.into_iter()
276 .map(|row| Self::map(None, &row))
277 .try_collect_vec()
278 }
279
280 pub async fn all_by_active_server(
281 database: &crate::database::Database,
282 server: &super::server::Server,
283 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
284 let node = server.node.fetch_cached(database).await?;
285
286 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
287 r#"
288 SELECT {}
289 FROM announcements
290 WHERE announcements.enabled = true
291 AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
292 AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
293 AND (
294 array_length(announcements.locations, 1) IS NOT NULL
295 OR array_length(announcements.nodes, 1) IS NOT NULL
296 OR array_length(announcements.backup_configurations, 1) IS NOT NULL
297 OR array_length(announcements.eggs, 1) IS NOT NULL
298 )
299 AND (
300 (announcements.locations IS NULL OR $1 = ANY(announcements.locations))
301 OR (announcements.nodes IS NULL OR $2 = ANY(announcements.nodes))
302 OR ($3 IS NOT NULL AND (announcements.backup_configurations IS NULL OR $3 = ANY(announcements.backup_configurations)))
303 OR (announcements.eggs IS NULL OR $4 = ANY(announcements.eggs))
304 )
305 ORDER BY announcements.created, announcements.uuid
306 "#,
307 Self::columns_sql(None)
308 )))
309 .bind(node.location.uuid)
310 .bind(node.uuid)
311 .bind(server.backup_configuration.as_ref().map_or_else(
312 || {
313 node.backup_configuration.as_ref().map_or_else(
314 || node.location.backup_configuration.as_ref().map(|b| b.uuid),
315 |b| Some(b.uuid),
316 )
317 },
318 |b| Some(b.uuid),
319 ))
320 .bind(server.egg.uuid)
321 .fetch_all(database.read())
322 .await?;
323
324 rows.into_iter()
325 .map(|row| Self::map(None, &row))
326 .try_collect_vec()
327 }
328
329 pub async fn cleanup_uuid_arrays(
330 database: &crate::database::Database,
331 ) -> Result<u64, crate::database::DatabaseError> {
332 let result = sqlx::query(
333 "UPDATE announcements
334 SET
335 locations = CASE
336 WHEN EXISTS (
337 SELECT 1 FROM unnest(locations) AS u
338 WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u)
339 )
340 THEN COALESCE(
341 (SELECT array_agg(u) FROM unnest(locations) AS u
342 WHERE EXISTS (SELECT 1 FROM locations WHERE uuid = u)),
343 '{}'::uuid[]
344 )
345 ELSE locations
346 END,
347 nodes = CASE
348 WHEN EXISTS (
349 SELECT 1 FROM unnest(nodes) AS u
350 WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u)
351 )
352 THEN COALESCE(
353 (SELECT array_agg(u) FROM unnest(nodes) AS u
354 WHERE EXISTS (SELECT 1 FROM nodes WHERE uuid = u)),
355 '{}'::uuid[]
356 )
357 ELSE nodes
358 END,
359 backup_configurations = CASE
360 WHEN EXISTS (
361 SELECT 1 FROM unnest(backup_configurations) AS u
362 WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)
363 )
364 THEN COALESCE(
365 (SELECT array_agg(u) FROM unnest(backup_configurations) AS u
366 WHERE EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u)),
367 '{}'::uuid[]
368 )
369 ELSE backup_configurations
370 END,
371 eggs = CASE
372 WHEN EXISTS (
373 SELECT 1 FROM unnest(eggs) AS u
374 WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)
375 )
376 THEN COALESCE(
377 (SELECT array_agg(u) FROM unnest(eggs) AS u
378 WHERE EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)),
379 '{}'::uuid[]
380 )
381 ELSE eggs
382 END
383 WHERE
384 EXISTS (SELECT 1 FROM unnest(locations) AS u WHERE NOT EXISTS (SELECT 1 FROM locations WHERE uuid = u))
385 OR EXISTS (SELECT 1 FROM unnest(nodes) AS u WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE uuid = u))
386 OR EXISTS (SELECT 1 FROM unnest(backup_configurations) AS u WHERE NOT EXISTS (SELECT 1 FROM backup_configurations WHERE uuid = u))
387 OR EXISTS (SELECT 1 FROM unnest(eggs) AS u WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u))",
388 )
389 .execute(database.write())
390 .await?;
391
392 Ok(result.rows_affected())
393 }
394}
395
396#[async_trait::async_trait]
397impl IntoAdminApiObject for Announcement {
398 type AdminApiObject = AdminApiAnnouncement;
399 type ExtraArgs<'a> = ();
400
401 async fn into_admin_api_object<'a>(
402 mut self,
403 state: &crate::State,
404 _args: Self::ExtraArgs<'a>,
405 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
406 let api_object = AdminApiAnnouncement::init_hooks(&self, state).await?;
407
408 let api_object = finish_extendible!(
409 AdminApiAnnouncement {
410 uuid: self.uuid,
411 r#type: self.r#type,
412 enabled: self.enabled,
413 enabled_start: self.enabled_start.map(|dt| dt.and_utc()),
414 enabled_end: self.enabled_end.map(|dt| dt.and_utc()),
415 dismissible: self.dismissible,
416 dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
417 title: self.title,
418 title_translations: self.title_translations,
419 content: self.content,
420 content_translations: self.content_translations,
421 locations: self.locations,
422 nodes: self.nodes,
423 backup_configurations: self.backup_configurations,
424 eggs: self.eggs,
425 created: self.created.and_utc(),
426 },
427 api_object,
428 state
429 )?;
430
431 Ok(api_object)
432 }
433}
434
435#[async_trait::async_trait]
436impl IntoApiObject for Announcement {
437 type ApiObject = ApiAnnouncement;
438 type ExtraArgs<'a> = ();
439
440 async fn into_api_object<'a>(
441 self,
442 state: &crate::State,
443 _args: Self::ExtraArgs<'a>,
444 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
445 let api_object = ApiAnnouncement::init_hooks(&self, state).await?;
446
447 let api_object = finish_extendible!(
448 ApiAnnouncement {
449 uuid: self.uuid,
450 r#type: self.r#type,
451 dismissible: self.dismissible,
452 dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
453 title: self.title,
454 title_translations: self.title_translations,
455 content: self.content,
456 content_translations: self.content_translations,
457 },
458 api_object,
459 state
460 )?;
461
462 Ok(api_object)
463 }
464}
465
466#[async_trait::async_trait]
467impl ByUuid for Announcement {
468 async fn by_uuid(
469 database: &crate::database::Database,
470 uuid: uuid::Uuid,
471 ) -> Result<Self, crate::database::DatabaseError> {
472 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
473 r#"
474 SELECT {}
475 FROM announcements
476 WHERE announcements.uuid = $1
477 "#,
478 Self::columns_sql(None)
479 )))
480 .bind(uuid)
481 .fetch_one(database.read())
482 .await?;
483
484 Self::map(None, &row)
485 }
486
487 async fn by_uuid_with_transaction(
488 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
489 uuid: uuid::Uuid,
490 ) -> Result<Self, crate::database::DatabaseError> {
491 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
492 r#"
493 SELECT {}
494 FROM announcements
495 WHERE announcements.uuid = $1
496 "#,
497 Self::columns_sql(None)
498 )))
499 .bind(uuid)
500 .fetch_one(&mut **transaction)
501 .await?;
502
503 Self::map(None, &row)
504 }
505}
506
507#[derive(ToSchema, Deserialize, Validate)]
508pub struct CreateAnnouncementOptions {
509 #[garde(skip)]
510 pub r#type: AnnouncementType,
511
512 #[garde(skip)]
513 pub enabled: bool,
514 #[garde(skip)]
515 pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
516 #[garde(skip)]
517 pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
518 #[garde(skip)]
519 pub dismissible: bool,
520 #[garde(skip)]
521 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
522
523 #[garde(length(chars, min = 1, max = 255))]
524 #[schema(min_length = 1, max_length = 255)]
525 pub title: compact_str::CompactString,
526 #[garde(custom(validate_title_translations))]
527 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
528 #[garde(length(chars, min = 1, max = 2048))]
529 #[schema(min_length = 1, max_length = 2048)]
530 pub content: compact_str::CompactString,
531 #[garde(custom(validate_content_translations))]
532 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
533
534 #[garde(length(max = 100))]
535 #[schema(max_length = 100)]
536 pub locations: Vec<uuid::Uuid>,
537 #[garde(length(max = 100))]
538 #[schema(max_length = 100)]
539 pub nodes: Vec<uuid::Uuid>,
540 #[garde(length(max = 100))]
541 #[schema(max_length = 100)]
542 pub backup_configurations: Vec<uuid::Uuid>,
543 #[garde(length(max = 100))]
544 #[schema(max_length = 100)]
545 pub eggs: Vec<uuid::Uuid>,
546}
547
548#[async_trait::async_trait]
549impl CreatableModel for Announcement {
550 type CreateOptions<'a> = CreateAnnouncementOptions;
551 type CreateResult = Self;
552
553 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
554 static CREATE_LISTENERS: LazyLock<CreateListenerList<Announcement>> =
555 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
556
557 &CREATE_LISTENERS
558 }
559
560 async fn create_with_transaction(
561 state: &crate::State,
562 mut options: Self::CreateOptions<'_>,
563 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
564 ) -> Result<Self, crate::database::DatabaseError> {
565 options.validate()?;
566
567 let mut query_builder = InsertQueryBuilder::new("announcements");
568
569 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
570
571 query_builder
572 .set("type", options.r#type)
573 .set("enabled", options.enabled)
574 .set("enabled_start", options.enabled_start)
575 .set("enabled_end", options.enabled_end)
576 .set("dismissible", options.dismissible)
577 .set("dismissible_end", options.dismissible_end)
578 .set("title", &options.title)
579 .set(
580 "title_translations",
581 serde_json::to_value(&options.title_translations)?,
582 )
583 .set("content", &options.content)
584 .set(
585 "content_translations",
586 serde_json::to_value(&options.content_translations)?,
587 )
588 .set("locations", &options.locations)
589 .set("nodes", &options.nodes)
590 .set("backup_configurations", &options.backup_configurations)
591 .set("eggs", &options.eggs);
592
593 let row = query_builder
594 .returning(&Self::columns_sql(None))
595 .fetch_one(&mut **transaction)
596 .await?;
597 let mut announcement = Self::map(None, &row)?;
598
599 Self::run_after_create_handlers(&mut announcement, &options, state, transaction).await?;
600
601 Ok(announcement)
602 }
603}
604
605#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
606pub struct UpdateAnnouncementOptions {
607 #[garde(skip)]
608 pub r#type: Option<AnnouncementType>,
609
610 #[garde(skip)]
611 pub enabled: Option<bool>,
612 #[garde(skip)]
613 #[serde(default, with = "::serde_with::rust::double_option")]
614 pub enabled_start: Option<Option<chrono::DateTime<chrono::Utc>>>,
615 #[garde(skip)]
616 #[serde(default, with = "::serde_with::rust::double_option")]
617 pub enabled_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
618 #[garde(skip)]
619 pub dismissible: Option<bool>,
620 #[garde(skip)]
621 #[serde(default, with = "::serde_with::rust::double_option")]
622 pub dismissible_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
623
624 #[garde(length(chars, min = 1, max = 255))]
625 #[schema(min_length = 1, max_length = 255)]
626 pub title: Option<compact_str::CompactString>,
627 #[garde(inner(custom(validate_title_translations)))]
628 pub title_translations:
629 Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
630 #[garde(length(chars, min = 1, max = 2048))]
631 #[schema(min_length = 1, max_length = 2048)]
632 pub content: Option<compact_str::CompactString>,
633 #[garde(inner(custom(validate_content_translations)))]
634 pub content_translations:
635 Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
636
637 #[garde(length(max = 100))]
638 #[schema(max_length = 100)]
639 pub locations: Option<Vec<uuid::Uuid>>,
640 #[garde(length(max = 100))]
641 #[schema(max_length = 100)]
642 pub nodes: Option<Vec<uuid::Uuid>>,
643 #[garde(length(max = 100))]
644 #[schema(max_length = 100)]
645 pub backup_configurations: Option<Vec<uuid::Uuid>>,
646 #[garde(length(max = 100))]
647 #[schema(max_length = 100)]
648 pub eggs: Option<Vec<uuid::Uuid>>,
649}
650
651#[async_trait::async_trait]
652impl UpdatableModel for Announcement {
653 type UpdateOptions = UpdateAnnouncementOptions;
654
655 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
656 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Announcement>> =
657 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
658
659 &UPDATE_LISTENERS
660 }
661
662 async fn update_with_transaction(
663 &mut self,
664 state: &crate::State,
665 mut options: Self::UpdateOptions,
666 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
667 ) -> Result<(), crate::database::DatabaseError> {
668 options.validate()?;
669
670 let mut query_builder = UpdateQueryBuilder::new("announcements");
671
672 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
673 .await?;
674
675 query_builder
676 .set("type", options.r#type)
677 .set("enabled", options.enabled)
678 .set(
679 "enabled_start",
680 options
681 .enabled_start
682 .as_ref()
683 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
684 )
685 .set(
686 "enabled_end",
687 options
688 .enabled_end
689 .as_ref()
690 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
691 )
692 .set("dismissible", options.dismissible)
693 .set(
694 "dismissible_end",
695 options
696 .dismissible_end
697 .as_ref()
698 .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
699 )
700 .set("title", options.title.as_ref())
701 .set(
702 "title_translations",
703 options
704 .title_translations
705 .as_ref()
706 .map(serde_json::to_value)
707 .transpose()?,
708 )
709 .set("content", options.content.as_ref())
710 .set(
711 "content_translations",
712 options
713 .content_translations
714 .as_ref()
715 .map(serde_json::to_value)
716 .transpose()?,
717 )
718 .set("locations", options.locations.as_ref())
719 .set("nodes", options.nodes.as_ref())
720 .set(
721 "backup_configurations",
722 options.backup_configurations.as_ref(),
723 )
724 .set("eggs", options.eggs.as_ref())
725 .where_eq("uuid", self.uuid);
726
727 query_builder.execute(&mut **transaction).await?;
728
729 if let Some(r#type) = options.r#type {
730 self.r#type = r#type;
731 }
732 if let Some(enabled) = options.enabled {
733 self.enabled = enabled;
734 }
735 if let Some(enabled_start) = options.enabled_start {
736 self.enabled_start = enabled_start.map(|dt| dt.naive_utc());
737 }
738 if let Some(enabled_end) = options.enabled_end {
739 self.enabled_end = enabled_end.map(|dt| dt.naive_utc());
740 }
741 if let Some(dismissible) = options.dismissible {
742 self.dismissible = dismissible;
743 }
744 if let Some(dismissible_end) = options.dismissible_end {
745 self.dismissible_end = dismissible_end.map(|dt| dt.naive_utc());
746 }
747 if let Some(title) = options.title {
748 self.title = title;
749 }
750 if let Some(title_translations) = options.title_translations {
751 self.title_translations = title_translations;
752 }
753 if let Some(content) = options.content {
754 self.content = content;
755 }
756 if let Some(content_translations) = options.content_translations {
757 self.content_translations = content_translations;
758 }
759 if let Some(locations) = options.locations {
760 self.locations = locations;
761 }
762 if let Some(nodes) = options.nodes {
763 self.nodes = nodes;
764 }
765 if let Some(backup_configurations) = options.backup_configurations {
766 self.backup_configurations = backup_configurations;
767 }
768 if let Some(eggs) = options.eggs {
769 self.eggs = eggs;
770 }
771
772 self.run_after_update_handlers(state, transaction).await?;
773
774 Ok(())
775 }
776}
777
778#[async_trait::async_trait]
779impl DeletableModel for Announcement {
780 type DeleteOptions = ();
781
782 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
783 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Announcement>> =
784 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
785
786 &DELETE_LISTENERS
787 }
788
789 async fn delete_with_transaction(
790 &self,
791 state: &crate::State,
792 options: Self::DeleteOptions,
793 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
794 ) -> Result<(), anyhow::Error> {
795 self.run_delete_handlers(&options, state, transaction)
796 .await?;
797
798 sqlx::query(
799 r#"
800 DELETE FROM announcements
801 WHERE announcements.uuid = $1
802 "#,
803 )
804 .bind(self.uuid)
805 .execute(&mut **transaction)
806 .await?;
807
808 self.run_after_delete_handlers(&options, state, transaction)
809 .await?;
810
811 Ok(())
812 }
813}
814
815#[derive(Validate)]
816pub struct DuplicateAnnouncementOptions {}
817
818#[async_trait::async_trait]
819impl DuplicableModel for Announcement {
820 type DuplicateOptions<'a> = DuplicateAnnouncementOptions;
821 type DuplicateResult = Self;
822
823 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
824 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Announcement>> =
825 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
826
827 &DUPLICATE_LISTENERS
828 }
829
830 async fn duplicate_with_transaction(
831 &self,
832 state: &crate::State,
833 options: Self::DuplicateOptions<'_>,
834 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
835 ) -> Result<Self, crate::database::DatabaseError> {
836 options.validate()?;
837
838 self.run_duplicate_handlers(&options, state, transaction)
839 .await?;
840
841 let mut query_builder = InsertQueryBuilder::new("announcements");
842
843 query_builder
844 .set("type", self.r#type)
845 .set("enabled", self.enabled)
846 .set("enabled_start", self.enabled_start)
847 .set("enabled_end", self.enabled_end)
848 .set("dismissible", self.dismissible)
849 .set("dismissible_end", self.dismissible_end)
850 .set("title", &self.title)
851 .set(
852 "title_translations",
853 serde_json::to_value(&self.title_translations)?,
854 )
855 .set("content", &self.content)
856 .set(
857 "content_translations",
858 serde_json::to_value(&self.content_translations)?,
859 )
860 .set("locations", &self.locations)
861 .set("nodes", &self.nodes)
862 .set("backup_configurations", &self.backup_configurations)
863 .set("eggs", &self.eggs);
864
865 let row = query_builder
866 .returning(&Self::columns_sql(None))
867 .fetch_one(&mut **transaction)
868 .await?;
869 let mut announcement = Self::map(None, &row)?;
870
871 self.run_after_duplicate_handlers(&mut announcement, &options, state, transaction)
872 .await?;
873
874 Ok(announcement)
875 }
876}
877
878#[schema_extension_derive::extendible]
879#[init_args(Announcement, crate::State)]
880#[hook_args(crate::State)]
881#[derive(ToSchema, Serialize)]
882#[schema(title = "AdminAnnouncement")]
883pub struct AdminApiAnnouncement {
884 pub uuid: uuid::Uuid,
885
886 pub r#type: AnnouncementType,
887 pub enabled: bool,
888 pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
889 pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
890 pub dismissible: bool,
891 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
892
893 pub title: compact_str::CompactString,
894 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
895 pub content: compact_str::CompactString,
896 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
897
898 pub locations: Vec<uuid::Uuid>,
899 pub nodes: Vec<uuid::Uuid>,
900 pub backup_configurations: Vec<uuid::Uuid>,
901 pub eggs: Vec<uuid::Uuid>,
902
903 pub created: chrono::DateTime<chrono::Utc>,
904}
905
906#[schema_extension_derive::extendible]
907#[init_args(Announcement, crate::State)]
908#[hook_args(crate::State)]
909#[derive(ToSchema, Serialize)]
910#[schema(title = "ApiAnnouncement")]
911pub struct ApiAnnouncement {
912 pub uuid: uuid::Uuid,
913
914 pub r#type: AnnouncementType,
915 pub dismissible: bool,
916 pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
917
918 pub title: compact_str::CompactString,
919 pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
920 pub content: compact_str::CompactString,
921 pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
922}