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}