Skip to main content

shared/models/
nest_egg.rs

1use crate::{
2    models::{
3        InsertQueryBuilder, UpdateQueryBuilder, nest_egg_variable::CreateNestEggVariableOptions,
4    },
5    prelude::*,
6};
7use garde::Validate;
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10use sqlx::{Row, postgres::PgRow};
11use std::{
12    collections::{BTreeMap, HashSet},
13    sync::{Arc, LazyLock},
14};
15use utoipa::ToSchema;
16
17pub fn validate_startup_commands(
18    startup_commands: &IndexMap<compact_str::CompactString, compact_str::CompactString>,
19    _context: &(),
20) -> Result<(), garde::Error> {
21    if startup_commands.is_empty() {
22        return Err(garde::Error::new(compact_str::format_compact!(
23            "at least one startup command is required"
24        )));
25    }
26
27    let mut seen_commands = HashSet::new();
28    for command in startup_commands.values() {
29        if !seen_commands.insert(command) {
30            return Err(garde::Error::new(compact_str::format_compact!(
31                "duplicate startup command: {}",
32                command
33            )));
34        }
35    }
36
37    Ok(())
38}
39
40pub fn validate_docker_images(
41    docker_images: &IndexMap<compact_str::CompactString, compact_str::CompactString>,
42    _context: &(),
43) -> Result<(), garde::Error> {
44    let mut seen_images = HashSet::new();
45    for image in docker_images.values() {
46        if !seen_images.insert(image) {
47            return Err(garde::Error::new(compact_str::format_compact!(
48                "duplicate docker image: {}",
49                image
50            )));
51        }
52    }
53
54    Ok(())
55}
56
57fn true_fn() -> bool {
58    true
59}
60
61#[derive(ToSchema, Serialize, Deserialize, Clone, Copy)]
62#[serde(rename_all = "snake_case")]
63pub enum ServerConfigurationFileParser {
64    File,
65    Yaml,
66    Properties,
67    Ini,
68    Json,
69    Xml,
70    Toml,
71}
72
73#[derive(ToSchema, Serialize, Deserialize, Clone)]
74pub struct ProcessConfigurationFileReplacement {
75    pub r#match: compact_str::CompactString,
76    #[serde(default)]
77    pub insert_new: bool,
78    #[serde(default = "true_fn")]
79    pub update_existing: bool,
80    pub if_value: Option<compact_str::CompactString>,
81    pub replace_with: serde_json::Value,
82}
83
84#[derive(ToSchema, Serialize, Deserialize, Clone)]
85pub struct ProcessConfigurationFile {
86    pub file: compact_str::CompactString,
87    #[serde(default = "true_fn")]
88    pub create_new: bool,
89    #[schema(inline)]
90    pub parser: ServerConfigurationFileParser,
91    #[schema(inline)]
92    pub replace: Vec<ProcessConfigurationFileReplacement>,
93}
94
95#[derive(ToSchema, Serialize, Clone)]
96pub struct ProcessConfiguration {
97    #[schema(inline)]
98    pub startup: crate::models::nest_egg::NestEggConfigStartup,
99    #[schema(inline)]
100    pub stop: crate::models::nest_egg::NestEggConfigStop,
101    #[schema(inline)]
102    pub configs: Vec<ProcessConfigurationFile>,
103}
104
105#[derive(ToSchema, Serialize, Deserialize, Clone, Default)]
106pub struct NestEggConfigStartup {
107    #[serde(
108        default,
109        deserialize_with = "crate::deserialize::deserialize_array_or_not"
110    )]
111    pub done: Vec<compact_str::CompactString>,
112    #[serde(default)]
113    pub strip_ansi: bool,
114}
115
116#[derive(ToSchema, Serialize, Deserialize, Clone, Default)]
117pub struct NestEggConfigStop {
118    pub r#type: compact_str::CompactString,
119    pub value: Option<compact_str::CompactString>,
120}
121
122#[derive(ToSchema, Serialize, Deserialize, Clone)]
123pub struct NestEggConfigScript {
124    pub container: compact_str::CompactString,
125    pub entrypoint: compact_str::CompactString,
126    #[serde(alias = "script")]
127    pub content: String,
128}
129
130#[derive(ToSchema, Serialize, Deserialize, Clone)]
131pub struct ExportedNestEggConfigsFilesFile {
132    #[serde(default = "true_fn")]
133    pub create_new: bool,
134    #[schema(inline)]
135    pub parser: ServerConfigurationFileParser,
136    #[schema(inline)]
137    pub replace: Vec<ProcessConfigurationFileReplacement>,
138}
139
140#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
141pub struct ExportedNestEggConfigs {
142    #[garde(skip)]
143    #[schema(inline)]
144    #[serde(
145        default,
146        deserialize_with = "crate::deserialize::deserialize_nest_egg_config_files"
147    )]
148    pub files: IndexMap<compact_str::CompactString, ExportedNestEggConfigsFilesFile>,
149    #[garde(skip)]
150    #[schema(inline)]
151    #[serde(
152        default,
153        deserialize_with = "crate::deserialize::deserialize_pre_stringified"
154    )]
155    pub startup: NestEggConfigStartup,
156    #[garde(skip)]
157    #[schema(inline)]
158    #[serde(
159        default,
160        deserialize_with = "crate::deserialize::deserialize_nest_egg_config_stop"
161    )]
162    pub stop: NestEggConfigStop,
163}
164
165#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
166pub struct ExportedNestEggScripts {
167    #[garde(skip)]
168    #[schema(inline)]
169    pub installation: NestEggConfigScript,
170}
171
172#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
173pub struct ExportedNestEgg {
174    #[garde(skip)]
175    #[serde(default = "uuid::Uuid::new_v4")]
176    pub uuid: uuid::Uuid,
177    #[garde(length(chars, min = 1, max = 255))]
178    #[schema(min_length = 1, max_length = 255)]
179    pub name: compact_str::CompactString,
180    #[garde(length(max = 1024))]
181    #[schema(max_length = 1024)]
182    #[serde(deserialize_with = "crate::deserialize::deserialize_string_option")]
183    pub description: Option<compact_str::CompactString>,
184    #[garde(length(chars, min = 2, max = 255))]
185    #[schema(min_length = 2, max_length = 255)]
186    pub author: compact_str::CompactString,
187
188    #[garde(skip)]
189    #[schema(inline)]
190    pub config: ExportedNestEggConfigs,
191    #[garde(skip)]
192    #[schema(inline)]
193    pub scripts: ExportedNestEggScripts,
194
195    #[garde(custom(validate_startup_commands))]
196    #[serde(
197        deserialize_with = "crate::deserialize::deserialize_map_or_not",
198        alias = "startup"
199    )]
200    pub startup_commands: IndexMap<compact_str::CompactString, compact_str::CompactString>,
201    #[garde(skip)]
202    #[serde(default)]
203    pub force_outgoing_ip: bool,
204    #[garde(skip)]
205    #[serde(default)]
206    pub separate_port: bool,
207
208    #[garde(skip)]
209    #[serde(
210        default,
211        deserialize_with = "crate::deserialize::deserialize_defaultable"
212    )]
213    pub features: Vec<compact_str::CompactString>,
214    #[garde(custom(validate_docker_images))]
215    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
216    #[garde(skip)]
217    #[serde(
218        default,
219        deserialize_with = "crate::deserialize::deserialize_defaultable"
220    )]
221    pub file_denylist: Vec<compact_str::CompactString>,
222
223    #[garde(skip)]
224    #[schema(inline)]
225    pub variables: Vec<super::nest_egg_variable::ExportedNestEggVariable>,
226}
227
228#[derive(Serialize, Deserialize, Clone)]
229pub struct NestEgg {
230    pub uuid: uuid::Uuid,
231    pub nest: Fetchable<super::nest::Nest>,
232    pub egg_repository_egg: Option<Fetchable<super::egg_repository_egg::EggRepositoryEgg>>,
233
234    pub name: compact_str::CompactString,
235    pub description: Option<compact_str::CompactString>,
236    pub author: compact_str::CompactString,
237
238    pub config_files: Vec<ProcessConfigurationFile>,
239    pub config_startup: NestEggConfigStartup,
240    pub config_stop: NestEggConfigStop,
241    pub config_script: NestEggConfigScript,
242
243    pub startup_commands: IndexMap<compact_str::CompactString, compact_str::CompactString>,
244    pub force_outgoing_ip: bool,
245    pub separate_port: bool,
246
247    pub features: Vec<compact_str::CompactString>,
248    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
249    pub file_denylist: Vec<compact_str::CompactString>,
250
251    pub created: chrono::NaiveDateTime,
252
253    extension_data: super::ModelExtensionData,
254}
255
256impl BaseModel for NestEgg {
257    const NAME: &'static str = "nest_egg";
258
259    fn get_extension_list() -> &'static super::ModelExtensionList {
260        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
261            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
262
263        &EXTENSIONS
264    }
265
266    fn get_extension_data(&self) -> &super::ModelExtensionData {
267        &self.extension_data
268    }
269
270    #[inline]
271    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
272        let prefix = prefix.unwrap_or_default();
273
274        BTreeMap::from([
275            (
276                "nest_eggs.uuid",
277                compact_str::format_compact!("{prefix}uuid"),
278            ),
279            (
280                "nest_eggs.nest_uuid",
281                compact_str::format_compact!("{prefix}nest_uuid"),
282            ),
283            (
284                "nest_eggs.egg_repository_egg_uuid",
285                compact_str::format_compact!("{prefix}egg_repository_egg_uuid"),
286            ),
287            (
288                "nest_eggs.name",
289                compact_str::format_compact!("{prefix}name"),
290            ),
291            (
292                "nest_eggs.description",
293                compact_str::format_compact!("{prefix}description"),
294            ),
295            (
296                "nest_eggs.author",
297                compact_str::format_compact!("{prefix}author"),
298            ),
299            (
300                "nest_eggs.config_files",
301                compact_str::format_compact!("{prefix}config_files"),
302            ),
303            (
304                "nest_eggs.config_startup",
305                compact_str::format_compact!("{prefix}config_startup"),
306            ),
307            (
308                "nest_eggs.config_stop",
309                compact_str::format_compact!("{prefix}config_stop"),
310            ),
311            (
312                "nest_eggs.config_script",
313                compact_str::format_compact!("{prefix}config_script"),
314            ),
315            (
316                "nest_eggs.startup_commands",
317                compact_str::format_compact!("{prefix}startup_commands"),
318            ),
319            (
320                "nest_eggs.force_outgoing_ip",
321                compact_str::format_compact!("{prefix}force_outgoing_ip"),
322            ),
323            (
324                "nest_eggs.separate_port",
325                compact_str::format_compact!("{prefix}separate_port"),
326            ),
327            (
328                "nest_eggs.features",
329                compact_str::format_compact!("{prefix}features"),
330            ),
331            (
332                "nest_eggs.docker_images",
333                compact_str::format_compact!("{prefix}docker_images"),
334            ),
335            (
336                "nest_eggs.file_denylist",
337                compact_str::format_compact!("{prefix}file_denylist"),
338            ),
339            (
340                "nest_eggs.created",
341                compact_str::format_compact!("{prefix}created"),
342            ),
343        ])
344    }
345
346    #[inline]
347    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
348        let prefix = prefix.unwrap_or_default();
349
350        Ok(Self {
351            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
352            nest: super::nest::Nest::get_fetchable(
353                row.try_get(compact_str::format_compact!("{prefix}nest_uuid").as_str())?,
354            ),
355            egg_repository_egg: row
356                .try_get::<Option<uuid::Uuid>, _>(
357                    compact_str::format_compact!("{prefix}egg_repository_egg_uuid").as_str(),
358                )?
359                .map(super::egg_repository_egg::EggRepositoryEgg::get_fetchable),
360            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
361            description: row
362                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
363            author: row.try_get(compact_str::format_compact!("{prefix}author").as_str())?,
364            config_files: serde_json::from_value(
365                row.try_get(compact_str::format_compact!("{prefix}config_files").as_str())?,
366            )?,
367            config_startup: serde_json::from_value(
368                row.try_get(compact_str::format_compact!("{prefix}config_startup").as_str())?,
369            )?,
370            config_stop: serde_json::from_value(
371                row.try_get(compact_str::format_compact!("{prefix}config_stop").as_str())?,
372            )?,
373            config_script: serde_json::from_value(
374                row.try_get(compact_str::format_compact!("{prefix}config_script").as_str())?,
375            )?,
376            startup_commands: serde_json::from_value(
377                row.try_get(compact_str::format_compact!("{prefix}startup_commands").as_str())?,
378            )?,
379            force_outgoing_ip: row
380                .try_get(compact_str::format_compact!("{prefix}force_outgoing_ip").as_str())?,
381            separate_port: row
382                .try_get(compact_str::format_compact!("{prefix}separate_port").as_str())?,
383            features: row.try_get(compact_str::format_compact!("{prefix}features").as_str())?,
384            docker_images: serde_json::from_value(
385                row.try_get(compact_str::format_compact!("{prefix}docker_images").as_str())?,
386            )?,
387            file_denylist: row
388                .try_get(compact_str::format_compact!("{prefix}file_denylist").as_str())?,
389            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
390            extension_data: Self::map_extensions(prefix, row)?,
391        })
392    }
393}
394
395impl NestEgg {
396    pub async fn import(
397        state: &crate::State,
398        nest_uuid: uuid::Uuid,
399        egg_repository_egg_uuid: Option<uuid::Uuid>,
400        exported_egg: ExportedNestEgg,
401    ) -> Result<Self, crate::database::DatabaseError> {
402        let egg = Self::create(
403            state,
404            CreateNestEggOptions {
405                nest_uuid,
406                egg_repository_egg_uuid,
407                author: exported_egg.author,
408                name: exported_egg.name,
409                description: exported_egg.description,
410                config_files: exported_egg
411                    .config
412                    .files
413                    .into_iter()
414                    .map(|(file, config)| ProcessConfigurationFile {
415                        file,
416                        create_new: config.create_new,
417                        parser: config.parser,
418                        replace: config.replace,
419                    })
420                    .collect(),
421                config_startup: exported_egg.config.startup,
422                config_stop: exported_egg.config.stop,
423                config_script: exported_egg.scripts.installation,
424                startup_commands: exported_egg.startup_commands,
425                force_outgoing_ip: exported_egg.force_outgoing_ip,
426                separate_port: exported_egg.separate_port,
427                features: exported_egg.features,
428                docker_images: exported_egg.docker_images,
429                file_denylist: exported_egg.file_denylist,
430            },
431        )
432        .await?;
433
434        for mut variable in exported_egg.variables {
435            if rule_validator::validate_rules(&variable.rules, &()).is_err() {
436                continue;
437            }
438
439            if variable.description.as_ref().is_some_and(|d| d.is_empty()) {
440                variable.description = None;
441            }
442
443            if let Err(err) = super::nest_egg_variable::NestEggVariable::create(
444                state,
445                CreateNestEggVariableOptions {
446                    egg_uuid: egg.uuid,
447                    name: variable.name,
448                    name_translations: variable.name_translations,
449                    description: variable.description,
450                    description_translations: variable.description_translations,
451                    order: variable.order,
452                    env_variable: variable.env_variable,
453                    default_value: variable.default_value,
454                    user_viewable: variable.user_viewable,
455                    user_editable: variable.user_editable,
456                    secret: variable.secret,
457                    rules: variable.rules,
458                },
459            )
460            .await
461            {
462                tracing::warn!("error while importing nest egg variable: {:?}", err);
463            }
464        }
465
466        Ok(egg)
467    }
468
469    pub async fn import_update(
470        &self,
471        database: &crate::database::Database,
472        mut exported_egg: ExportedNestEgg,
473    ) -> Result<(), crate::database::DatabaseError> {
474        sqlx::query!(
475            "UPDATE nest_eggs
476            SET
477                author = $2, name = $3, description = $4,
478                config_files = $5, config_startup = $6, config_stop = $7,
479                config_script = $8, startup_commands = $9::json,
480                force_outgoing_ip = $10, separate_port = $11, features = $12,
481                docker_images = $13::json, file_denylist = $14
482            WHERE nest_eggs.uuid = $1",
483            self.uuid,
484            &exported_egg.author,
485            &exported_egg.name,
486            exported_egg.description.as_deref(),
487            serde_json::to_value(
488                &exported_egg
489                    .config
490                    .files
491                    .into_iter()
492                    .map(|(file, config)| ProcessConfigurationFile {
493                        file,
494                        create_new: config.create_new,
495                        parser: config.parser,
496                        replace: config.replace,
497                    })
498                    .collect::<Vec<_>>(),
499            )?,
500            serde_json::to_value(&exported_egg.config.startup)?,
501            serde_json::to_value(&exported_egg.config.stop)?,
502            serde_json::to_value(&exported_egg.scripts.installation)?,
503            serde_json::to_string(&exported_egg.startup_commands)? as String,
504            exported_egg.force_outgoing_ip,
505            exported_egg.separate_port,
506            &exported_egg
507                .features
508                .into_iter()
509                .map(|f| f.into())
510                .collect::<Vec<_>>(),
511            serde_json::to_string(&exported_egg.docker_images)? as String,
512            &exported_egg
513                .file_denylist
514                .into_iter()
515                .map(|f| f.into())
516                .collect::<Vec<_>>(),
517        )
518        .execute(database.write())
519        .await?;
520
521        let unused_variables = sqlx::query!(
522            "SELECT nest_egg_variables.uuid
523            FROM nest_egg_variables
524            WHERE nest_egg_variables.egg_uuid = $1 AND nest_egg_variables.env_variable != ALL($2)",
525            self.uuid,
526            &exported_egg
527                .variables
528                .iter()
529                .map(|v| v.env_variable.as_str())
530                .collect::<Vec<_>>() as &[&str]
531        )
532        .fetch_all(database.read())
533        .await?;
534
535        for (i, variable) in exported_egg.variables.iter_mut().enumerate() {
536            if rule_validator::validate_rules(&variable.rules, &()).is_err() {
537                continue;
538            }
539
540            if variable.description.as_ref().is_some_and(|d| d.is_empty()) {
541                variable.description = None;
542            }
543
544            if let Err(err) = sqlx::query!(
545                "INSERT INTO nest_egg_variables (
546                    egg_uuid, name, name_translations, description, description_translations, order_, env_variable,
547                    default_value, user_viewable, user_editable, rules
548                )
549                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
550                ON CONFLICT (egg_uuid, env_variable) DO UPDATE SET
551                    name = EXCLUDED.name,
552                    name_translations = EXCLUDED.name_translations,
553                    description = EXCLUDED.description,
554                    description_translations = EXCLUDED.description_translations,
555                    order_ = EXCLUDED.order_,
556                    default_value = EXCLUDED.default_value,
557                    user_viewable = EXCLUDED.user_viewable,
558                    user_editable = EXCLUDED.user_editable,
559                    rules = EXCLUDED.rules",
560                self.uuid,
561                &variable.name,
562                serde_json::to_value(&variable.name_translations)?,
563                variable.description.as_deref(),
564                serde_json::to_value(&variable.description_translations)?,
565                if variable.order == 0 {
566                    i as i16 + 1
567                } else {
568                    variable.order
569                },
570                &variable.env_variable,
571                variable.default_value.as_deref(),
572                variable.user_viewable,
573                variable.user_editable,
574                &variable
575                    .rules
576                    .iter()
577                    .map(|r| r.as_str())
578                    .collect::<Vec<_>>() as &[&str]
579            )
580            .execute(database.read())
581            .await
582            {
583                tracing::warn!("error while importing nest egg variable: {:?}", err);
584            }
585        }
586
587        let order_base = exported_egg.variables.len() as i16
588            + exported_egg
589                .variables
590                .iter()
591                .map(|v| v.order)
592                .max()
593                .unwrap_or_default();
594
595        sqlx::query!(
596            "UPDATE nest_egg_variables
597            SET order_ = $1 + array_position($2, nest_egg_variables.uuid)
598            WHERE nest_egg_variables.uuid = ANY($2) AND nest_egg_variables.egg_uuid = $3",
599            order_base as i32,
600            &unused_variables
601                .into_iter()
602                .map(|v| v.uuid)
603                .collect::<Vec<_>>(),
604            self.uuid,
605        )
606        .execute(database.write())
607        .await?;
608
609        Ok(())
610    }
611
612    pub async fn all(
613        database: &crate::database::Database,
614    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
615        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
616            r#"
617            SELECT {}
618            FROM nest_eggs
619            ORDER BY nest_eggs.created
620            "#,
621            Self::columns_sql(None)
622        )))
623        .fetch_all(database.read())
624        .await?;
625
626        rows.into_iter()
627            .map(|row| Self::map(None, &row))
628            .try_collect_vec()
629    }
630
631    pub async fn by_nest_uuid_with_pagination(
632        database: &crate::database::Database,
633        nest_uuid: uuid::Uuid,
634        page: i64,
635        per_page: i64,
636        search: Option<&str>,
637    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
638        let offset = (page - 1) * per_page;
639
640        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
641            r#"
642            SELECT {}, COUNT(*) OVER() AS total_count
643            FROM nest_eggs
644            WHERE nest_eggs.nest_uuid = $1 AND ($2 IS NULL OR nest_eggs.name ILIKE '%' || $2 || '%')
645            ORDER BY nest_eggs.created
646            LIMIT $3 OFFSET $4
647            "#,
648            Self::columns_sql(None)
649        )))
650        .bind(nest_uuid)
651        .bind(search)
652        .bind(per_page)
653        .bind(offset)
654        .fetch_all(database.read())
655        .await?;
656
657        Ok(super::Pagination {
658            total: rows
659                .first()
660                .map_or(Ok(0), |row| row.try_get("total_count"))?,
661            per_page,
662            page,
663            data: rows
664                .into_iter()
665                .map(|row| Self::map(None, &row))
666                .try_collect_vec()?,
667        })
668    }
669
670    pub async fn by_user_with_pagination(
671        database: &crate::database::Database,
672        user: &super::user::User,
673        page: i64,
674        per_page: i64,
675        search: Option<&str>,
676    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
677        let offset = (page - 1) * per_page;
678
679        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
680            r#"
681            SELECT *, COUNT(*) OVER() AS total_count
682            FROM (
683                SELECT DISTINCT ON (nest_eggs.uuid) {}
684                FROM servers
685                JOIN nest_eggs ON nest_eggs.uuid = servers.egg_uuid
686                LEFT JOIN server_subusers ON server_subusers.server_uuid = servers.uuid AND server_subusers.user_uuid = $1
687                JOIN nests ON nests.uuid = nest_eggs.nest_uuid
688                WHERE (servers.owner_uuid = $1 OR server_subusers.user_uuid = $1 OR $2)
689                    AND ($3 IS NULL OR nest_eggs.name ILIKE '%' || $3 || '%')
690                ORDER BY nest_eggs.uuid
691            ) AS eggs
692            ORDER BY eggs.created
693            LIMIT $4 OFFSET $5
694            "#,
695            Self::columns_sql(None)
696        )))
697        .bind(user.uuid)
698        .bind(user.role.as_ref().map_or(user.admin, |r| r.admin_permissions.iter().any(|p| p == "servers.read")))
699        .bind(search)
700        .bind(per_page)
701        .bind(offset)
702        .fetch_all(database.read())
703        .await?;
704
705        Ok(super::Pagination {
706            total: rows
707                .first()
708                .map_or(Ok(0), |row| row.try_get("total_count"))?,
709            per_page,
710            page,
711            data: rows
712                .into_iter()
713                .map(|row| Self::map(None, &row))
714                .try_collect_vec()?,
715        })
716    }
717
718    pub async fn by_nest_uuid_uuid(
719        database: &crate::database::Database,
720        nest_uuid: uuid::Uuid,
721        uuid: uuid::Uuid,
722    ) -> Result<Option<Self>, crate::database::DatabaseError> {
723        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
724            r#"
725            SELECT {}
726            FROM nest_eggs
727            WHERE nest_eggs.nest_uuid = $1 AND nest_eggs.uuid = $2
728            "#,
729            Self::columns_sql(None)
730        )))
731        .bind(nest_uuid)
732        .bind(uuid)
733        .fetch_optional(database.read())
734        .await?;
735
736        row.try_map(|row| Self::map(None, &row))
737    }
738
739    pub async fn by_nest_uuid_name(
740        database: &crate::database::Database,
741        nest_uuid: uuid::Uuid,
742        name: &str,
743    ) -> Result<Option<Self>, crate::database::DatabaseError> {
744        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
745            r#"
746            SELECT {}
747            FROM nest_eggs
748            WHERE nest_eggs.nest_uuid = $1 AND nest_eggs.name = $2
749            "#,
750            Self::columns_sql(None)
751        )))
752        .bind(nest_uuid)
753        .bind(name)
754        .fetch_optional(database.read())
755        .await?;
756
757        row.try_map(|row| Self::map(None, &row))
758    }
759
760    pub async fn all_by_nest_uuid(
761        database: &crate::database::Database,
762        nest_uuid: uuid::Uuid,
763    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
764        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
765            r#"
766            SELECT {}
767            FROM nest_eggs
768            WHERE nest_eggs.nest_uuid = $1
769            "#,
770            Self::columns_sql(None)
771        )))
772        .bind(nest_uuid)
773        .fetch_all(database.read())
774        .await?;
775
776        rows.into_iter()
777            .map(|row| Self::map(None, &row))
778            .try_collect_vec()
779    }
780
781    pub async fn count_by_nest_uuid(
782        database: &crate::database::Database,
783        nest_uuid: uuid::Uuid,
784    ) -> Result<i64, sqlx::Error> {
785        sqlx::query_scalar(
786            r#"
787            SELECT COUNT(*)
788            FROM nest_eggs
789            WHERE nest_eggs.nest_uuid = $1
790            "#,
791        )
792        .bind(nest_uuid)
793        .fetch_one(database.read())
794        .await
795    }
796
797    pub async fn configuration(
798        &self,
799        database: &crate::database::Database,
800    ) -> Result<super::egg_configuration::MergedEggConfiguration, anyhow::Error> {
801        database
802            .cache
803            .cached(
804                &format!("nest_egg::{}::configuration", self.uuid),
805                10,
806                || async {
807                    super::egg_configuration::EggConfiguration::merged_by_egg_uuid(
808                        database, self.uuid,
809                    )
810                    .await
811                },
812            )
813            .await
814    }
815
816    #[inline]
817    pub async fn into_exported(
818        self,
819        database: &crate::database::Database,
820    ) -> Result<ExportedNestEgg, crate::database::DatabaseError> {
821        Ok(ExportedNestEgg {
822            uuid: self.uuid,
823            author: self.author,
824            name: self.name,
825            description: self.description,
826            config: ExportedNestEggConfigs {
827                files: self
828                    .config_files
829                    .into_iter()
830                    .map(|file| {
831                        (
832                            file.file,
833                            ExportedNestEggConfigsFilesFile {
834                                create_new: file.create_new,
835                                parser: file.parser,
836                                replace: file.replace,
837                            },
838                        )
839                    })
840                    .collect(),
841                startup: self.config_startup,
842                stop: self.config_stop,
843            },
844            scripts: ExportedNestEggScripts {
845                installation: self.config_script,
846            },
847            startup_commands: self.startup_commands,
848            force_outgoing_ip: self.force_outgoing_ip,
849            separate_port: self.separate_port,
850            features: self.features,
851            docker_images: self.docker_images,
852            file_denylist: self.file_denylist,
853            variables: super::nest_egg_variable::NestEggVariable::all_by_egg_uuid(
854                database, self.uuid,
855            )
856            .await?
857            .into_iter()
858            .map(|variable| variable.into_exported())
859            .collect(),
860        })
861    }
862}
863
864#[async_trait::async_trait]
865impl IntoAdminApiObject for NestEgg {
866    type AdminApiObject = AdminApiNestEgg;
867    type ExtraArgs<'a> = ();
868
869    async fn into_admin_api_object<'a>(
870        self,
871        state: &crate::State,
872        _args: Self::ExtraArgs<'a>,
873    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
874        let api_object = AdminApiNestEgg::init_hooks(&self, state).await?;
875
876        let api_object = finish_extendible!(
877            AdminApiNestEgg {
878                uuid: self.uuid,
879                egg_repository_egg: match self.egg_repository_egg {
880                    Some(egg_repository_egg) => Some(
881                        egg_repository_egg
882                            .fetch_cached(&state.database)
883                            .await?
884                            .into_admin_egg_api_object(state, ())
885                            .await?,
886                    ),
887                    None => None,
888                },
889                name: self.name,
890                description: self.description,
891                author: self.author,
892                config_files: self.config_files,
893                config_startup: self.config_startup,
894                config_stop: self.config_stop,
895                config_script: self.config_script,
896                startup_commands: self.startup_commands,
897                force_outgoing_ip: self.force_outgoing_ip,
898                separate_port: self.separate_port,
899                features: self.features,
900                docker_images: self.docker_images,
901                file_denylist: self.file_denylist,
902                created: self.created.and_utc(),
903            },
904            api_object,
905            state
906        )?;
907
908        Ok(api_object)
909    }
910}
911
912#[async_trait::async_trait]
913impl IntoApiObject for NestEgg {
914    type ApiObject = ApiNestEgg;
915    type ExtraArgs<'a> = ();
916
917    async fn into_api_object<'a>(
918        self,
919        state: &crate::State,
920        _args: Self::ExtraArgs<'a>,
921    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
922        let api_object = ApiNestEgg::init_hooks(&self, state).await?;
923
924        let api_object = finish_extendible!(
925            ApiNestEgg {
926                uuid: self.uuid,
927                name: self.name,
928                description: self.description,
929                startup_commands: self.startup_commands,
930                separate_port: self.separate_port,
931                features: self.features,
932                docker_images: self.docker_images,
933                created: self.created.and_utc(),
934            },
935            api_object,
936            state
937        )?;
938
939        Ok(api_object)
940    }
941}
942
943#[async_trait::async_trait]
944impl ByUuid for NestEgg {
945    async fn by_uuid(
946        database: &crate::database::Database,
947        uuid: uuid::Uuid,
948    ) -> Result<Self, crate::database::DatabaseError> {
949        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
950            r#"
951            SELECT {}
952            FROM nest_eggs
953            WHERE nest_eggs.uuid = $1
954            "#,
955            Self::columns_sql(None)
956        )))
957        .bind(uuid)
958        .fetch_one(database.read())
959        .await?;
960
961        Self::map(None, &row)
962    }
963
964    async fn by_uuid_with_transaction(
965        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
966        uuid: uuid::Uuid,
967    ) -> Result<Self, crate::database::DatabaseError> {
968        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
969            r#"
970            SELECT {}
971            FROM nest_eggs
972            WHERE nest_eggs.uuid = $1
973            "#,
974            Self::columns_sql(None)
975        )))
976        .bind(uuid)
977        .fetch_one(&mut **transaction)
978        .await?;
979
980        Self::map(None, &row)
981    }
982}
983
984#[derive(ToSchema, Deserialize, Validate)]
985pub struct CreateNestEggOptions {
986    #[garde(skip)]
987    pub nest_uuid: uuid::Uuid,
988    #[garde(skip)]
989    pub egg_repository_egg_uuid: Option<uuid::Uuid>,
990    #[garde(length(chars, min = 2, max = 255))]
991    #[schema(min_length = 2, max_length = 255)]
992    pub author: compact_str::CompactString,
993    #[garde(length(chars, min = 1, max = 255))]
994    #[schema(min_length = 1, max_length = 255)]
995    pub name: compact_str::CompactString,
996    #[garde(length(chars, min = 1, max = 1024))]
997    #[schema(min_length = 1, max_length = 1024)]
998    pub description: Option<compact_str::CompactString>,
999    #[garde(skip)]
1000    #[schema(inline)]
1001    pub config_files: Vec<ProcessConfigurationFile>,
1002    #[garde(skip)]
1003    #[schema(inline)]
1004    pub config_startup: NestEggConfigStartup,
1005    #[garde(skip)]
1006    #[schema(inline)]
1007    pub config_stop: NestEggConfigStop,
1008    #[garde(skip)]
1009    #[schema(inline)]
1010    pub config_script: NestEggConfigScript,
1011    #[garde(custom(validate_startup_commands))]
1012    pub startup_commands: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1013    #[garde(skip)]
1014    pub force_outgoing_ip: bool,
1015    #[garde(skip)]
1016    pub separate_port: bool,
1017    #[garde(skip)]
1018    pub features: Vec<compact_str::CompactString>,
1019    #[garde(custom(validate_docker_images))]
1020    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1021    #[garde(skip)]
1022    pub file_denylist: Vec<compact_str::CompactString>,
1023}
1024
1025#[async_trait::async_trait]
1026impl CreatableModel for NestEgg {
1027    type CreateOptions<'a> = CreateNestEggOptions;
1028    type CreateResult = Self;
1029
1030    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
1031        static CREATE_LISTENERS: LazyLock<CreateListenerList<NestEgg>> =
1032            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1033
1034        &CREATE_LISTENERS
1035    }
1036
1037    async fn create_with_transaction(
1038        state: &crate::State,
1039        mut options: Self::CreateOptions<'_>,
1040        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1041    ) -> Result<Self, crate::database::DatabaseError> {
1042        options.validate()?;
1043
1044        if let Some(egg_repository_egg_uuid) = options.egg_repository_egg_uuid {
1045            super::egg_repository_egg::EggRepositoryEgg::by_uuid_optional_cached(
1046                &state.database,
1047                egg_repository_egg_uuid,
1048            )
1049            .await?
1050            .ok_or(crate::database::InvalidRelationError("egg_repository_egg"))?;
1051        }
1052
1053        let mut query_builder = InsertQueryBuilder::new("nest_eggs");
1054
1055        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
1056
1057        query_builder
1058            .set("nest_uuid", options.nest_uuid)
1059            .set("egg_repository_egg_uuid", options.egg_repository_egg_uuid)
1060            .set("author", &options.author)
1061            .set("name", &options.name)
1062            .set("description", &options.description)
1063            .set("config_files", serde_json::to_value(&options.config_files)?)
1064            .set(
1065                "config_startup",
1066                serde_json::to_value(&options.config_startup)?,
1067            )
1068            .set("config_stop", serde_json::to_value(&options.config_stop)?)
1069            .set(
1070                "config_script",
1071                serde_json::to_value(&options.config_script)?,
1072            )
1073            .set("startup_commands", OrderedJson(&options.startup_commands))
1074            .set("force_outgoing_ip", options.force_outgoing_ip)
1075            .set("separate_port", options.separate_port)
1076            .set("features", &options.features)
1077            .set("docker_images", OrderedJson(&options.docker_images))
1078            .set("file_denylist", &options.file_denylist);
1079
1080        let row = query_builder
1081            .returning(&Self::columns_sql(None))
1082            .fetch_one(&mut **transaction)
1083            .await?;
1084        let mut nest_egg = Self::map(None, &row)?;
1085
1086        Self::run_after_create_handlers(&mut nest_egg, &options, state, transaction).await?;
1087
1088        Ok(nest_egg)
1089    }
1090}
1091
1092#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
1093pub struct UpdateNestEggOptions {
1094    #[garde(skip)]
1095    #[serde(
1096        default,
1097        skip_serializing_if = "Option::is_none",
1098        with = "::serde_with::rust::double_option"
1099    )]
1100    pub egg_repository_egg_uuid: Option<Option<uuid::Uuid>>,
1101    #[garde(length(chars, min = 2, max = 255))]
1102    #[schema(min_length = 2, max_length = 255)]
1103    pub author: Option<compact_str::CompactString>,
1104    #[garde(length(chars, min = 3, max = 255))]
1105    #[schema(min_length = 3, max_length = 255)]
1106    pub name: Option<compact_str::CompactString>,
1107    #[garde(length(chars, min = 1, max = 1024))]
1108    #[schema(min_length = 1, max_length = 1024)]
1109    #[serde(
1110        default,
1111        skip_serializing_if = "Option::is_none",
1112        with = "::serde_with::rust::double_option"
1113    )]
1114    pub description: Option<Option<compact_str::CompactString>>,
1115    #[garde(skip)]
1116    #[schema(inline)]
1117    pub config_files: Option<Vec<ProcessConfigurationFile>>,
1118    #[garde(skip)]
1119    #[schema(inline)]
1120    pub config_startup: Option<NestEggConfigStartup>,
1121    #[garde(skip)]
1122    #[schema(inline)]
1123    pub config_stop: Option<NestEggConfigStop>,
1124    #[garde(skip)]
1125    #[schema(inline)]
1126    pub config_script: Option<NestEggConfigScript>,
1127    #[garde(inner(custom(validate_startup_commands)))]
1128    pub startup_commands: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
1129    #[garde(skip)]
1130    pub force_outgoing_ip: Option<bool>,
1131    #[garde(skip)]
1132    pub separate_port: Option<bool>,
1133    #[garde(skip)]
1134    pub features: Option<Vec<compact_str::CompactString>>,
1135    #[garde(inner(custom(validate_docker_images)))]
1136    pub docker_images: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
1137    #[garde(skip)]
1138    pub file_denylist: Option<Vec<compact_str::CompactString>>,
1139}
1140
1141#[async_trait::async_trait]
1142impl UpdatableModel for NestEgg {
1143    type UpdateOptions = UpdateNestEggOptions;
1144
1145    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
1146        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<NestEgg>> =
1147            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1148
1149        &UPDATE_LISTENERS
1150    }
1151
1152    async fn update_with_transaction(
1153        &mut self,
1154        state: &crate::State,
1155        mut options: Self::UpdateOptions,
1156        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1157    ) -> Result<(), crate::database::DatabaseError> {
1158        options.validate()?;
1159
1160        let egg_repository_egg =
1161            if let Some(egg_repository_egg_uuid) = &options.egg_repository_egg_uuid {
1162                match egg_repository_egg_uuid {
1163                    Some(uuid) => {
1164                        super::egg_repository_egg::EggRepositoryEgg::by_uuid_optional_cached(
1165                            &state.database,
1166                            *uuid,
1167                        )
1168                        .await?
1169                        .ok_or(crate::database::InvalidRelationError("egg_repository_egg"))?;
1170                        Some(Some(
1171                            super::egg_repository_egg::EggRepositoryEgg::get_fetchable(*uuid),
1172                        ))
1173                    }
1174                    None => Some(None),
1175                }
1176            } else {
1177                None
1178            };
1179
1180        let mut query_builder = UpdateQueryBuilder::new("nest_eggs");
1181
1182        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
1183            .await?;
1184
1185        query_builder
1186            .set(
1187                "egg_repository_egg_uuid",
1188                options.egg_repository_egg_uuid.as_ref().map(|o| o.as_ref()),
1189            )
1190            .set("author", options.author.as_ref())
1191            .set("name", options.name.as_ref())
1192            .set(
1193                "description",
1194                options.description.as_ref().map(|d| d.as_ref()),
1195            )
1196            .set(
1197                "config_files",
1198                options
1199                    .config_files
1200                    .as_ref()
1201                    .map(serde_json::to_value)
1202                    .transpose()?,
1203            )
1204            .set(
1205                "config_startup",
1206                options
1207                    .config_startup
1208                    .as_ref()
1209                    .map(serde_json::to_value)
1210                    .transpose()?,
1211            )
1212            .set(
1213                "config_stop",
1214                options
1215                    .config_stop
1216                    .as_ref()
1217                    .map(serde_json::to_value)
1218                    .transpose()?,
1219            )
1220            .set(
1221                "config_script",
1222                options
1223                    .config_script
1224                    .as_ref()
1225                    .map(serde_json::to_value)
1226                    .transpose()?,
1227            )
1228            .set(
1229                "startup_commands",
1230                options.startup_commands.as_ref().map(OrderedJson),
1231            )
1232            .set("force_outgoing_ip", options.force_outgoing_ip)
1233            .set("separate_port", options.separate_port)
1234            .set("features", options.features.as_ref())
1235            .set(
1236                "docker_images",
1237                options.docker_images.as_ref().map(OrderedJson),
1238            )
1239            .set("file_denylist", options.file_denylist.as_ref())
1240            .where_eq("uuid", self.uuid);
1241
1242        query_builder.execute(&mut **transaction).await?;
1243
1244        if let Some(egg_repository_egg) = egg_repository_egg {
1245            self.egg_repository_egg = egg_repository_egg;
1246        }
1247        if let Some(author) = options.author {
1248            self.author = author;
1249        }
1250        if let Some(name) = options.name {
1251            self.name = name;
1252        }
1253        if let Some(description) = options.description {
1254            self.description = description;
1255        }
1256        if let Some(config_files) = options.config_files {
1257            self.config_files = config_files;
1258        }
1259        if let Some(config_startup) = options.config_startup {
1260            self.config_startup = config_startup;
1261        }
1262        if let Some(config_stop) = options.config_stop {
1263            self.config_stop = config_stop;
1264        }
1265        if let Some(config_script) = options.config_script {
1266            self.config_script = config_script;
1267        }
1268        if let Some(startup_commands) = options.startup_commands {
1269            self.startup_commands = startup_commands;
1270        }
1271        if let Some(force_outgoing_ip) = options.force_outgoing_ip {
1272            self.force_outgoing_ip = force_outgoing_ip;
1273        }
1274        if let Some(separate_port) = options.separate_port {
1275            self.separate_port = separate_port;
1276        }
1277        if let Some(features) = options.features {
1278            self.features = features;
1279        }
1280        if let Some(docker_images) = options.docker_images {
1281            self.docker_images = docker_images;
1282        }
1283        if let Some(file_denylist) = options.file_denylist {
1284            self.file_denylist = file_denylist;
1285        }
1286
1287        self.run_after_update_handlers(state, transaction).await?;
1288
1289        Ok(())
1290    }
1291}
1292
1293#[async_trait::async_trait]
1294impl DeletableModel for NestEgg {
1295    type DeleteOptions = ();
1296
1297    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
1298        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<NestEgg>> =
1299            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1300
1301        &DELETE_LISTENERS
1302    }
1303
1304    async fn delete_with_transaction(
1305        &self,
1306        state: &crate::State,
1307        options: Self::DeleteOptions,
1308        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1309    ) -> Result<(), anyhow::Error> {
1310        self.run_delete_handlers(&options, state, transaction)
1311            .await?;
1312
1313        sqlx::query(
1314            r#"
1315            DELETE FROM nest_eggs
1316            WHERE nest_eggs.uuid = $1
1317            "#,
1318        )
1319        .bind(self.uuid)
1320        .execute(&mut **transaction)
1321        .await?;
1322
1323        self.run_after_delete_handlers(&options, state, transaction)
1324            .await?;
1325
1326        Ok(())
1327    }
1328}
1329
1330#[derive(Validate)]
1331pub struct DuplicateNestEggOptions {
1332    #[garde(skip)]
1333    pub nest_uuid: uuid::Uuid,
1334    #[garde(length(chars, min = 1, max = 255))]
1335    pub name: compact_str::CompactString,
1336}
1337
1338#[async_trait::async_trait]
1339impl DuplicableModel for NestEgg {
1340    type DuplicateOptions<'a> = DuplicateNestEggOptions;
1341
1342    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
1343        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<NestEgg>> =
1344            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1345
1346        &DUPLICATE_LISTENERS
1347    }
1348
1349    async fn duplicate_with_transaction(
1350        &self,
1351        state: &crate::State,
1352        options: Self::DuplicateOptions<'_>,
1353        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1354    ) -> Result<Self, crate::database::DatabaseError> {
1355        options.validate()?;
1356
1357        self.run_duplicate_handlers(&options, state, transaction)
1358            .await?;
1359
1360        let mut query_builder = InsertQueryBuilder::new("nest_eggs");
1361
1362        query_builder
1363            .set("nest_uuid", options.nest_uuid)
1364            .set("egg_repository_egg_uuid", None::<uuid::Uuid>)
1365            .set("author", &self.author)
1366            .set("name", &options.name)
1367            .set("description", &self.description)
1368            .set("config_files", serde_json::to_value(&self.config_files)?)
1369            .set(
1370                "config_startup",
1371                serde_json::to_value(&self.config_startup)?,
1372            )
1373            .set("config_stop", serde_json::to_value(&self.config_stop)?)
1374            .set("config_script", serde_json::to_value(&self.config_script)?)
1375            .set("startup_commands", OrderedJson(&self.startup_commands))
1376            .set("force_outgoing_ip", self.force_outgoing_ip)
1377            .set("separate_port", self.separate_port)
1378            .set("features", &self.features)
1379            .set("docker_images", OrderedJson(&self.docker_images))
1380            .set("file_denylist", &self.file_denylist);
1381
1382        let row = query_builder
1383            .returning(&Self::columns_sql(None))
1384            .fetch_one(&mut **transaction)
1385            .await?;
1386        let mut nest_egg = Self::map(None, &row)?;
1387
1388        sqlx::query!(
1389            "INSERT INTO nest_egg_variables (
1390                egg_uuid, name, name_translations, description, description_translations,
1391                order_, env_variable, default_value, user_viewable, user_editable, secret, rules
1392            )
1393            SELECT
1394                $1, nest_egg_variables.name, nest_egg_variables.name_translations,
1395                nest_egg_variables.description, nest_egg_variables.description_translations,
1396                nest_egg_variables.order_, nest_egg_variables.env_variable,
1397                nest_egg_variables.default_value, nest_egg_variables.user_viewable,
1398                nest_egg_variables.user_editable, nest_egg_variables.secret, nest_egg_variables.rules
1399            FROM nest_egg_variables
1400            WHERE nest_egg_variables.egg_uuid = $2",
1401            nest_egg.uuid,
1402            self.uuid,
1403        )
1404        .execute(&mut **transaction)
1405        .await?;
1406
1407        sqlx::query!(
1408            "INSERT INTO nest_egg_mounts (egg_uuid, mount_uuid)
1409            SELECT $1, nest_egg_mounts.mount_uuid
1410            FROM nest_egg_mounts
1411            WHERE nest_egg_mounts.egg_uuid = $2",
1412            nest_egg.uuid,
1413            self.uuid,
1414        )
1415        .execute(&mut **transaction)
1416        .await?;
1417
1418        self.run_after_duplicate_handlers(&mut nest_egg, &options, state, transaction)
1419            .await?;
1420
1421        Ok(nest_egg)
1422    }
1423}
1424
1425#[schema_extension_derive::extendible]
1426#[init_args(NestEgg, crate::State)]
1427#[hook_args(crate::State)]
1428#[derive(ToSchema, Serialize)]
1429#[schema(title = "AdminNestEgg")]
1430pub struct AdminApiNestEgg {
1431    pub uuid: uuid::Uuid,
1432    pub egg_repository_egg: Option<super::egg_repository_egg::AdminApiEggEggRepositoryEgg>,
1433
1434    pub name: compact_str::CompactString,
1435    pub description: Option<compact_str::CompactString>,
1436    pub author: compact_str::CompactString,
1437
1438    #[schema(inline)]
1439    pub config_files: Vec<ProcessConfigurationFile>,
1440    #[schema(inline)]
1441    pub config_startup: NestEggConfigStartup,
1442    #[schema(inline)]
1443    pub config_stop: NestEggConfigStop,
1444    #[schema(inline)]
1445    pub config_script: NestEggConfigScript,
1446
1447    pub startup_commands: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1448    pub force_outgoing_ip: bool,
1449    pub separate_port: bool,
1450
1451    pub features: Vec<compact_str::CompactString>,
1452    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1453    pub file_denylist: Vec<compact_str::CompactString>,
1454
1455    pub created: chrono::DateTime<chrono::Utc>,
1456}
1457
1458#[schema_extension_derive::extendible]
1459#[init_args(NestEgg, crate::State)]
1460#[hook_args(crate::State)]
1461#[derive(ToSchema, Serialize)]
1462#[schema(title = "NestEgg")]
1463pub struct ApiNestEgg {
1464    pub uuid: uuid::Uuid,
1465
1466    pub name: compact_str::CompactString,
1467    pub description: Option<compact_str::CompactString>,
1468
1469    pub startup_commands: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1470    pub separate_port: bool,
1471
1472    pub features: Vec<compact_str::CompactString>,
1473    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
1474
1475    pub created: chrono::DateTime<chrono::Utc>,
1476}