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}