Skip to content

Commit 5d0ae76

Browse files
authored
Merge pull request #10563 from Turbo87/model-krate-deps
models/krate: Move out code unrelated to database interaction
2 parents a5694a8 + 04c34f6 commit 5d0ae76

File tree

4 files changed

+128
-91
lines changed

4 files changed

+128
-91
lines changed

src/controllers/krate/metadata.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::models::{
1111
Version, VersionOwnerAction,
1212
};
1313
use crate::schema::*;
14-
use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError};
14+
use crate::util::errors::{
15+
bad_request, crate_not_found, version_not_found, AppResult, BoxedAppError,
16+
};
1517
use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion};
1618
use axum::extract::{FromRequestParts, Query};
1719
use axum_extra::json;
@@ -123,6 +125,8 @@ pub async fn find_crate(
123125
.filter(|_| include.default_version && !include.versions)
124126
{
125127
let version = krate.find_version(&mut conn, default_version).await?;
128+
let version = version.ok_or_else(|| version_not_found(&krate.name, default_version))?;
129+
126130
let (actions, published_by) = tokio::try_join!(
127131
VersionOwnerAction::by_version(&mut conn, &version),
128132
version.published_by(&mut conn),

src/controllers/krate/owners.rs

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
//! All routes related to managing owners of a crate
22
33
use crate::controllers::krate::CratePath;
4-
use crate::models::{krate::NewOwnerInvite, token::EndpointScope};
4+
use crate::models::krate::OwnerRemoveError;
5+
use crate::models::{
6+
krate::NewOwnerInvite, token::EndpointScope, CrateOwner, NewCrateOwnerInvitation,
7+
NewCrateOwnerInvitationOutcome, OwnerKind,
8+
};
59
use crate::models::{Crate, Owner, Rights, Team, User};
6-
use crate::util::errors::{bad_request, crate_not_found, custom, AppResult};
10+
use crate::util::errors::{bad_request, crate_not_found, custom, AppResult, BoxedAppError};
711
use crate::views::EncodableOwner;
8-
use crate::{app::AppState, models::krate::OwnerAddError};
12+
use crate::{app::AppState, App};
913
use crate::{auth::AuthCheck, email::Email};
1014
use axum::Json;
1115
use axum_extra::json;
1216
use axum_extra::response::ErasedJson;
17+
use chrono::Utc;
18+
use crates_io_database::schema::crate_owners;
1319
use diesel::prelude::*;
1420
use diesel_async::scoped_futures::ScopedFutureExt;
15-
use diesel_async::{AsyncConnection, RunQueryDsl};
21+
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
1622
use http::request::Parts;
1723
use http::StatusCode;
1824
use secrecy::{ExposeSecret, SecretString};
25+
use thiserror::Error;
1926

2027
/// List crate owners.
2128
#[utoipa::path(
@@ -199,7 +206,7 @@ async fn modify_owners(
199206
return Err(bad_request(format_args!("`{login}` is already an owner")));
200207
}
201208

202-
match krate.owner_add(&app, conn, user, login).await {
209+
match add_owner(&app, conn, user, &krate, login).await {
203210
// A user was successfully invited, and they must accept
204211
// the invite, and a best-effort attempt should be made
205212
// to email them the invite token for one-click
@@ -274,6 +281,96 @@ async fn modify_owners(
274281
Ok(json!({ "msg": comma_sep_msg, "ok": true }))
275282
}
276283

284+
/// Invite `login` as an owner of this crate, returning the created
285+
/// [`NewOwnerInvite`].
286+
async fn add_owner(
287+
app: &App,
288+
conn: &mut AsyncPgConnection,
289+
req_user: &User,
290+
krate: &Crate,
291+
login: &str,
292+
) -> Result<NewOwnerInvite, OwnerAddError> {
293+
use diesel::insert_into;
294+
295+
let owner = Owner::find_or_create_by_login(app, conn, req_user, login).await?;
296+
match owner {
297+
// Users are invited and must accept before being added
298+
Owner::User(user) => {
299+
let expires_at = Utc::now() + app.config.ownership_invitations_expiration;
300+
let invite = NewCrateOwnerInvitation {
301+
invited_user_id: user.id,
302+
invited_by_user_id: req_user.id,
303+
crate_id: krate.id,
304+
expires_at,
305+
};
306+
307+
let creation_ret = invite.create(conn).await.map_err(BoxedAppError::from)?;
308+
309+
match creation_ret {
310+
NewCrateOwnerInvitationOutcome::InviteCreated { plaintext_token } => {
311+
Ok(NewOwnerInvite::User(user, plaintext_token))
312+
}
313+
NewCrateOwnerInvitationOutcome::AlreadyExists => {
314+
Err(OwnerAddError::AlreadyInvited(Box::new(user)))
315+
}
316+
}
317+
}
318+
// Teams are added as owners immediately
319+
Owner::Team(team) => {
320+
insert_into(crate_owners::table)
321+
.values(&CrateOwner {
322+
crate_id: krate.id,
323+
owner_id: team.id,
324+
created_by: req_user.id,
325+
owner_kind: OwnerKind::Team,
326+
email_notifications: true,
327+
})
328+
.on_conflict(crate_owners::table.primary_key())
329+
.do_update()
330+
.set(crate_owners::deleted.eq(false))
331+
.execute(conn)
332+
.await
333+
.map_err(BoxedAppError::from)?;
334+
335+
Ok(NewOwnerInvite::Team(team))
336+
}
337+
}
338+
}
339+
340+
/// Error results from a [`add_owner()`] model call.
341+
#[derive(Debug, Error)]
342+
enum OwnerAddError {
343+
/// An opaque [`BoxedAppError`].
344+
#[error("{0}")] // AppError does not impl Error
345+
AppError(BoxedAppError),
346+
347+
/// The requested invitee already has a pending invite.
348+
///
349+
/// Note: Teams are always immediately added, so they cannot have a pending
350+
/// invite to cause this error.
351+
#[error("user already has pending invite")]
352+
AlreadyInvited(Box<User>),
353+
}
354+
355+
/// A [`BoxedAppError`] does not impl [`std::error::Error`] so it needs a manual
356+
/// [`From`] impl.
357+
impl From<BoxedAppError> for OwnerAddError {
358+
fn from(value: BoxedAppError) -> Self {
359+
Self::AppError(value)
360+
}
361+
}
362+
363+
impl From<OwnerRemoveError> for BoxedAppError {
364+
fn from(error: OwnerRemoveError) -> Self {
365+
match error {
366+
OwnerRemoveError::Diesel(error) => error.into(),
367+
OwnerRemoveError::NotFound { login } => {
368+
bad_request(format!("could not find owner with login `{login}`"))
369+
}
370+
}
371+
}
372+
}
373+
277374
pub struct OwnerInviteEmail {
278375
/// The destination email address for this email.
279376
recipient_email_address: String,

src/controllers/version.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use utoipa::IntoParams;
1414

1515
use crate::controllers::krate::load_crate;
1616
use crate::models::{Crate, Version};
17-
use crate::util::errors::AppResult;
17+
use crate::util::errors::{version_not_found, AppResult};
1818

1919
#[derive(Deserialize, FromRequestParts, IntoParams)]
2020
#[into_params(parameter_in = Path)]
@@ -47,7 +47,10 @@ async fn version_and_crate(
4747
semver: &str,
4848
) -> AppResult<(Version, Crate)> {
4949
let krate = load_crate(conn, crate_name).await?;
50-
let version = krate.find_version(conn, semver).await?;
50+
let version = krate
51+
.find_version(conn, semver)
52+
.await?
53+
.ok_or_else(|| version_not_found(crate_name, semver))?;
5154

5255
Ok((version, krate))
5356
}

src/models/krate.rs

Lines changed: 16 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use chrono::{NaiveDateTime, Utc};
1+
use chrono::NaiveDateTime;
22
use diesel::associations::Identifiable;
33
use diesel::dsl;
44
use diesel::pg::Pg;
@@ -11,13 +11,8 @@ use thiserror::Error;
1111

1212
use crate::models::helpers::with_count::*;
1313
use crate::models::version::TopVersions;
14-
use crate::models::{
15-
CrateOwner, NewCrateOwnerInvitation, NewCrateOwnerInvitationOutcome, Owner, OwnerKind,
16-
ReverseDependency, User, Version,
17-
};
14+
use crate::models::{CrateOwner, Owner, OwnerKind, ReverseDependency, User, Version};
1815
use crate::schema::*;
19-
use crate::util::errors::{bad_request, version_not_found, AppResult};
20-
use crate::{app::App, util::errors::BoxedAppError};
2116
use crates_io_diesel_helpers::canon_crate_name;
2217

2318
use super::Team;
@@ -208,14 +203,13 @@ impl Crate {
208203
&self,
209204
conn: &mut AsyncPgConnection,
210205
version: &str,
211-
) -> AppResult<Version> {
206+
) -> QueryResult<Option<Version>> {
212207
Version::belonging_to(self)
213208
.filter(versions::num.eq(version))
214209
.select(Version::as_select())
215210
.first(conn)
216211
.await
217-
.optional()?
218-
.ok_or_else(|| version_not_found(&self.name, version))
212+
.optional()
219213
}
220214

221215
// Validates the name is a valid crate name.
@@ -385,63 +379,11 @@ impl Crate {
385379
Ok(users.chain(teams).collect())
386380
}
387381

388-
/// Invite `login` as an owner of this crate, returning the created
389-
/// [`NewOwnerInvite`].
390-
pub async fn owner_add(
382+
pub async fn owner_remove(
391383
&self,
392-
app: &App,
393384
conn: &mut AsyncPgConnection,
394-
req_user: &User,
395385
login: &str,
396-
) -> Result<NewOwnerInvite, OwnerAddError> {
397-
use diesel::insert_into;
398-
399-
let owner = Owner::find_or_create_by_login(app, conn, req_user, login).await?;
400-
match owner {
401-
// Users are invited and must accept before being added
402-
Owner::User(user) => {
403-
let expires_at = Utc::now() + app.config.ownership_invitations_expiration;
404-
let invite = NewCrateOwnerInvitation {
405-
invited_user_id: user.id,
406-
invited_by_user_id: req_user.id,
407-
crate_id: self.id,
408-
expires_at,
409-
};
410-
411-
let creation_ret = invite.create(conn).await.map_err(BoxedAppError::from)?;
412-
413-
match creation_ret {
414-
NewCrateOwnerInvitationOutcome::InviteCreated { plaintext_token } => {
415-
Ok(NewOwnerInvite::User(user, plaintext_token))
416-
}
417-
NewCrateOwnerInvitationOutcome::AlreadyExists => {
418-
Err(OwnerAddError::AlreadyInvited(Box::new(user)))
419-
}
420-
}
421-
}
422-
// Teams are added as owners immediately
423-
Owner::Team(team) => {
424-
insert_into(crate_owners::table)
425-
.values(&CrateOwner {
426-
crate_id: self.id,
427-
owner_id: team.id,
428-
created_by: req_user.id,
429-
owner_kind: OwnerKind::Team,
430-
email_notifications: true,
431-
})
432-
.on_conflict(crate_owners::table.primary_key())
433-
.do_update()
434-
.set(crate_owners::deleted.eq(false))
435-
.execute(conn)
436-
.await
437-
.map_err(BoxedAppError::from)?;
438-
439-
Ok(NewOwnerInvite::Team(team))
440-
}
441-
}
442-
}
443-
444-
pub async fn owner_remove(&self, conn: &mut AsyncPgConnection, login: &str) -> AppResult<()> {
386+
) -> Result<(), OwnerRemoveError> {
445387
let query = diesel::sql_query(
446388
r#"WITH crate_owners_with_login AS (
447389
SELECT
@@ -477,8 +419,7 @@ impl Crate {
477419
.await?;
478420

479421
if num_updated_rows == 0 {
480-
let error = format!("could not find owner with login `{login}`");
481-
return Err(bad_request(error));
422+
return Err(OwnerRemoveError::not_found(login));
482423
}
483424

484425
Ok(())
@@ -518,26 +459,18 @@ pub enum NewOwnerInvite {
518459
Team(Team),
519460
}
520461

521-
/// Error results from a [`Crate::owner_add()`] model call.
522462
#[derive(Debug, Error)]
523-
pub enum OwnerAddError {
524-
/// An opaque [`BoxedAppError`].
525-
#[error("{0}")] // AppError does not impl Error
526-
AppError(BoxedAppError),
527-
528-
/// The requested invitee already has a pending invite.
529-
///
530-
/// Note: Teams are always immediately added, so they cannot have a pending
531-
/// invite to cause this error.
532-
#[error("user already has pending invite")]
533-
AlreadyInvited(Box<User>),
463+
pub enum OwnerRemoveError {
464+
#[error(transparent)]
465+
Diesel(#[from] diesel::result::Error),
466+
#[error("Could not find owner with login `{login}`")]
467+
NotFound { login: String },
534468
}
535469

536-
/// A [`BoxedAppError`] does not impl [`std::error::Error`] so it needs a manual
537-
/// [`From`] impl.
538-
impl From<BoxedAppError> for OwnerAddError {
539-
fn from(value: BoxedAppError) -> Self {
540-
Self::AppError(value)
470+
impl OwnerRemoveError {
471+
pub fn not_found(login: &str) -> Self {
472+
let login = login.to_string();
473+
Self::NotFound { login }
541474
}
542475
}
543476

0 commit comments

Comments
 (0)