Skip to main content

shared/models/
egg_repository_egg.rs

1use crate::prelude::*;
2use serde::{Deserialize, Serialize};
3use sqlx::{Row, postgres::PgRow};
4use std::{
5    collections::{BTreeMap, HashMap},
6    sync::{Arc, LazyLock},
7};
8use utoipa::ToSchema;
9
10#[derive(Serialize, Deserialize, Clone)]
11pub struct EggRepositoryEgg {
12    pub uuid: uuid::Uuid,
13    pub path: String,
14    pub egg_repository: Fetchable<super::egg_repository::EggRepository>,
15
16    pub readme: Option<compact_str::CompactString>,
17    pub banner: Option<String>,
18    pub exported_egg: super::nest_egg::ExportedNestEgg,
19    pub hash: Option<compact_str::CompactString>,
20
21    pub updated: chrono::NaiveDateTime,
22
23    extension_data: super::ModelExtensionData,
24}
25
26impl BaseModel for EggRepositoryEgg {
27    const NAME: &'static str = "egg_repository_egg";
28
29    fn get_extension_list() -> &'static super::ModelExtensionList {
30        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
31            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
32
33        &EXTENSIONS
34    }
35
36    fn get_extension_data(&self) -> &super::ModelExtensionData {
37        &self.extension_data
38    }
39
40    #[inline]
41    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
42        let prefix = prefix.unwrap_or_default();
43
44        BTreeMap::from([
45            (
46                "egg_repository_eggs.uuid",
47                compact_str::format_compact!("{prefix}uuid"),
48            ),
49            (
50                "egg_repository_eggs.path",
51                compact_str::format_compact!("{prefix}path"),
52            ),
53            (
54                "egg_repository_eggs.egg_repository_uuid",
55                compact_str::format_compact!("{prefix}egg_repository_uuid"),
56            ),
57            (
58                "egg_repository_eggs.readme",
59                compact_str::format_compact!("{prefix}readme"),
60            ),
61            (
62                "egg_repository_eggs.banner",
63                compact_str::format_compact!("{prefix}banner"),
64            ),
65            (
66                "egg_repository_eggs.exported_egg",
67                compact_str::format_compact!("{prefix}exported_egg"),
68            ),
69            (
70                "egg_repository_eggs.hash",
71                compact_str::format_compact!("{prefix}hash"),
72            ),
73            (
74                "egg_repository_eggs.updated",
75                compact_str::format_compact!("{prefix}updated"),
76            ),
77        ])
78    }
79
80    #[inline]
81    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
82        let prefix = prefix.unwrap_or_default();
83
84        Ok(Self {
85            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
86            path: row.try_get(compact_str::format_compact!("{prefix}path").as_str())?,
87            egg_repository: super::egg_repository::EggRepository::get_fetchable(
88                row.try_get(compact_str::format_compact!("{prefix}egg_repository_uuid").as_str())?,
89            ),
90            readme: row.try_get(compact_str::format_compact!("{prefix}readme").as_str())?,
91            banner: row.try_get(compact_str::format_compact!("{prefix}banner").as_str())?,
92            exported_egg: serde_json::from_value(
93                row.try_get(compact_str::format_compact!("{prefix}exported_egg").as_str())?,
94            )?,
95            hash: row.try_get(compact_str::format_compact!("{prefix}hash").as_str())?,
96            updated: row.try_get(compact_str::format_compact!("{prefix}updated").as_str())?,
97            extension_data: Self::map_extensions(prefix, row)?,
98        })
99    }
100
101    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
102        vec![compact_str::format_compact!(
103            "{}::{}",
104            Self::NAME,
105            self.uuid
106        )]
107    }
108}
109
110impl EggRepositoryEgg {
111    pub async fn create(
112        database: &crate::database::Database,
113        egg_repository_uuid: uuid::Uuid,
114        path: impl AsRef<str>,
115        readme: Option<&str>,
116        banner: Option<&str>,
117        exported_egg: &super::nest_egg::ExportedNestEgg,
118        updated: chrono::NaiveDateTime,
119    ) -> Result<Self, crate::database::DatabaseError> {
120        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
121            r#"
122            INSERT INTO egg_repository_eggs (egg_repository_uuid, path, readme, banner, exported_egg, hash, updated)
123            VALUES ($1, $2, $3, $4, $5, $6, $7)
124            ON CONFLICT (egg_repository_uuid, path) DO UPDATE SET
125                readme = EXCLUDED.readme,
126                banner = EXCLUDED.banner,
127                exported_egg = EXCLUDED.exported_egg,
128                hash = EXCLUDED.hash,
129                updated = EXCLUDED.updated
130            RETURNING {}
131            "#,
132            Self::columns_sql(None)
133        )))
134        .bind(egg_repository_uuid)
135        .bind(path.as_ref())
136        .bind(readme)
137        .bind(banner)
138        .bind(OrderedJson(exported_egg))
139        .bind(exported_egg.content_hash())
140        .bind(updated)
141        .fetch_one(database.write())
142        .await?;
143
144        Self::map(None, &row)
145    }
146
147    pub async fn by_egg_repository_uuid_uuid(
148        database: &crate::database::Database,
149        egg_repository_uuid: uuid::Uuid,
150        uuid: uuid::Uuid,
151    ) -> Result<Option<Self>, crate::database::DatabaseError> {
152        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
153            r#"
154            SELECT {}
155            FROM egg_repository_eggs
156            WHERE egg_repository_eggs.egg_repository_uuid = $1 AND egg_repository_eggs.uuid = $2
157            "#,
158            Self::columns_sql(None)
159        )))
160        .bind(egg_repository_uuid)
161        .bind(uuid)
162        .fetch_optional(database.read())
163        .await?;
164
165        match row {
166            Some(row) => Ok(Some(Self::map(None, &row)?)),
167            None => Ok(None),
168        }
169    }
170
171    pub async fn by_egg_repository_uuid_with_pagination(
172        database: &crate::database::Database,
173        egg_repository_uuid: uuid::Uuid,
174        page: i64,
175        per_page: i64,
176        search: Option<&str>,
177    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
178        super::PaginatedQueryBuilder::new(
179            Self::columns_sql(None),
180            format!(
181                r#"
182                FROM egg_repository_eggs
183                WHERE egg_repository_eggs.egg_repository_uuid = $1 AND {search}
184                "#,
185                search = super::search_sql(
186                    2,
187                    &[
188                        "egg_repository_eggs.path",
189                        "egg_repository_eggs.exported_egg->>'name'"
190                    ],
191                    &["egg_repository_eggs.uuid"]
192                ),
193            ),
194            "egg_repository_eggs.exported_egg->>'name'",
195        )
196        .bind(egg_repository_uuid)
197        .bind(search)
198        .fetch(database, page, per_page)
199        .await
200    }
201
202    pub async fn all_with_pagination(
203        database: &crate::database::Database,
204        page: i64,
205        per_page: i64,
206        search: Option<&str>,
207    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
208        super::PaginatedQueryBuilder::new(
209            Self::columns_sql(None),
210            format!(
211                r#"
212                FROM egg_repository_eggs
213                WHERE {search}
214                "#,
215                search = super::search_sql(
216                    1,
217                    &[
218                        "egg_repository_eggs.path",
219                        "egg_repository_eggs.exported_egg->>'name'"
220                    ],
221                    &["egg_repository_eggs.uuid"]
222                ),
223            ),
224            "egg_repository_eggs.exported_egg->>'name', egg_repository_eggs.path",
225        )
226        .bind(search)
227        .fetch(database, page, per_page)
228        .await
229    }
230
231    pub async fn installed_eggs_by_uuids(
232        database: &crate::database::Database,
233        uuids: &[uuid::Uuid],
234    ) -> Result<
235        HashMap<uuid::Uuid, Vec<EggRepositoryEggInstalledEgg>>,
236        crate::database::DatabaseError,
237    > {
238        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
239            r#"
240            SELECT
241                nest_eggs.uuid, nest_eggs.name, nest_eggs.egg_repository_egg_uuid,
242                nest_eggs.egg_repository_egg_hash, {}
243            FROM nest_eggs
244            JOIN nests ON nests.uuid = nest_eggs.nest_uuid
245            WHERE nest_eggs.egg_repository_egg_uuid = ANY($1)
246            ORDER BY nest_eggs.created, nest_eggs.uuid
247            "#,
248            super::nest::Nest::columns_sql(Some("nest_"))
249        )))
250        .bind(uuids)
251        .fetch_all(database.read())
252        .await?;
253
254        let mut installed: HashMap<uuid::Uuid, Vec<EggRepositoryEggInstalledEgg>> = HashMap::new();
255        for row in rows {
256            installed
257                .entry(row.try_get("egg_repository_egg_uuid")?)
258                .or_default()
259                .push(EggRepositoryEggInstalledEgg {
260                    uuid: row.try_get("uuid")?,
261                    name: row.try_get("name")?,
262                    egg_repository_egg_hash: row.try_get("egg_repository_egg_hash")?,
263                    nest: super::nest::Nest::map(Some("nest_"), &row)?,
264                });
265        }
266
267        Ok(installed)
268    }
269
270    pub async fn banners_by_egg_repository_uuid(
271        database: &crate::database::Database,
272        egg_repository_uuid: uuid::Uuid,
273    ) -> Result<Vec<String>, crate::database::DatabaseError> {
274        let rows = sqlx::query(
275            r#"
276            SELECT DISTINCT egg_repository_eggs.banner
277            FROM egg_repository_eggs
278            WHERE egg_repository_eggs.egg_repository_uuid = $1 AND egg_repository_eggs.banner IS NOT NULL
279            "#,
280        )
281        .bind(egg_repository_uuid)
282        .fetch_all(database.write())
283        .await?;
284
285        Ok(rows
286            .into_iter()
287            .map(|row| row.try_get("banner"))
288            .try_collect_vec()?)
289    }
290
291    pub async fn delete_unused(
292        database: &crate::database::Database,
293        egg_repository_uuid: uuid::Uuid,
294        paths: &[compact_str::CompactString],
295    ) -> Result<(), crate::database::DatabaseError> {
296        sqlx::query(
297            r#"
298            DELETE FROM egg_repository_eggs
299            WHERE egg_repository_eggs.egg_repository_uuid = $1 AND egg_repository_eggs.path != ALL($2)
300            "#,
301        )
302        .bind(egg_repository_uuid)
303        .bind(paths)
304        .execute(database.write())
305        .await?;
306
307        Ok(())
308    }
309
310    pub async fn into_admin_egg_api_object(
311        self,
312        state: &crate::State,
313        _args: (),
314    ) -> Result<AdminApiEggEggRepositoryEgg, crate::database::DatabaseError> {
315        Ok(AdminApiEggEggRepositoryEgg {
316            uuid: self.uuid,
317            path: self.path,
318            egg_repository: self
319                .egg_repository
320                .fetch_cached(&state.database)
321                .await?
322                .into_admin_api_object(state, ())
323                .await?,
324            readme: self.readme,
325            banner: match self.banner {
326                Some(banner) => Some(state.storage.retrieve_urls().await?.get_url(banner)),
327                None => None,
328            },
329            hash: self
330                .hash
331                .unwrap_or_else(|| self.exported_egg.content_hash()),
332            exported_egg: self.exported_egg,
333            updated: self.updated.and_utc(),
334        })
335    }
336}
337
338pub struct EggRepositoryEggInstalledEgg {
339    pub uuid: uuid::Uuid,
340    pub name: compact_str::CompactString,
341    pub egg_repository_egg_hash: Option<compact_str::CompactString>,
342    pub nest: super::nest::Nest,
343}
344
345impl EggRepositoryEggInstalledEgg {
346    pub async fn into_admin_api_object(
347        self,
348        state: &crate::State,
349    ) -> Result<AdminApiEggRepositoryEggInstalledEgg, crate::database::DatabaseError> {
350        Ok(AdminApiEggRepositoryEggInstalledEgg {
351            uuid: self.uuid,
352            name: self.name,
353            egg_repository_egg_hash: self.egg_repository_egg_hash,
354            nest: self.nest.into_admin_api_object(state, ()).await?,
355        })
356    }
357}
358
359#[async_trait::async_trait]
360impl IntoAdminApiObject for EggRepositoryEgg {
361    type AdminApiObject = AdminApiEggRepositoryEgg;
362    type ExtraArgs<'a> = ();
363
364    async fn into_admin_api_object<'a>(
365        self,
366        state: &crate::State,
367        _args: Self::ExtraArgs<'a>,
368    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
369        let api_object = AdminApiEggRepositoryEgg::init_hooks(&self, state).await?;
370
371        let api_object = finish_extendible!(
372            AdminApiEggRepositoryEgg {
373                uuid: self.uuid,
374                path: self.path,
375                readme: self.readme,
376                banner: match self.banner {
377                    Some(banner) => Some(state.storage.retrieve_urls().await?.get_url(banner)),
378                    None => None,
379                },
380                hash: self
381                    .hash
382                    .unwrap_or_else(|| self.exported_egg.content_hash()),
383                exported_egg: self.exported_egg,
384                updated: self.updated.and_utc(),
385            },
386            api_object,
387            state
388        )?;
389
390        Ok(api_object)
391    }
392}
393
394#[async_trait::async_trait]
395impl ByUuid for EggRepositoryEgg {
396    async fn by_uuid(
397        database: &crate::database::Database,
398        uuid: uuid::Uuid,
399    ) -> Result<Self, crate::database::DatabaseError> {
400        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
401            r#"
402            SELECT {}
403            FROM egg_repository_eggs
404            WHERE egg_repository_eggs.uuid = $1
405            "#,
406            Self::columns_sql(None)
407        )))
408        .bind(uuid)
409        .fetch_one(database.read())
410        .await?;
411
412        Self::map(None, &row)
413    }
414
415    async fn by_uuid_with_transaction(
416        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
417        uuid: uuid::Uuid,
418    ) -> Result<Self, crate::database::DatabaseError> {
419        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
420            r#"
421            SELECT {}
422            FROM egg_repository_eggs
423            WHERE egg_repository_eggs.uuid = $1
424            "#,
425            Self::columns_sql(None)
426        )))
427        .bind(uuid)
428        .fetch_one(&mut **transaction)
429        .await?;
430
431        Self::map(None, &row)
432    }
433}
434
435#[async_trait::async_trait]
436impl DeletableModel for EggRepositoryEgg {
437    type DeleteOptions = ();
438
439    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
440        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<EggRepositoryEgg>> =
441            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
442
443        &DELETE_LISTENERS
444    }
445
446    async fn delete_with_transaction(
447        &self,
448        state: &crate::State,
449        options: Self::DeleteOptions,
450        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
451    ) -> Result<(), anyhow::Error> {
452        self.run_delete_handlers(&options, state, transaction)
453            .await?;
454
455        sqlx::query(
456            r#"
457            DELETE FROM egg_repository_eggs
458            WHERE egg_repository_eggs.uuid = $1
459            "#,
460        )
461        .bind(self.uuid)
462        .execute(&mut **transaction)
463        .await?;
464
465        self.run_after_delete_handlers(&options, state, transaction)
466            .await?;
467
468        Ok(())
469    }
470}
471
472#[schema_extension_derive::extendible]
473#[init_args(EggRepositoryEgg, crate::State)]
474#[hook_args(crate::State)]
475#[derive(ToSchema, Serialize)]
476#[schema(title = "EggRepositoryEgg")]
477pub struct AdminApiEggRepositoryEgg {
478    pub uuid: uuid::Uuid,
479    pub path: String,
480
481    pub readme: Option<compact_str::CompactString>,
482    pub banner: Option<String>,
483    pub exported_egg: super::nest_egg::ExportedNestEgg,
484    pub hash: compact_str::CompactString,
485
486    pub updated: chrono::DateTime<chrono::Utc>,
487}
488
489#[derive(ToSchema, Serialize)]
490#[schema(title = "EggRepositoryEggInstalledEgg")]
491pub struct AdminApiEggRepositoryEggInstalledEgg {
492    pub uuid: uuid::Uuid,
493    pub name: compact_str::CompactString,
494    pub egg_repository_egg_hash: Option<compact_str::CompactString>,
495    pub nest: super::nest::AdminApiNest,
496}
497
498#[derive(ToSchema, Serialize)]
499#[schema(title = "EggEggRepositoryEgg")]
500pub struct AdminApiEggEggRepositoryEgg {
501    pub uuid: uuid::Uuid,
502    pub path: String,
503    pub egg_repository: super::egg_repository::AdminApiEggRepository,
504
505    pub readme: Option<compact_str::CompactString>,
506    pub banner: Option<String>,
507    pub exported_egg: super::nest_egg::ExportedNestEgg,
508    pub hash: compact_str::CompactString,
509
510    pub updated: chrono::DateTime<chrono::Utc>,
511}