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