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