Skip to main content

shared/models/
announcement.rs

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}