Skip to main content

shared/models/
nest_egg_variable.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14pub fn validate_name_translations(
15    name_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
16    _context: &(),
17) -> Result<(), garde::Error> {
18    if name_translations.len() > 512 {
19        return Err(garde::Error::new("cannot have more than 512 entries"));
20    }
21
22    for (lang, translation) in name_translations {
23        if lang.len() < 2 || lang.len() > 15 {
24            return Err(garde::Error::new(format!(
25                "language code '{}' must be between 2 and 15 characters",
26                lang
27            )));
28        }
29        if translation.is_empty() || translation.len() > 255 {
30            return Err(garde::Error::new(format!(
31                "translation for language '{}' must be between 1 and 255 characters",
32                lang
33            )));
34        }
35    }
36
37    Ok(())
38}
39
40pub fn validate_description_translations(
41    description_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
42    _context: &(),
43) -> Result<(), garde::Error> {
44    if description_translations.len() > 512 {
45        return Err(garde::Error::new("cannot have more than 512 entries"));
46    }
47
48    for (lang, translation) in description_translations {
49        if lang.len() < 2 || lang.len() > 15 {
50            return Err(garde::Error::new(format!(
51                "language code '{}' must be between 2 and 15 characters",
52                lang
53            )));
54        }
55        if translation.is_empty() || translation.len() > 1024 {
56            return Err(garde::Error::new(format!(
57                "translation for language '{}' must be between 1 and 1024 characters",
58                lang
59            )));
60        }
61    }
62
63    Ok(())
64}
65
66#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
67pub struct ExportedNestEggVariable {
68    #[garde(length(chars, min = 1, max = 255))]
69    #[schema(min_length = 1, max_length = 255)]
70    pub name: compact_str::CompactString,
71    #[garde(custom(validate_name_translations))]
72    #[serde(default)]
73    pub name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
74    #[garde(length(max = 1024))]
75    #[schema(max_length = 1024)]
76    pub description: Option<compact_str::CompactString>,
77    #[garde(custom(validate_description_translations))]
78    #[serde(default)]
79    pub description_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
80    #[garde(skip)]
81    #[serde(
82        default,
83        alias = "sort",
84        deserialize_with = "crate::deserialize::deserialize_defaultable"
85    )]
86    pub order: i16,
87
88    #[garde(length(chars, min = 1, max = 255))]
89    #[schema(min_length = 1, max_length = 255)]
90    pub env_variable: compact_str::CompactString,
91    #[garde(length(max = 1024))]
92    #[schema(max_length = 1024)]
93    #[serde(
94        default,
95        deserialize_with = "crate::deserialize::deserialize_stringable_option"
96    )]
97    pub default_value: Option<String>,
98
99    #[garde(skip)]
100    pub user_viewable: bool,
101    #[garde(skip)]
102    pub user_editable: bool,
103    #[garde(skip)]
104    #[serde(default)]
105    pub secret: bool,
106    #[garde(skip)]
107    #[serde(
108        default,
109        deserialize_with = "crate::deserialize::deserialize_nest_egg_variable_rules"
110    )]
111    pub rules: Vec<compact_str::CompactString>,
112}
113
114#[derive(Serialize, Deserialize)]
115pub struct NestEggVariable {
116    pub uuid: uuid::Uuid,
117
118    pub name: compact_str::CompactString,
119    pub name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
120    pub description: Option<compact_str::CompactString>,
121    pub description_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
122    pub order: i16,
123
124    pub env_variable: compact_str::CompactString,
125    pub default_value: Option<String>,
126    pub user_viewable: bool,
127    pub user_editable: bool,
128    pub secret: bool,
129    pub rules: Vec<compact_str::CompactString>,
130
131    pub created: chrono::NaiveDateTime,
132
133    extension_data: super::ModelExtensionData,
134}
135
136impl BaseModel for NestEggVariable {
137    const NAME: &'static str = "nest_egg_variable";
138
139    fn get_extension_list() -> &'static super::ModelExtensionList {
140        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
141            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
142
143        &EXTENSIONS
144    }
145
146    fn get_extension_data(&self) -> &super::ModelExtensionData {
147        &self.extension_data
148    }
149
150    #[inline]
151    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
152        let prefix = prefix.unwrap_or_default();
153
154        BTreeMap::from([
155            (
156                "nest_egg_variables.uuid",
157                compact_str::format_compact!("{prefix}uuid"),
158            ),
159            (
160                "nest_egg_variables.name",
161                compact_str::format_compact!("{prefix}name"),
162            ),
163            (
164                "nest_egg_variables.name_translations",
165                compact_str::format_compact!("{prefix}name_translations"),
166            ),
167            (
168                "nest_egg_variables.description",
169                compact_str::format_compact!("{prefix}description"),
170            ),
171            (
172                "nest_egg_variables.description_translations",
173                compact_str::format_compact!("{prefix}description_translations"),
174            ),
175            (
176                "nest_egg_variables.order_",
177                compact_str::format_compact!("{prefix}order"),
178            ),
179            (
180                "nest_egg_variables.env_variable",
181                compact_str::format_compact!("{prefix}env_variable"),
182            ),
183            (
184                "nest_egg_variables.default_value",
185                compact_str::format_compact!("{prefix}default_value"),
186            ),
187            (
188                "nest_egg_variables.user_viewable",
189                compact_str::format_compact!("{prefix}user_viewable"),
190            ),
191            (
192                "nest_egg_variables.user_editable",
193                compact_str::format_compact!("{prefix}user_editable"),
194            ),
195            (
196                "nest_egg_variables.secret",
197                compact_str::format_compact!("{prefix}secret"),
198            ),
199            (
200                "nest_egg_variables.rules",
201                compact_str::format_compact!("{prefix}rules"),
202            ),
203            (
204                "nest_egg_variables.created",
205                compact_str::format_compact!("{prefix}created"),
206            ),
207        ])
208    }
209
210    #[inline]
211    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
212        let prefix = prefix.unwrap_or_default();
213
214        Ok(Self {
215            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
216            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
217            name_translations: serde_json::from_value(
218                row.try_get(compact_str::format_compact!("{prefix}name_translations").as_str())?,
219            )?,
220            description: row
221                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
222            description_translations: serde_json::from_value(row.try_get(
223                compact_str::format_compact!("{prefix}description_translations").as_str(),
224            )?)?,
225            order: row.try_get(compact_str::format_compact!("{prefix}order").as_str())?,
226            env_variable: row
227                .try_get(compact_str::format_compact!("{prefix}env_variable").as_str())?,
228            default_value: row
229                .try_get(compact_str::format_compact!("{prefix}default_value").as_str())?,
230            user_viewable: row
231                .try_get(compact_str::format_compact!("{prefix}user_viewable").as_str())?,
232            user_editable: row
233                .try_get(compact_str::format_compact!("{prefix}user_editable").as_str())?,
234            secret: row.try_get(compact_str::format_compact!("{prefix}secret").as_str())?,
235            rules: row.try_get(compact_str::format_compact!("{prefix}rules").as_str())?,
236            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
237            extension_data: Self::map_extensions(prefix, row)?,
238        })
239    }
240}
241
242impl NestEggVariable {
243    pub async fn by_egg_uuid_uuid(
244        database: &crate::database::Database,
245        egg_uuid: uuid::Uuid,
246        uuid: uuid::Uuid,
247    ) -> Result<Option<Self>, crate::database::DatabaseError> {
248        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
249            r#"
250            SELECT {}
251            FROM nest_egg_variables
252            WHERE nest_egg_variables.egg_uuid = $1 AND nest_egg_variables.uuid = $2
253            "#,
254            Self::columns_sql(None)
255        )))
256        .bind(egg_uuid)
257        .bind(uuid)
258        .fetch_optional(database.read())
259        .await?;
260
261        row.try_map(|row| Self::map(None, &row))
262    }
263
264    pub async fn all_by_egg_uuid(
265        database: &crate::database::Database,
266        egg_uuid: uuid::Uuid,
267    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
268        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
269            r#"
270            SELECT {}
271            FROM nest_egg_variables
272            WHERE nest_egg_variables.egg_uuid = $1
273            ORDER BY nest_egg_variables.order_, nest_egg_variables.created
274            "#,
275            Self::columns_sql(None)
276        )))
277        .bind(egg_uuid)
278        .fetch_all(database.read())
279        .await?;
280
281        rows.into_iter()
282            .map(|row| Self::map(None, &row))
283            .try_collect_vec()
284    }
285
286    #[inline]
287    pub fn into_exported(self) -> ExportedNestEggVariable {
288        ExportedNestEggVariable {
289            name: self.name,
290            name_translations: self.name_translations,
291            description: self.description,
292            description_translations: self.description_translations,
293            order: self.order,
294            env_variable: self.env_variable,
295            default_value: self.default_value,
296            user_viewable: self.user_viewable,
297            user_editable: self.user_editable,
298            secret: self.secret,
299            rules: self.rules,
300        }
301    }
302}
303
304#[async_trait::async_trait]
305impl IntoAdminApiObject for NestEggVariable {
306    type AdminApiObject = AdminApiNestEggVariable;
307    type ExtraArgs<'a> = ();
308
309    async fn into_admin_api_object<'a>(
310        self,
311        state: &crate::State,
312        _args: Self::ExtraArgs<'a>,
313    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
314        let api_object = AdminApiNestEggVariable::init_hooks(&self, state).await?;
315
316        let api_object = finish_extendible!(
317            AdminApiNestEggVariable {
318                uuid: self.uuid,
319                name: self.name,
320                name_translations: self.name_translations,
321                description: self.description,
322                description_translations: self.description_translations,
323                order: self.order,
324                env_variable: self.env_variable,
325                default_value: self.default_value,
326                user_viewable: self.user_viewable,
327                user_editable: self.user_editable,
328                is_secret: self.secret,
329                rules: self.rules,
330                created: self.created.and_utc(),
331            },
332            api_object,
333            state
334        )?;
335
336        Ok(api_object)
337    }
338}
339
340#[derive(ToSchema, Deserialize, Validate)]
341pub struct CreateNestEggVariableOptions {
342    #[garde(skip)]
343    pub egg_uuid: uuid::Uuid,
344
345    #[garde(length(chars, min = 3, max = 255))]
346    #[schema(min_length = 3, max_length = 255)]
347    pub name: compact_str::CompactString,
348    #[garde(custom(validate_name_translations))]
349    pub name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
350    #[garde(length(chars, min = 1, max = 1024))]
351    #[schema(min_length = 1, max_length = 1024)]
352    pub description: Option<compact_str::CompactString>,
353    #[garde(custom(validate_description_translations))]
354    pub description_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
355
356    #[garde(skip)]
357    pub order: i16,
358
359    #[garde(length(chars, min = 1, max = 255))]
360    #[schema(min_length = 1, max_length = 255)]
361    pub env_variable: compact_str::CompactString,
362
363    #[garde(length(max = 1024))]
364    #[schema(max_length = 1024)]
365    pub default_value: Option<String>,
366
367    #[garde(skip)]
368    pub user_viewable: bool,
369    #[garde(skip)]
370    pub user_editable: bool,
371    #[garde(skip)]
372    pub secret: bool,
373
374    #[garde(custom(rule_validator::validate_rules))]
375    pub rules: Vec<compact_str::CompactString>,
376}
377
378#[async_trait::async_trait]
379impl CreatableModel for NestEggVariable {
380    type CreateOptions<'a> = CreateNestEggVariableOptions;
381    type CreateResult = Self;
382
383    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
384        static CREATE_LISTENERS: LazyLock<CreateListenerList<NestEggVariable>> =
385            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
386
387        &CREATE_LISTENERS
388    }
389
390    async fn create_with_transaction(
391        state: &crate::State,
392        mut options: Self::CreateOptions<'_>,
393        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
394    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
395        options.validate()?;
396
397        let mut query_builder = InsertQueryBuilder::new("nest_egg_variables");
398
399        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
400
401        query_builder
402            .set("egg_uuid", options.egg_uuid)
403            .set("name", &options.name)
404            .set(
405                "name_translations",
406                serde_json::to_value(&options.name_translations)?,
407            )
408            .set("description", &options.description)
409            .set(
410                "description_translations",
411                serde_json::to_value(&options.description_translations)?,
412            )
413            .set("order_", options.order)
414            .set("env_variable", &options.env_variable)
415            .set("default_value", &options.default_value)
416            .set("user_viewable", options.user_viewable)
417            .set("user_editable", options.user_editable)
418            .set("secret", options.secret)
419            .set("rules", &options.rules);
420
421        let row = query_builder
422            .returning(&Self::columns_sql(None))
423            .fetch_one(&mut **transaction)
424            .await?;
425        let mut nest_egg_variable = Self::map(None, &row)?;
426
427        Self::run_after_create_handlers(&mut nest_egg_variable, &options, state, transaction)
428            .await?;
429
430        Ok(nest_egg_variable)
431    }
432}
433
434#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
435pub struct UpdateNestEggVariableOptions {
436    #[garde(length(chars, min = 3, max = 255))]
437    #[schema(min_length = 3, max_length = 255)]
438    pub name: Option<compact_str::CompactString>,
439    #[garde(inner(custom(validate_name_translations)))]
440    pub name_translations: Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
441    #[garde(length(chars, min = 1, max = 1024))]
442    #[schema(min_length = 1, max_length = 1024)]
443    #[serde(
444        default,
445        skip_serializing_if = "Option::is_none",
446        with = "::serde_with::rust::double_option"
447    )]
448    pub description: Option<Option<compact_str::CompactString>>,
449    #[garde(inner(custom(validate_description_translations)))]
450    pub description_translations:
451        Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
452
453    #[garde(skip)]
454    pub order: Option<i16>,
455
456    #[garde(length(chars, min = 1, max = 255))]
457    #[schema(min_length = 1, max_length = 255)]
458    pub env_variable: Option<compact_str::CompactString>,
459
460    #[garde(length(max = 1024))]
461    #[schema(max_length = 1024)]
462    #[serde(
463        default,
464        skip_serializing_if = "Option::is_none",
465        with = "::serde_with::rust::double_option"
466    )]
467    pub default_value: Option<Option<String>>,
468
469    #[garde(skip)]
470    pub user_viewable: Option<bool>,
471    #[garde(skip)]
472    pub user_editable: Option<bool>,
473    #[garde(skip)]
474    pub secret: Option<bool>,
475
476    #[garde(inner(custom(rule_validator::validate_rules)))]
477    pub rules: Option<Vec<compact_str::CompactString>>,
478}
479
480#[async_trait::async_trait]
481impl UpdatableModel for NestEggVariable {
482    type UpdateOptions = UpdateNestEggVariableOptions;
483
484    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
485        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<NestEggVariable>> =
486            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
487
488        &UPDATE_LISTENERS
489    }
490
491    async fn update_with_transaction(
492        &mut self,
493        state: &crate::State,
494        mut options: Self::UpdateOptions,
495        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
496    ) -> Result<(), crate::database::DatabaseError> {
497        options.validate()?;
498
499        let mut query_builder = UpdateQueryBuilder::new("nest_egg_variables");
500
501        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
502            .await?;
503
504        query_builder
505            .set("name", options.name.as_ref())
506            .set(
507                "name_translations",
508                options
509                    .name_translations
510                    .as_ref()
511                    .map(serde_json::to_value)
512                    .transpose()?,
513            )
514            .set(
515                "description",
516                options.description.as_ref().map(|d| d.as_ref()),
517            )
518            .set(
519                "description_translations",
520                options
521                    .description_translations
522                    .as_ref()
523                    .map(serde_json::to_value)
524                    .transpose()?,
525            )
526            .set("order_", options.order)
527            .set("env_variable", options.env_variable.as_ref())
528            .set(
529                "default_value",
530                options.default_value.as_ref().map(|d| d.as_ref()),
531            )
532            .set("user_viewable", options.user_viewable)
533            .set("user_editable", options.user_editable)
534            .set("secret", options.secret)
535            .set("rules", options.rules.as_ref())
536            .where_eq("uuid", self.uuid);
537
538        query_builder.execute(&mut **transaction).await?;
539
540        if let Some(name) = options.name {
541            self.name = name;
542        }
543        if let Some(name_translations) = options.name_translations {
544            self.name_translations = name_translations;
545        }
546        if let Some(description) = options.description {
547            self.description = description;
548        }
549        if let Some(description_translations) = options.description_translations {
550            self.description_translations = description_translations;
551        }
552        if let Some(order) = options.order {
553            self.order = order;
554        }
555        if let Some(env_variable) = options.env_variable {
556            self.env_variable = env_variable;
557        }
558        if let Some(default_value) = options.default_value {
559            self.default_value = default_value;
560        }
561        if let Some(user_viewable) = options.user_viewable {
562            self.user_viewable = user_viewable;
563        }
564        if let Some(user_editable) = options.user_editable {
565            self.user_editable = user_editable;
566        }
567        if let Some(secret) = options.secret {
568            self.secret = secret;
569        }
570        if let Some(rules) = options.rules {
571            self.rules = rules;
572        }
573
574        self.run_after_update_handlers(state, transaction).await?;
575
576        Ok(())
577    }
578}
579
580#[async_trait::async_trait]
581impl DeletableModel for NestEggVariable {
582    type DeleteOptions = ();
583
584    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
585        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<NestEggVariable>> =
586            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
587
588        &DELETE_LISTENERS
589    }
590
591    async fn delete_with_transaction(
592        &self,
593        state: &crate::State,
594        options: Self::DeleteOptions,
595        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
596    ) -> Result<(), anyhow::Error> {
597        self.run_delete_handlers(&options, state, transaction)
598            .await?;
599
600        sqlx::query(
601            r#"
602            DELETE FROM nest_egg_variables
603            WHERE nest_egg_variables.uuid = $1
604            "#,
605        )
606        .bind(self.uuid)
607        .execute(&mut **transaction)
608        .await?;
609
610        self.run_after_delete_handlers(&options, state, transaction)
611            .await?;
612
613        Ok(())
614    }
615}
616
617#[derive(Validate)]
618pub struct DuplicateNestEggVariableOptions {
619    #[garde(skip)]
620    pub egg_uuid: uuid::Uuid,
621    #[garde(length(chars, min = 1, max = 255))]
622    pub name: compact_str::CompactString,
623    #[garde(length(chars, min = 1, max = 255))]
624    pub env_variable: compact_str::CompactString,
625}
626
627#[async_trait::async_trait]
628impl DuplicableModel for NestEggVariable {
629    type DuplicateOptions<'a> = DuplicateNestEggVariableOptions;
630
631    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
632        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<NestEggVariable>> =
633            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
634
635        &DUPLICATE_LISTENERS
636    }
637
638    async fn duplicate_with_transaction(
639        &self,
640        state: &crate::State,
641        options: Self::DuplicateOptions<'_>,
642        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
643    ) -> Result<Self, crate::database::DatabaseError> {
644        options.validate()?;
645
646        self.run_duplicate_handlers(&options, state, transaction)
647            .await?;
648
649        let mut query_builder = InsertQueryBuilder::new("nest_egg_variables");
650
651        query_builder
652            .set("egg_uuid", options.egg_uuid)
653            .set("name", &options.name)
654            .set(
655                "name_translations",
656                serde_json::to_value(&self.name_translations)?,
657            )
658            .set("description", &self.description)
659            .set(
660                "description_translations",
661                serde_json::to_value(&self.description_translations)?,
662            )
663            .set("order_", self.order)
664            .set("env_variable", &options.env_variable)
665            .set("default_value", &self.default_value)
666            .set("user_viewable", self.user_viewable)
667            .set("user_editable", self.user_editable)
668            .set("secret", self.secret)
669            .set("rules", &self.rules);
670
671        let row = query_builder
672            .returning(&Self::columns_sql(None))
673            .fetch_one(&mut **transaction)
674            .await?;
675        let mut nest_egg_variable = Self::map(None, &row)?;
676
677        self.run_after_duplicate_handlers(&mut nest_egg_variable, &options, state, transaction)
678            .await?;
679
680        Ok(nest_egg_variable)
681    }
682}
683
684#[schema_extension_derive::extendible]
685#[init_args(NestEggVariable, crate::State)]
686#[hook_args(crate::State)]
687#[derive(ToSchema, Serialize)]
688#[schema(title = "NestEggVariable")]
689pub struct AdminApiNestEggVariable {
690    pub uuid: uuid::Uuid,
691
692    pub name: compact_str::CompactString,
693    pub name_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
694    pub description: Option<compact_str::CompactString>,
695    pub description_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
696    pub order: i16,
697
698    pub env_variable: compact_str::CompactString,
699    pub default_value: Option<String>,
700    pub user_viewable: bool,
701    pub user_editable: bool,
702    pub is_secret: bool,
703    pub rules: Vec<compact_str::CompactString>,
704
705    pub created: chrono::DateTime<chrono::Utc>,
706}