Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: wiring in the application layer #144

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions crates/ratings_new/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ tonic = "0.12.2"
tonic-reflection = "0.12.2"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
ratings = {path = "../ratings"}

[dev-dependencies]
simple_test_case = "1.2.0"

[build-dependencies]
git2 = { version = "0.18.2", default-features = false }
tonic-build = { version = "0.11", features = ["prost"] }
7 changes: 0 additions & 7 deletions crates/ratings_new/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
use dotenvy::dotenv;
use secrecy::SecretString;
use serde::Deserialize;
use tokio::sync::OnceCell;

static CONFIG: OnceCell<Config> = OnceCell::const_new();

/// Configuration for the general app center ratings backend service.
#[derive(Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -35,10 +32,6 @@ impl Config {
envy::prefixed("APP_").from_env::<Config>()
}

pub async fn get() -> envy::Result<&'static Config> {
CONFIG.get_or_try_init(|| async { Config::load() }).await
}

/// Return a [`String`] representing the socket to run the service on
pub fn socket(&self) -> String {
let Config { port, host, .. } = self;
Expand Down
12 changes: 11 additions & 1 deletion crates/ratings_new/src/db/categories.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
use crate::db::Result;
use sqlx::{PgConnection, Postgres, QueryBuilder};

#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, strum::EnumString, strum::Display)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
sqlx::Type,
strum::EnumString,
strum::Display,
strum::FromRepr,
)]
#[repr(i32)]
pub enum Category {
ArtAndDesign = 0,
Expand Down
66 changes: 0 additions & 66 deletions crates/ratings_new/src/db/migrator.rs

This file was deleted.

57 changes: 24 additions & 33 deletions crates/ratings_new/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use thiserror::Error;
use tokio::sync::OnceCell;
use tracing::info;

pub mod categories;
pub mod user;
pub mod vote;
mod categories;
mod user;
mod vote;

mod migrator;
use migrator::Migrator;
pub use categories::{set_categories_for_snap, snap_has_categories, Category};
pub use user::User;
pub use vote::{Timeframe, Vote, VoteSummary};

#[macro_export]
macro_rules! conn {
Expand All @@ -21,27 +22,26 @@ macro_rules! conn {
pub type ClientHash = String;
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can occur when a user votes.
#[derive(Error, Debug)]
pub enum Error {
/// A record could not be created for the user
#[error("failed to create user record")]
FailedToCreateUserRecord,
/// We were unable to delete a user with the given instance ID

#[error("failed to delete user by instance id")]
FailedToDeleteUserRecord,
/// We could not get a vote by a given user

#[error("failed to get user vote")]
FailedToGetUserVote,
/// The user was unable to cast a vote

#[error("failed to cast vote")]
FailedToCastVote,

#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
/// An error that occurred in category updating

#[error(transparent)]
Sqlx(#[from] sqlx::Error),
/// An error that occurred in the configuration

#[error(transparent)]
Envy(#[from] envy::Error),
}
Expand Down Expand Up @@ -94,10 +94,7 @@ mod tests {
.with_env_filter(EnvFilter::from_default_env())
.init();

let test_users = [
user::User::new(client_hash_1),
user::User::new(client_hash_2),
];
let test_users = [client_hash_1, client_hash_2];

let test_votes = [
vote::Vote {
Expand All @@ -118,29 +115,23 @@ mod tests {

let conn = conn!();

for user in test_users.into_iter() {
user.create_or_seen(conn).await?;
for client_hash in test_users.into_iter() {
User::create_or_seen(client_hash, conn).await?;
}

for vote in test_votes.into_iter() {
vote.save_to_db(conn).await?;
}

let votes_client_1 = vote::Vote::get_all_by_client_hash(
String::from(client_hash_1),
Some(String::from(snap_id_1)),
conn,
)
.await
.unwrap();

let votes_client_2 = vote::Vote::get_all_by_client_hash(
String::from(client_hash_2),
Some(String::from(snap_id_2)),
conn,
)
.await
.unwrap();
let votes_client_1 =
vote::Vote::get_all_by_client_hash(client_hash_1, Some(String::from(snap_id_1)), conn)
.await
.unwrap();

let votes_client_2 =
vote::Vote::get_all_by_client_hash(client_hash_2, Some(String::from(snap_id_2)), conn)
.await
.unwrap();

assert_eq!(votes_client_1.len(), 1);
let first_vote = votes_client_1.first().unwrap();
Expand Down
17 changes: 3 additions & 14 deletions crates/ratings_new/src/db/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,8 @@ pub struct User {
}

impl User {
/// Creates a new user from the given [`ClientHash`]
pub fn new(client_hash: &str) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id: -1,
client_hash: client_hash.to_string(),
last_seen: now,
created: now,
}
}

/// Create a [`User`] entry, or note that the user has recently been seen
pub async fn create_or_seen(self, conn: &mut PgConnection) -> Result<Self> {
pub async fn create_or_seen(client_hash: &str, conn: &mut PgConnection) -> Result<Self> {
d-loose marked this conversation as resolved.
Show resolved Hide resolved
let user_with_id = sqlx::query_as(
r#"
INSERT INTO users (client_hash, created, last_seen)
Expand All @@ -38,7 +27,7 @@ impl User {
RETURNING id, client_hash, created, last_seen;
"#,
)
.bind(self.client_hash)
.bind(client_hash)
.fetch_one(conn)
.await
.map_err(|error| {
Expand All @@ -49,7 +38,7 @@ impl User {
Ok(user_with_id)
}

pub async fn delete_by_client_hash(client_hash: String, conn: &mut PgConnection) -> Result<()> {
pub async fn delete_by_client_hash(client_hash: &str, conn: &mut PgConnection) -> Result<()> {
sqlx::query(
r#"
DELETE FROM users
Expand Down
99 changes: 93 additions & 6 deletions crates/ratings_new/src/db/vote.rs
d-loose marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{ClientHash, Error, Result};
use sqlx::{types::time::OffsetDateTime, FromRow, PgConnection};
use crate::db::{categories::Category, ClientHash, Error, Result};
use sqlx::{types::time::OffsetDateTime, FromRow, PgConnection, QueryBuilder};
use tracing::error;

/// A Vote, as submitted by a user
Expand All @@ -19,12 +19,12 @@ pub struct Vote {
pub timestamp: OffsetDateTime,
}

/// Gets votes for a snap with the given ID from a given [`ClientHash`]
///
/// [`ClientHash`]: crate::db::ClientHash
impl Vote {
/// Gets votes for a snap with the given ID from a given [`ClientHash`]
///
/// [`ClientHash`]: crate::db::ClientHash
pub async fn get_all_by_client_hash(
client_hash: String,
client_hash: &str,
snap_id_filter: Option<String>,
conn: &mut PgConnection,
) -> Result<Vec<Vote>> {
Expand Down Expand Up @@ -85,3 +85,90 @@ impl Vote {
Ok(result.rows_affected())
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::FromRepr)]
#[repr(i32)]
pub enum Timeframe {
Unspecified,
Week,
Month,
}

/// A summary of votes for a given snap, this is then aggregated before transfer.
#[derive(Debug, Clone, FromRow)]
pub struct VoteSummary {
/// The ID of the snap being checked.
pub snap_id: String,
/// The total votes this snap has received.
pub total_votes: i64,
/// The number of the votes which are positive.
pub positive_votes: i64,
}

impl VoteSummary {
pub async fn get_by_snap_id(snap_id: &str, conn: &mut PgConnection) -> Result<VoteSummary> {
let result: Option<VoteSummary> = sqlx::query_as(
r#"
SELECT
votes.snap_id,
COUNT(*) AS total_votes,
COUNT(*) FILTER (WHERE votes.vote_up) AS positive_votes
FROM
votes
WHERE
votes.snap_id = $1
GROUP BY votes.snap_id
"#,
)
.bind(snap_id)
.fetch_optional(conn)
.await?;

let summary = result.unwrap_or_else(|| VoteSummary {
snap_id: snap_id.to_string(),
total_votes: 0,
positive_votes: 0,
});

Ok(summary)
}

/// Retrieves the vote summary over a given [Timeframe], optionally for a specific [Category]
pub async fn get_for_timeframe(
timeframe: Timeframe,
category: Option<Category>,
conn: &mut PgConnection,
) -> Result<Vec<VoteSummary>> {
let mut builder = QueryBuilder::new(
r"
SELECT
votes.snap_id,
COUNT(*) AS total_votes,
COUNT(*) FILTER (WHERE votes.vote_up) AS positive_votes
FROM
votes",
);

builder.push(match timeframe {
Timeframe::Week => " WHERE votes.created >= NOW() - INTERVAL '1 week'",
Timeframe::Month => " WHERE votes.created >= NOW() - INTERVAL '1 month'",
Timeframe::Unspecified => "",
});

if let Some(category) = category {
builder
.push(
r"
WHERE votes.snap_id IN (
SELECT snap_categories.snap_id FROM snap_categories
WHERE snap_categories.category = $1)",
)
.push_bind(category);
}

builder.push(" GROUP BY votes.snap_id");
let summaries = builder.build_query_as().fetch_all(conn).await?;

Ok(summaries)
}
}
Loading
Loading