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