Skip to main content

kellnr_db/database/
mod.rs

1mod acl;
2mod auth_tokens;
3mod crates;
4mod cratesio_cache;
5mod docs;
6mod groups;
7mod oauth2;
8mod operations;
9mod search;
10mod sessions;
11mod stats;
12#[cfg(feature = "test-support")]
13pub mod test_utils;
14mod toolchains;
15mod users;
16mod webhooks;
17
18use std::collections::HashMap;
19use std::path::Path;
20
21use chrono::{DateTime, Utc};
22use kellnr_common::account_name::{GroupName, UserName};
23use kellnr_common::canonical_name::CanonicalName;
24use kellnr_common::crate_data::CrateData;
25use kellnr_common::crate_dependents::CrateDependents;
26use kellnr_common::crate_overview::CrateOverview;
27use kellnr_common::cratesio_prefetch_msg::CratesioPrefetchMsg;
28use kellnr_common::index_metadata::IndexMetadata;
29use kellnr_common::original_name::OriginalName;
30use kellnr_common::plain_token::PlainToken;
31use kellnr_common::prefetch::Prefetch;
32use kellnr_common::publish_metadata::PublishMetadata;
33use kellnr_common::version::Version;
34use kellnr_common::webhook::{Webhook, WebhookEvent, WebhookQueue};
35use kellnr_entity::{group, krate, user};
36use sea_orm::prelude::async_trait::async_trait;
37use sea_orm::query::TransactionTrait;
38use sea_orm::sea_query::{Alias, Expr, Func, Iden, Query};
39use sea_orm::{
40    ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait, ExprTrait,
41    FromQueryResult, ModelTrait, QueryFilter, Statement,
42};
43use tracing::warn;
44
45use crate::error::DbError;
46use crate::provider::{
47    CachedCratesioVersion, ChannelInfo, CrateSort, CrateTagFilter, CrateVisibility,
48    CratesioVersionMeta, DbResult, OAuth2StateData, PrefetchState, SessionInfo,
49    ToolchainWithTargets,
50};
51use crate::tables::init_database;
52use crate::{
53    AuthToken, AuthTokenId, ConString, CrateId, CrateMeta, DbProvider, DocQueueEntry, Group,
54    ToolchainTargetStatus, User, UserFlags,
55};
56
57const DB_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
58
59use kellnr_settings::SessionAge;
60
61/// Resolve a configured session age into the internal expiry duration,
62/// [`None`] if sessions never expire.
63fn session_age_to_duration(age: SessionAge) -> Option<chrono::Duration> {
64    // The expiry is at most `MAX_SESSION_AGE`, which fits comfortably in a
65    // `chrono::Duration`, so the conversion cannot fail.
66    age.expiry()
67        .map(|d| chrono::Duration::from_std(d).expect("bounded session age fits chrono::Duration"))
68}
69
70#[derive(Debug, PartialEq, FromQueryResult)]
71struct CountResult {
72    count: Option<i64>,
73}
74
75pub struct Database {
76    db_con: DatabaseConnection,
77    /// Maximum lifetime of a session before it is treated as expired, or
78    /// `None` if sessions never expire (configured age of zero).
79    session_age: Option<chrono::Duration>,
80}
81
82impl Database {
83    pub async fn new(con: &ConString, max_con: u32) -> Result<Self, DbError> {
84        let db_con = init_database(con, max_con)
85            .await
86            .map_err(|e| DbError::InitializationError(e.to_string()))?;
87
88        if operations::no_user_exists(&db_con).await? {
89            operations::insert_admin_credentials(&db_con, con).await?;
90        }
91
92        Ok(Self {
93            db_con,
94            session_age: session_age_to_duration(con.session_age()),
95        })
96    }
97
98    /// Runs `f` in a transaction that is committed if `f` succeeds. If `f`
99    /// fails, the transaction is rolled back right away. A dropped transaction
100    /// is only rolled back later, so on `SQLite` the next write could still
101    /// find the database locked.
102    async fn in_transaction<T>(
103        &self,
104        f: impl AsyncFnOnce(&DatabaseTransaction) -> DbResult<T>,
105    ) -> DbResult<T> {
106        let txn = self.db_con.begin().await?;
107        match f(&txn).await {
108            Ok(value) => {
109                txn.commit().await?;
110                Ok(value)
111            }
112            Err(e) => {
113                if let Err(rollback) = txn.rollback().await {
114                    warn!("Failed to roll back transaction: {rollback}");
115                }
116                Err(e)
117            }
118        }
119    }
120
121    /// Looks up a user by name; returns the entity model or [`DbError::UserNotFound`].
122    async fn get_user_model(&self, name: &UserName) -> DbResult<user::Model> {
123        user::Entity::find()
124            .filter(user::Column::Name.eq(name))
125            .one(&self.db_con)
126            .await?
127            .ok_or_else(|| DbError::UserNotFound(name.to_string()))
128    }
129
130    /// Looks up a krate by canonical name; returns the entity model or [`DbError::CrateNotFound`].
131    async fn get_krate_model(&self, crate_name: &CanonicalName) -> DbResult<krate::Model> {
132        krate::Entity::find()
133            .filter(krate::Column::Name.eq(crate_name))
134            .one(&self.db_con)
135            .await?
136            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))
137    }
138
139    /// Looks up a group by name; returns the entity model or [`DbError::GroupNotFound`].
140    async fn get_group_model(&self, name: &GroupName) -> DbResult<group::Model> {
141        group::Entity::find()
142            .filter(group::Column::Name.eq(name))
143            .one(&self.db_con)
144            .await?
145            .ok_or_else(|| DbError::GroupNotFound(name.to_string()))
146    }
147
148    /// Executes a count query `SELECT COUNT(id_column) FROM table` and returns the count as u64.
149    async fn count<T>(&self, table: T, id_column: T, error: DbError) -> DbResult<u64>
150    where
151        T: Iden + Copy,
152    {
153        let statement = self.db_con.get_database_backend().build(
154            Query::select()
155                .expr_as(Expr::col((table, id_column)).count(), Alias::new("count"))
156                .from(table),
157        );
158        self.count_by_statement(statement, error).await
159    }
160
161    /// Executes `SELECT SUM(column) FROM table` and returns the sum as u64, 0 for an
162    /// empty table. Cast, as PostgreSQL sums a bigint column to a numeric.
163    async fn sum<T>(&self, table: T, column: T, error: DbError) -> DbResult<u64>
164    where
165        T: Iden + Copy + 'static,
166    {
167        let sum = Func::coalesce([Expr::col((table, column)).sum(), Expr::val(0)]);
168        let statement = self.db_con.get_database_backend().build(
169            Query::select()
170                .expr_as(
171                    Func::cast_as(sum, Alias::new("BIGINT")),
172                    Alias::new("count"),
173                )
174                .from(table),
175        );
176        self.count_by_statement(statement, error).await
177    }
178
179    /// Runs a statement that selects a single `count` column and returns it as u64.
180    async fn count_by_statement(&self, statement: Statement, error: DbError) -> DbResult<u64> {
181        let Some(result) = CountResult::find_by_statement(statement)
182            .one(&self.db_con)
183            .await?
184        else {
185            return Err(error);
186        };
187        result
188            .count
189            .and_then(|v| u64::try_from(v).ok())
190            .ok_or(error)
191    }
192
193    async fn delete_or_not_found<M, A>(&self, model: Option<M>, err: DbError) -> DbResult<()>
194    where
195        M: ModelTrait + sea_orm::IntoActiveModel<A>,
196        A: ActiveModelTrait<Entity = M::Entity> + sea_orm::ActiveModelBehavior + Send,
197    {
198        model.ok_or(err)?.delete(&self.db_con).await?;
199        Ok(())
200    }
201}
202
203// The methods are implemented per domain in the submodules. Their inherent
204// methods take precedence over the trait methods of the same name.
205#[async_trait]
206impl DbProvider for Database {
207    async fn get_total_unique_cached_crates(&self) -> DbResult<u64> {
208        Database::get_total_unique_cached_crates(self).await
209    }
210
211    async fn get_total_cached_crate_versions(&self) -> DbResult<u64> {
212        Database::get_total_cached_crate_versions(self).await
213    }
214
215    async fn get_total_cached_downloads(&self) -> DbResult<u64> {
216        Database::get_total_cached_downloads(self).await
217    }
218
219    async fn authenticate_user(&self, name: &UserName, pwd: &str) -> DbResult<User> {
220        Database::authenticate_user(self, name, pwd).await
221    }
222
223    async fn increase_download_counter(
224        &self,
225        crate_name: &CanonicalName,
226        crate_version: &Version,
227    ) -> DbResult<()> {
228        Database::increase_download_counter(self, crate_name, crate_version).await
229    }
230
231    async fn increase_cached_download_counter(
232        &self,
233        crate_name: &CanonicalName,
234        crate_version: &Version,
235    ) -> DbResult<()> {
236        Database::increase_cached_download_counter(self, crate_name, crate_version).await
237    }
238
239    async fn increase_download_counter_by(
240        &self,
241        crate_name: &CanonicalName,
242        crate_version: &Version,
243        count: u64,
244    ) -> DbResult<()> {
245        Database::increase_download_counter_by(self, crate_name, crate_version, count).await
246    }
247
248    async fn increase_cached_download_counter_by(
249        &self,
250        crate_name: &CanonicalName,
251        crate_version: &Version,
252        count: u64,
253    ) -> DbResult<()> {
254        Database::increase_cached_download_counter_by(self, crate_name, crate_version, count).await
255    }
256
257    async fn get_last_updated_crate(
258        &self,
259        visibility: &CrateVisibility,
260    ) -> DbResult<Option<(OriginalName, Version)>> {
261        Database::get_last_updated_crate(self, visibility).await
262    }
263
264    async fn validate_session(&self, session_token: &PlainToken) -> DbResult<SessionInfo> {
265        Database::validate_session(self, session_token).await
266    }
267
268    async fn delete_expired_sessions(&self) -> DbResult<u64> {
269        Database::delete_expired_sessions(self).await
270    }
271
272    async fn add_session_token(&self, name: &UserName, session_token: &PlainToken) -> DbResult<()> {
273        Database::add_session_token(self, name, session_token).await
274    }
275
276    async fn delete_user_sessions(&self, user_name: &UserName) -> DbResult<u64> {
277        Database::delete_user_sessions(self, user_name).await
278    }
279
280    async fn delete_other_user_sessions(
281        &self,
282        user_name: &UserName,
283        keep_session_token: &PlainToken,
284    ) -> DbResult<u64> {
285        Database::delete_other_user_sessions(self, user_name, keep_session_token).await
286    }
287
288    async fn add_crate_user(&self, crate_name: &CanonicalName, user: &UserName) -> DbResult<()> {
289        Database::add_crate_user(self, crate_name, user).await
290    }
291
292    async fn add_crate_group(&self, crate_name: &CanonicalName, group: &GroupName) -> DbResult<()> {
293        Database::add_crate_group(self, crate_name, group).await
294    }
295
296    async fn add_group_user(&self, group_name: &GroupName, user: &UserName) -> DbResult<()> {
297        Database::add_group_user(self, group_name, user).await
298    }
299
300    async fn add_owner(&self, crate_name: &CanonicalName, owner: &UserName) -> DbResult<()> {
301        Database::add_owner(self, crate_name, owner).await
302    }
303
304    async fn is_download_restricted(&self, crate_name: &CanonicalName) -> DbResult<bool> {
305        Database::is_download_restricted(self, crate_name).await
306    }
307
308    async fn change_download_restricted(
309        &self,
310        crate_name: &CanonicalName,
311        restricted: bool,
312    ) -> DbResult<()> {
313        Database::change_download_restricted(self, crate_name, restricted).await
314    }
315
316    async fn is_crate_user(&self, crate_name: &CanonicalName, user: &UserName) -> DbResult<bool> {
317        Database::is_crate_user(self, crate_name, user).await
318    }
319
320    async fn is_crate_group(
321        &self,
322        crate_name: &CanonicalName,
323        group: &GroupName,
324    ) -> DbResult<bool> {
325        Database::is_crate_group(self, crate_name, group).await
326    }
327
328    async fn is_crate_group_user(
329        &self,
330        crate_name: &CanonicalName,
331        user: &UserName,
332    ) -> DbResult<bool> {
333        Database::is_crate_group_user(self, crate_name, user).await
334    }
335
336    async fn is_group_user(&self, group_name: &GroupName, user: &UserName) -> DbResult<bool> {
337        Database::is_group_user(self, group_name, user).await
338    }
339
340    async fn is_owner(&self, crate_name: &CanonicalName, user: &UserName) -> DbResult<bool> {
341        Database::is_owner(self, crate_name, user).await
342    }
343
344    async fn get_crate_id(&self, crate_name: &CanonicalName) -> DbResult<Option<CrateId>> {
345        Database::get_crate_id(self, crate_name).await
346    }
347
348    async fn get_crate_owners(&self, crate_name: &CanonicalName) -> DbResult<Vec<User>> {
349        Database::get_crate_owners(self, crate_name).await
350    }
351
352    async fn get_crate_users(&self, crate_name: &CanonicalName) -> DbResult<Vec<User>> {
353        Database::get_crate_users(self, crate_name).await
354    }
355
356    async fn get_crate_groups(&self, crate_name: &CanonicalName) -> DbResult<Vec<Group>> {
357        Database::get_crate_groups(self, crate_name).await
358    }
359
360    async fn get_group_users(&self, group_name: &GroupName) -> DbResult<Vec<User>> {
361        Database::get_group_users(self, group_name).await
362    }
363
364    async fn get_crate_versions(&self, crate_name: &CanonicalName) -> DbResult<Vec<Version>> {
365        Database::get_crate_versions(self, crate_name).await
366    }
367
368    async fn delete_session_token(&self, session_token: &PlainToken) -> DbResult<()> {
369        Database::delete_session_token(self, session_token).await
370    }
371
372    async fn delete_user(&self, user_name: &UserName) -> DbResult<()> {
373        Database::delete_user(self, user_name).await
374    }
375
376    async fn delete_group(&self, group_name: &GroupName) -> DbResult<()> {
377        Database::delete_group(self, group_name).await
378    }
379
380    async fn change_pwd(&self, user_name: &UserName, new_pwd: &str) -> DbResult<()> {
381        Database::change_pwd(self, user_name, new_pwd).await
382    }
383
384    async fn change_read_only_state(&self, user_name: &UserName, state: bool) -> DbResult<()> {
385        Database::change_read_only_state(self, user_name, state).await
386    }
387
388    async fn change_admin_state(&self, user_name: &UserName, state: bool) -> DbResult<()> {
389        Database::change_admin_state(self, user_name, state).await
390    }
391
392    async fn crate_version_exists(&self, crate_id: CrateId, version: &Version) -> DbResult<bool> {
393        Database::crate_version_exists(self, crate_id, version).await
394    }
395
396    async fn get_max_version_from_id(&self, crate_id: CrateId) -> DbResult<Version> {
397        Database::get_max_version_from_id(self, crate_id).await
398    }
399
400    async fn get_max_version_from_name(&self, crate_name: &CanonicalName) -> DbResult<Version> {
401        Database::get_max_version_from_name(self, crate_name).await
402    }
403
404    async fn add_auth_token(
405        &self,
406        name: &str,
407        token: &PlainToken,
408        user: &UserName,
409    ) -> DbResult<()> {
410        Database::add_auth_token(self, name, token, user).await
411    }
412
413    async fn get_user_from_token(&self, token: &PlainToken) -> DbResult<User> {
414        Database::get_user_from_token(self, token).await
415    }
416
417    async fn get_user(&self, name: &UserName) -> DbResult<User> {
418        Database::get_user(self, name).await
419    }
420
421    async fn get_auth_token_name(&self, token: &PlainToken) -> DbResult<Option<String>> {
422        Database::get_auth_token_name(self, token).await
423    }
424
425    async fn get_auth_tokens(&self, user_name: &UserName) -> DbResult<Vec<AuthToken>> {
426        Database::get_auth_tokens(self, user_name).await
427    }
428
429    async fn delete_auth_token(&self, user_name: &UserName, id: AuthTokenId) -> DbResult<()> {
430        Database::delete_auth_token(self, user_name, id).await
431    }
432
433    async fn delete_owners(
434        &self,
435        crate_name: &CanonicalName,
436        owners: &[UserName],
437        allow_ownerless: bool,
438    ) -> DbResult<()> {
439        Database::delete_owners(self, crate_name, owners, allow_ownerless).await
440    }
441
442    async fn delete_crate_user(&self, crate_name: &CanonicalName, user: &UserName) -> DbResult<()> {
443        Database::delete_crate_user(self, crate_name, user).await
444    }
445
446    async fn delete_crate_group(
447        &self,
448        crate_name: &CanonicalName,
449        group: &GroupName,
450    ) -> DbResult<()> {
451        Database::delete_crate_group(self, crate_name, group).await
452    }
453
454    async fn delete_group_user(&self, group_name: &GroupName, user: &UserName) -> DbResult<()> {
455        Database::delete_group_user(self, group_name, user).await
456    }
457
458    async fn add_user(&self, name: &UserName, pwd: &str, flags: UserFlags) -> DbResult<()> {
459        Database::add_user(self, name, pwd, flags).await
460    }
461
462    async fn add_group(&self, name: &GroupName) -> DbResult<()> {
463        Database::add_group(self, name).await
464    }
465
466    async fn get_users(&self) -> DbResult<Vec<User>> {
467        Database::get_users(self).await
468    }
469
470    async fn get_groups(&self) -> DbResult<Vec<Group>> {
471        Database::get_groups(self).await
472    }
473
474    async fn get_total_unique_crates(&self) -> DbResult<u64> {
475        Database::get_total_unique_crates(self).await
476    }
477
478    async fn get_total_crate_versions(&self) -> DbResult<u64> {
479        Database::get_total_crate_versions(self).await
480    }
481
482    async fn get_total_downloads(&self) -> DbResult<u64> {
483        Database::get_total_downloads(self).await
484    }
485
486    async fn get_top_crates_downloads(
487        &self,
488        top: u64,
489        visibility: &CrateVisibility,
490    ) -> DbResult<Vec<(String, u64)>> {
491        Database::get_top_crates_downloads(self, top, visibility).await
492    }
493
494    async fn add_doc_queue(
495        &self,
496        krate: &OriginalName,
497        version: &Version,
498        path: &Path,
499    ) -> DbResult<()> {
500        Database::add_doc_queue(self, krate, version, path).await
501    }
502
503    async fn delete_doc_queue(&self, id: i64) -> DbResult<()> {
504        Database::delete_doc_queue(self, id).await
505    }
506
507    async fn get_doc_queue(&self) -> DbResult<Vec<DocQueueEntry>> {
508        Database::get_doc_queue(self).await
509    }
510
511    async fn delete_crate(&self, krate: &CanonicalName, version: &Version) -> DbResult<()> {
512        Database::delete_crate(self, krate, version).await
513    }
514
515    async fn get_crate_meta_list(&self, crate_name: &CanonicalName) -> DbResult<Vec<CrateMeta>> {
516        Database::get_crate_meta_list(self, crate_name).await
517    }
518
519    async fn search_in_crate_name_and_description(
520        &self,
521        contains: &str,
522        limit: u64,
523        offset: u64,
524        cache: bool,
525        visibility: &CrateVisibility,
526    ) -> DbResult<Vec<CrateOverview>> {
527        Database::search_in_crate_name_and_description(
528            self, contains, limit, offset, cache, visibility,
529        )
530        .await
531    }
532
533    async fn count_by_crate_name_and_description(
534        &self,
535        contains: &str,
536        cache: bool,
537        visibility: &CrateVisibility,
538    ) -> DbResult<u64> {
539        Database::count_by_crate_name_and_description(self, contains, cache, visibility).await
540    }
541
542    async fn get_crate_overview_list(
543        &self,
544        limit: u64,
545        offset: u64,
546        cache: bool,
547        visibility: &CrateVisibility,
548        sort: CrateSort,
549        tags: &CrateTagFilter,
550    ) -> DbResult<Vec<CrateOverview>> {
551        Database::get_crate_overview_list(self, limit, offset, cache, visibility, sort, tags).await
552    }
553
554    async fn count_crate_overview_list(
555        &self,
556        cache: bool,
557        visibility: &CrateVisibility,
558        tags: &CrateTagFilter,
559    ) -> DbResult<u64> {
560        Database::count_crate_overview_list(self, cache, visibility, tags).await
561    }
562
563    async fn get_crate_data(&self, crate_name: &CanonicalName) -> DbResult<CrateData> {
564        Database::get_crate_data(self, crate_name).await
565    }
566
567    async fn get_crate_dependents(&self, crate_name: &CanonicalName) -> DbResult<CrateDependents> {
568        Database::get_crate_dependents(self, crate_name).await
569    }
570
571    async fn add_empty_crate(&self, name: &str, created: &DateTime<Utc>) -> DbResult<CrateId> {
572        Database::add_empty_crate(self, name, created).await
573    }
574
575    async fn add_crate(
576        &self,
577        pub_metadata: &PublishMetadata,
578        cksum: &str,
579        size: i64,
580        created: &DateTime<Utc>,
581        owner: &UserName,
582        token_name: Option<String>,
583    ) -> DbResult<CrateId> {
584        Database::add_crate(self, pub_metadata, cksum, size, created, owner, token_name).await
585    }
586
587    async fn update_docs_link(
588        &self,
589        crate_name: &CanonicalName,
590        version: &Version,
591        docs_link: &str,
592    ) -> DbResult<()> {
593        Database::update_docs_link(self, crate_name, version, docs_link).await
594    }
595
596    async fn get_prefetch_data(&self, crate_name: &OriginalName) -> DbResult<Prefetch> {
597        Database::get_prefetch_data(self, crate_name).await
598    }
599
600    async fn is_cratesio_cache_up_to_date(
601        &self,
602        crate_name: &OriginalName,
603        etag: Option<String>,
604        last_modified: Option<String>,
605    ) -> DbResult<PrefetchState> {
606        Database::is_cratesio_cache_up_to_date(self, crate_name, etag, last_modified).await
607    }
608
609    async fn add_cratesio_prefetch_data(
610        &self,
611        crate_name: &OriginalName,
612        etag: &str,
613        last_modified: &str,
614        description: Option<String>,
615        indices: &[IndexMetadata],
616        licenses: &HashMap<String, String>,
617    ) -> DbResult<Prefetch> {
618        Database::add_cratesio_prefetch_data(
619            self,
620            crate_name,
621            etag,
622            last_modified,
623            description,
624            indices,
625            licenses,
626        )
627        .await
628    }
629
630    async fn remove_deleted_cratesio_versions(
631        &self,
632        crate_name: &OriginalName,
633        upstream_versions: &[String],
634    ) -> DbResult<Vec<CachedCratesioVersion>> {
635        Database::remove_deleted_cratesio_versions(self, crate_name, upstream_versions).await
636    }
637
638    async fn delete_cratesio_crate(
639        &self,
640        crate_name: &OriginalName,
641    ) -> DbResult<Vec<CachedCratesioVersion>> {
642        Database::delete_cratesio_crate(self, crate_name).await
643    }
644
645    async fn get_cratesio_index_update_list(&self) -> DbResult<Vec<CratesioPrefetchMsg>> {
646        Database::get_cratesio_index_update_list(self).await
647    }
648
649    async fn get_cratesio_version_meta(
650        &self,
651        crate_name: &CanonicalName,
652        version: &Version,
653    ) -> DbResult<Option<CratesioVersionMeta>> {
654        Database::get_cratesio_version_meta(self, crate_name, version).await
655    }
656
657    async fn get_crate_license(
658        &self,
659        crate_name: &CanonicalName,
660        version: &Version,
661    ) -> DbResult<Option<String>> {
662        Database::get_crate_license(self, crate_name, version).await
663    }
664
665    async fn unyank_crate(&self, crate_name: &CanonicalName, version: &Version) -> DbResult<()> {
666        Database::unyank_crate(self, crate_name, version).await
667    }
668
669    async fn yank_crate(&self, crate_name: &CanonicalName, version: &Version) -> DbResult<()> {
670        Database::yank_crate(self, crate_name, version).await
671    }
672
673    async fn register_webhook(&self, webhook: Webhook) -> DbResult<String> {
674        Database::register_webhook(self, webhook).await
675    }
676
677    async fn delete_webhook(&self, id: &str) -> DbResult<()> {
678        Database::delete_webhook(self, id).await
679    }
680
681    async fn get_webhook(&self, id: &str) -> DbResult<Webhook> {
682        Database::get_webhook(self, id).await
683    }
684
685    async fn get_all_webhooks(&self) -> DbResult<Vec<Webhook>> {
686        Database::get_all_webhooks(self).await
687    }
688
689    async fn add_webhook_queue(
690        &self,
691        event: WebhookEvent,
692        payload: serde_json::Value,
693    ) -> DbResult<()> {
694        Database::add_webhook_queue(self, event, payload).await
695    }
696
697    async fn get_pending_webhook_queue_entries(
698        &self,
699        timestamp: DateTime<Utc>,
700    ) -> DbResult<Vec<WebhookQueue>> {
701        Database::get_pending_webhook_queue_entries(self, timestamp).await
702    }
703
704    async fn update_webhook_queue(
705        &self,
706        id: &str,
707        last_attempt: DateTime<Utc>,
708        next_attempt: DateTime<Utc>,
709    ) -> DbResult<()> {
710        Database::update_webhook_queue(self, id, last_attempt, next_attempt).await
711    }
712
713    async fn delete_webhook_queue(&self, id: &str) -> DbResult<()> {
714        Database::delete_webhook_queue(self, id).await
715    }
716
717    async fn get_user_by_oauth2_identity(
718        &self,
719        issuer: &str,
720        subject: &str,
721    ) -> DbResult<Option<User>> {
722        Database::get_user_by_oauth2_identity(self, issuer, subject).await
723    }
724
725    async fn has_oauth2_admin(&self) -> DbResult<bool> {
726        Database::has_oauth2_admin(self).await
727    }
728
729    async fn create_oauth2_user(
730        &self,
731        username: &UserName,
732        issuer: &str,
733        subject: &str,
734        email: Option<String>,
735        flags: UserFlags,
736    ) -> DbResult<User> {
737        Database::create_oauth2_user(self, username, issuer, subject, email, flags).await
738    }
739
740    async fn store_oauth2_state(
741        &self,
742        state: &str,
743        pkce_verifier: &str,
744        nonce: &str,
745    ) -> DbResult<()> {
746        Database::store_oauth2_state(self, state, pkce_verifier, nonce).await
747    }
748
749    async fn get_and_delete_oauth2_state(&self, state: &str) -> DbResult<Option<OAuth2StateData>> {
750        Database::get_and_delete_oauth2_state(self, state).await
751    }
752
753    async fn cleanup_expired_oauth2_states(&self) -> DbResult<u64> {
754        Database::cleanup_expired_oauth2_states(self).await
755    }
756
757    async fn is_username_available(&self, username: &UserName) -> DbResult<bool> {
758        Database::is_username_available(self, username).await
759    }
760
761    async fn add_toolchain(
762        &self,
763        name: &str,
764        version: &str,
765        date: &str,
766        channel: Option<String>,
767    ) -> DbResult<i64> {
768        Database::add_toolchain(self, name, version, date, channel).await
769    }
770
771    async fn add_toolchain_target(
772        &self,
773        toolchain_id: i64,
774        target: &str,
775        storage_path: &str,
776        hash: &str,
777        size: i64,
778    ) -> DbResult<i64> {
779        Database::add_toolchain_target(self, toolchain_id, target, storage_path, hash, size).await
780    }
781
782    async fn set_target_status(
783        &self,
784        target_id: i64,
785        status: ToolchainTargetStatus,
786    ) -> DbResult<()> {
787        Database::set_target_status(self, target_id, status).await
788    }
789
790    async fn fail_processing_toolchain_targets(&self) -> DbResult<u64> {
791        Database::fail_processing_toolchain_targets(self).await
792    }
793
794    async fn add_toolchain_component(
795        &self,
796        target_id: i64,
797        name: &str,
798        storage_path: &str,
799        hash: &str,
800        size: i64,
801    ) -> DbResult<()> {
802        Database::add_toolchain_component(self, target_id, name, storage_path, hash, size).await
803    }
804
805    async fn is_toolchain_component_stored_at(&self, storage_path: &str) -> DbResult<bool> {
806        Database::is_toolchain_component_stored_at(self, storage_path).await
807    }
808
809    async fn get_toolchain_by_channel(
810        &self,
811        channel: &str,
812    ) -> DbResult<Option<ToolchainWithTargets>> {
813        Database::get_toolchain_by_channel(self, channel).await
814    }
815
816    async fn get_toolchain_by_version(
817        &self,
818        name: &str,
819        version: &str,
820    ) -> DbResult<Option<ToolchainWithTargets>> {
821        Database::get_toolchain_by_version(self, name, version).await
822    }
823
824    async fn list_toolchains(&self) -> DbResult<Vec<ToolchainWithTargets>> {
825        Database::list_toolchains(self).await
826    }
827
828    async fn delete_toolchain(&self, name: &str, version: &str) -> DbResult<()> {
829        Database::delete_toolchain(self, name, version).await
830    }
831
832    async fn delete_toolchain_target(
833        &self,
834        name: &str,
835        version: &str,
836        target: &str,
837    ) -> DbResult<()> {
838        Database::delete_toolchain_target(self, name, version, target).await
839    }
840
841    async fn set_channel(&self, channel: &str, name: &str, version: &str) -> DbResult<()> {
842        Database::set_channel(self, channel, name, version).await
843    }
844
845    async fn get_channels(&self) -> DbResult<Vec<ChannelInfo>> {
846        Database::get_channels(self).await
847    }
848
849    async fn get_crate_original_name(
850        &self,
851        crate_name: &CanonicalName,
852    ) -> DbResult<Option<OriginalName>> {
853        Database::get_crate_original_name(self, crate_name).await
854    }
855
856    async fn get_original_name(
857        &self,
858        crate_name: &CanonicalName,
859        version: &Version,
860    ) -> DbResult<Option<OriginalName>> {
861        Database::get_original_name(self, crate_name, version).await
862    }
863}
864
865/// Converts a count to the type of the database columns, saturating as no
866/// realistic count gets that large.
867fn saturating_i64(count: u64) -> i64 {
868    i64::try_from(count).unwrap_or(i64::MAX)
869}
870
871/// The version if it is a valid semantic version. Invalid versions are logged,
872/// so callers can skip them instead of failing on the whole crate.
873fn valid_version(value: &str, crate_name: &str) -> Option<Version> {
874    Version::try_from(value)
875        .inspect_err(|_| warn!("Skipping invalid version {value} of crate {crate_name}"))
876        .ok()
877}
878
879fn parse_db_version(value: &str) -> DbResult<Version> {
880    Version::try_from(value).map_err(|_| DbError::InvalidVersion(value.to_owned()))
881}
882
883#[cfg(test)]
884mod session_age_tests {
885    use kellnr_settings::constants::MAX_SESSION_AGE;
886
887    use super::{SessionAge, session_age_to_duration};
888
889    #[test]
890    fn zero_means_never_expire() {
891        assert_eq!(session_age_to_duration(SessionAge::from_secs(0)), None);
892    }
893
894    #[test]
895    fn positive_age_is_preserved() {
896        assert_eq!(
897            session_age_to_duration(SessionAge::from_secs(3600)),
898            Some(chrono::Duration::seconds(3600))
899        );
900    }
901
902    #[test]
903    fn absurd_age_is_clamped_without_panicking() {
904        // u64::MAX seconds would overflow chrono::Duration if not clamped.
905        let resolved = session_age_to_duration(SessionAge::from_secs(u64::MAX))
906            .expect("non-zero age yields Some");
907        assert_eq!(
908            resolved,
909            chrono::Duration::from_std(MAX_SESSION_AGE).unwrap()
910        );
911    }
912}