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