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}