Skip to content

Commit f6ebfe0

Browse files
committed
Allow emails table to contain multiple emails per user
1 parent 067c12e commit f6ebfe0

31 files changed

+761
-67
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"rust-analyzer.check.command": "check"
3+
}

crates/crates_io_database/src/models/email.rs

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub struct Email {
1313
pub user_id: i32,
1414
pub email: String,
1515
pub verified: bool,
16+
pub primary: bool,
1617
#[diesel(deserialize_as = String, serialize_as = String)]
1718
pub token: SecretString,
1819
}
@@ -24,46 +25,65 @@ pub struct NewEmail<'a> {
2425
pub email: &'a str,
2526
#[builder(default = false)]
2627
pub verified: bool,
28+
#[builder(default = false)]
29+
pub primary: bool,
2730
}
2831

2932
impl NewEmail<'_> {
30-
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<()> {
33+
pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<Email> {
3134
diesel::insert_into(emails::table)
3235
.values(self)
33-
.execute(conn)
34-
.await?;
35-
36-
Ok(())
36+
.returning(Email::as_returning())
37+
.get_result(conn)
38+
.await
3739
}
3840

39-
/// Inserts the email into the database and returns the confirmation token,
40-
/// or does nothing if it already exists and returns `None`.
41-
pub async fn insert_if_missing(
41+
/// Inserts the email into the database and returns it, unless the user already has a
42+
/// primary email, in which case it will do nothing and return `None`.
43+
pub async fn insert_primary_if_missing(
4244
&self,
4345
conn: &mut AsyncPgConnection,
44-
) -> QueryResult<Option<SecretString>> {
45-
diesel::insert_into(emails::table)
46-
.values(self)
47-
.on_conflict_do_nothing()
48-
.returning(emails::token)
49-
.get_result::<String>(conn)
50-
.await
51-
.map(Into::into)
52-
.optional()
46+
) -> QueryResult<Option<Email>> {
47+
// Check if the user already has a primary email
48+
let primary_count = emails::table
49+
.filter(emails::user_id.eq(self.user_id))
50+
.filter(emails::primary.eq(true))
51+
.count()
52+
.get_result::<i64>(conn)
53+
.await?;
54+
55+
if primary_count > 0 {
56+
return Ok(None); // User already has a primary email
57+
}
58+
59+
self.insert(conn).await.map(Some)
5360
}
5461

55-
pub async fn insert_or_update(
62+
// Inserts an email for the user, replacing the primary email if it exists.
63+
pub async fn insert_or_update_primary(
5664
&self,
5765
conn: &mut AsyncPgConnection,
58-
) -> QueryResult<SecretString> {
59-
diesel::insert_into(emails::table)
60-
.values(self)
61-
.on_conflict(emails::user_id)
62-
.do_update()
63-
.set(self)
64-
.returning(emails::token)
65-
.get_result::<String>(conn)
66-
.await
67-
.map(Into::into)
66+
) -> QueryResult<Email> {
67+
// Attempt to update an existing primary email
68+
let updated_email = diesel::update(
69+
emails::table
70+
.filter(emails::user_id.eq(self.user_id))
71+
.filter(emails::primary.eq(true)),
72+
)
73+
.set((
74+
emails::email.eq(self.email),
75+
emails::verified.eq(self.verified),
76+
))
77+
.returning(Email::as_returning())
78+
.get_result(conn)
79+
.await
80+
.optional()?;
81+
82+
if let Some(email) = updated_email {
83+
Ok(email)
84+
} else {
85+
// Otherwise, insert a new email
86+
self.insert(conn).await
87+
}
6888
}
6989
}

crates/crates_io_database/src/models/user.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ impl User {
6161
Ok(users.collect())
6262
}
6363

64-
/// Queries the database for the verified emails
65-
/// belonging to a given user
64+
/// Queries the database for a verified email address belonging to the user.
65+
/// It will ideally return the primary email address if it exists and is
66+
/// verified, otherwise, it will return any verified email address.
6667
pub async fn verified_email(
6768
&self,
6869
conn: &mut AsyncPgConnection,
6970
) -> QueryResult<Option<String>> {
7071
Email::belonging_to(self)
7172
.select(emails::email)
7273
.filter(emails::verified.eq(true))
74+
.order(emails::primary.desc())
7375
.first(conn)
7476
.await
7577
.optional()

crates/crates_io_database/src/schema.patch

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@
4545
/// The `target` column of the `dependencies` table.
4646
///
4747
/// Its SQL type is `Nullable<Varchar>`.
48+
@@ -536,13 +536,14 @@
49+
///
50+
/// Its SQL type is `Nullable<Timestamptz>`.
51+
///
52+
/// (Automatically generated by Diesel.)
53+
token_generated_at -> Nullable<Timestamptz>,
54+
/// Whether this email is the primary email address for the user.
55+
- is_primary -> Bool,
56+
+ #[sql_name = "is_primary"]
57+
+ primary -> Bool,
58+
}
59+
}
60+
61+
diesel::table! {
62+
/// Representation of the `follows` table.
63+
///
4864
@@ -710,6 +702,24 @@
4965
}
5066

crates/crates_io_database/src/schema.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ diesel::table! {
538538
///
539539
/// (Automatically generated by Diesel.)
540540
token_generated_at -> Nullable<Timestamptz>,
541+
/// Whether this email is the primary email address for the user.
542+
#[sql_name = "is_primary"]
543+
primary -> Bool,
541544
}
542545
}
543546

0 commit comments

Comments
 (0)