Skip to content

Commit

Permalink
288: Add report statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
m-milek committed Dec 21, 2024
1 parent 76c6a63 commit eda66f8
Show file tree
Hide file tree
Showing 32 changed files with 325 additions and 139 deletions.
13 changes: 13 additions & 0 deletions backend/migrations/20241204183100_initial.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,28 @@ CREATE TABLE IF NOT EXISTS reports (
title TEXT NOT NULL,
description TEXT NOT NULL,
is_public BOOLEAN NOT NULL,
is_successful BOOLEAN NOT NULL,
is_in_progress BOOLEAN NOT NULL,
is_error BOOLEAN NOT NULL,
error_message TEXT DEFAULT NULL,
metadata_id uuid NOT NULL UNIQUE,
analyses_map_id uuid NOT NULL UNIQUE,
--
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,

CHECK ((is_error = TRUE AND error_message IS NOT NULL) OR
(is_error = FALSE AND error_message IS NULL)),
CHECK (is_public OR is_error OR is_in_progress),
-- xor checks, only one of those state columns can be true
CHECK ((is_public AND NOT is_error AND NOT is_in_progress) OR
(NOT is_public AND is_error AND NOT is_in_progress) OR
(NOT is_public AND NOT is_error AND is_in_progress)),

FOREIGN KEY (user_id) REFERENCES users (id), -- DO NOT CASCADE
FOREIGN KEY (metadata_id) REFERENCES report_metadata (id) ON DELETE CASCADE,
FOREIGN KEY (analyses_map_id) REFERENCES report_analyses_maps (id) ON DELETE CASCADE

);

CREATE OR REPLACE TRIGGER update_reports_updated_at
Expand Down
8 changes: 6 additions & 2 deletions backend/src/api/report/generate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::report::report_ack::ReportAck;
use crate::app_error::AppError;
use crate::auth::google::{GoogleId, JwtUserInfo};
use crate::auth::google::JwtUserInfo;
use crate::auth::user::GoogleId;
use crate::db::db_stored::DbStored;
use crate::fetcher::feed_request::{FetcherFeedRequest, RedditFeedKind};
use crate::fetcher::fetcher::RMoodsFetcher;
Expand All @@ -10,7 +11,9 @@ use crate::fetcher::model::reddit_data::RedditFeedData;
use crate::fetcher::model::user_posts::UserPosts;
use crate::nlp::analysis::NlpAnalysisKind;
use crate::nlp::nlp_client::NlpClient;
use crate::nlp::report::{new_report_id, Report, ReportAnalysesMap, ReportMetadata};
use crate::report::report::{
new_report_id, Report, ReportAnalysesMap, ReportMetadata, ReportStatus,
};
use crate::websocket::SystemMessage;
use crate::websocket::SystemMessage::ReportError;
use crate::AppState;
Expand Down Expand Up @@ -39,6 +42,7 @@ pub async fn nlp_analysis<T: RedditFeedData>(
title: "RMoods Report".to_string(),
description: "An RMoods report generated from Reddit data.".to_string(),
is_public: true,
status: ReportStatus::Success,
metadata: ReportMetadata {
created_at: Utc::now(),
updated_at: Utc::now(),
Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/report/get.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::app_error::AppError;
use crate::db::impls::report_repository::{ReportQuery, ReportRepository};
use crate::nlp::report::Report;
use crate::report::report::Report;
use crate::AppState;
use axum::extract::State;
use axum::Json;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::app_error::AppError;
use crate::auth::google::User;
use crate::auth::user::User;
use crate::db::db_stored::DbStored;
use crate::AppState;
use axum::extract::{Query, State};
Expand Down
5 changes: 3 additions & 2 deletions backend/src/app_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use serde::Serialize;
use serde_json::json;

use crate::auth::error::AuthError;
use crate::db::db_error::DbError;
use crate::fetcher::fetcher_error::FetcherError;
use crate::fetcher::reddit::error::RedditError;
use crate::nlp::error::NlpError;
Expand Down Expand Up @@ -88,8 +89,8 @@ impl From<NlpError> for AppError {
}
}

impl From<sqlx::Error> for AppError {
fn from(_value: sqlx::Error) -> Self {
impl From<DbError> for AppError {
fn from(_value: DbError) -> Self {
AppError::internal_server_error()
}
}
Expand Down
18 changes: 1 addition & 17 deletions backend/src/auth/google.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::auth::error::AuthError;
use crate::auth::jwt::decode_jwt;
use crate::auth::middleware::jwt_from_header_or_uri;
use crate::auth::user::{GoogleId, User};
use axum::async_trait;
use axum::extract::FromRequestParts;
use derive_getters::Getters;
Expand All @@ -20,23 +21,6 @@ pub struct GoogleTokenResponse {
id_token: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)]
pub struct User {
/// Unique user ID
#[serde(rename = "sub")]
#[sqlx(rename = "google_id")]
pub id: String,
pub name: String,
pub given_name: String,
pub family_name: Option<String>,
/// URL to the user's picture
pub picture: String,
pub email: String,
pub email_verified: bool,
}

pub type GoogleId = String;

#[derive(Serialize, Deserialize, Getters, Clone, Debug)]
pub struct JwtUserInfo {
pub(crate) id: GoogleId,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/auth/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::auth::error::AuthError;
use crate::auth::google::{JwtUserInfo, User};
use crate::auth::google::JwtUserInfo;
use crate::auth::user::User;
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, DecodingKey, Header, TokenData, Validation};
use log_derive::logfn;
Expand Down
1 change: 1 addition & 0 deletions backend/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod error;
pub mod google;
pub mod jwt;
pub mod middleware;
pub(crate) mod user;
18 changes: 18 additions & 0 deletions backend/src/auth/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};

pub type GoogleId = String;

#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)]
pub struct User {
/// Unique user ID
#[serde(rename = "sub")]
#[sqlx(rename = "google_id")]
pub id: GoogleId,
pub name: String,
pub given_name: String,
pub family_name: Option<String>,
/// URL to the user's picture
pub picture: String,
pub email: String,
pub email_verified: bool,
}
9 changes: 9 additions & 0 deletions backend/src/db/db_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::report::report_error::ReportError;
use thiserror::Error;

#[derive(Error, Debug)]
#[error("Database error: {0}")]
pub enum DbError {
SqlxError(#[from] sqlx::Error),
InvalidStateError(#[from] ReportError),
}
51 changes: 28 additions & 23 deletions backend/src/db/db_stored.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::db::db_client::DbClient;
use crate::db::db_error::DbError;
use crate::db::pagination::DbPagination;
use axum::async_trait;
use sqlx::{Error, PgPool, Postgres, Transaction};
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

/// The public-facing trait for database-stored objects.
///
/// It's implemented only for the top-level types, such as the [Report](crate::nlp::report::Report)
/// It's implemented only for the top-level types, such as the [Report](crate::report::report::Report)
/// or [User](crate::auth::user::User) structs.
///
/// The actual implementation is done in the [DbStoredInner](crate::db::db_stored::DbStoredInner) trait.
Expand All @@ -18,69 +19,73 @@ use uuid::Uuid;
pub trait DbStored: DbStoredInner {
/// Saves the object to the database.
/// Rollbacks the transaction should the operation fail.
async fn save(&self, db: &DbClient) -> Result<(), Error> {
async fn save(&self, db: &DbClient) -> Result<(), DbError> {
let mut tx = db.raw_db().begin().await?;
let result = self.inner_save(&mut tx).await;
handle_db_result(result, tx).await
handle_db_result(result, tx).await.map_err(DbError::from)
}
/// Updates the object in the database.
/// Rollbacks the transaction should the operation fail.
async fn update(&self, db: &DbClient) -> Result<(), Error> {
async fn update(&self, db: &DbClient) -> Result<(), DbError> {
let mut tx = db.raw_db().begin().await?;
let result = self.inner_update(&mut tx).await;
handle_db_result(result, tx).await
handle_db_result(result, tx).await.map_err(DbError::from)
}
/// Deletes the object from the database.
/// Rollbacks the transaction should the operation fail.
async fn delete(&self, db: &DbClient) -> Result<(), Error> {
async fn delete(&self, db: &DbClient) -> Result<(), DbError> {
let mut tx = db.raw_db().begin().await?;
let result = self.inner_delete(&mut tx).await;
handle_db_result(result, tx).await
handle_db_result(result, tx).await.map_err(DbError::from)
}
async fn get_by_id(id: &str, db: &DbClient) -> Result<Option<Self>, Error> {
Self::inner_get_by_id(id, db.raw_db()).await
async fn get_by_id(id: &str, db: &DbClient) -> Result<Option<Self>, DbError> {
Self::inner_get_by_id(id, db.raw_db())
.await
.map_err(DbError::from)
}
async fn get_all(pagination: DbPagination, db: &DbClient) -> Result<Vec<Self>, Error> {
Self::inner_get_all(pagination, db.raw_db()).await
async fn get_all(pagination: DbPagination, db: &DbClient) -> Result<Vec<Self>, DbError> {
Self::inner_get_all(pagination, db.raw_db())
.await
.map_err(DbError::from)
}
}
impl<T> DbStored for T where T: DbStoredInner {}

/// Internal trait for database-stored objects.
///
/// As opposed to the [DbStored](crate::db::db_stored::DbStored) trait, this one has concrete implementations
/// for top-level types - [Report](crate::nlp::report::Report) and [User](crate::auth::user::User).
/// for top-level types - [Report](crate::report::report::Report) and [User](crate::auth::user::User).
#[async_trait]
pub(super) trait DbStoredInner: Sized {
/// Saves the object to the database using the provided transaction.
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<(), Error>;
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<(), DbError>;
/// Updates the object in the database using the provided transaction.
async fn inner_update(&self, tx: &mut Transaction<Postgres>) -> Result<(), Error>;
async fn inner_update(&self, tx: &mut Transaction<Postgres>) -> Result<(), DbError>;
/// Deletes the object from the database using the provided transaction.
async fn inner_delete(&self, tx: &mut Transaction<Postgres>) -> Result<(), Error>;
async fn inner_delete(&self, tx: &mut Transaction<Postgres>) -> Result<(), DbError>;

async fn inner_get_by_id(id: &str, pool: &PgPool) -> Result<Option<Self>, Error>;
async fn inner_get_by_id(id: &str, pool: &PgPool) -> Result<Option<Self>, DbError>;

async fn inner_get_all(pagination: DbPagination, pool: &PgPool) -> Result<Vec<Self>, Error>;
async fn inner_get_all(pagination: DbPagination, pool: &PgPool) -> Result<Vec<Self>, DbError>;
}

/// Internal trait for database-stored objects that depend on other objects.
///
/// This is used for objects that are referenced by other objects, such as with [Report](crate::nlp::report::Report) and its [ReportMetadata](crate::nlp::report::ReportMetadata).
/// This is used for objects that are referenced by other objects, such as with [Report](crate::report::report::Report) and its [ReportMetadata](crate::report::report::ReportMetadata).
/// The latter is saved first (using this trait) - and its UUID is then used to save the former (using the [DbStored](crate::db::db_stored::DbStored) trait).
#[async_trait]
pub(super) trait DbStoredDependentlyInner: Sized {
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, Error>;
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, DbError>;
}

/// Handles the result of a database operation, committing the transaction if successful and
/// rolling back if not.
///
/// Also logs the result of the transaction.
async fn handle_db_result<T>(
result: Result<T, Error>,
result: Result<T, DbError>,
tx: Transaction<'_, Postgres>,
) -> Result<T, Error> {
) -> Result<T, DbError> {
match result {
Ok(result) => {
log::info!("Committing transaction");
Expand All @@ -95,7 +100,7 @@ async fn handle_db_result<T>(
} else {
log::warn!("Transaction rolled back");
}
Err(e)
Err(DbError::from(e))
}
}
}
5 changes: 3 additions & 2 deletions backend/src/db/from_db.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::db::db_error::DbError;
use axum::async_trait;
use sqlx::{Error, PgPool};
use sqlx::PgPool;

/// Trait for converting a database model to the corresponding domain type, i.e. Rust struct.
#[async_trait]
pub(super) trait FromDb: Sized {
type DbModel;
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, Error>;
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, DbError>;
}
1 change: 1 addition & 0 deletions backend/src/db/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ mod report;
mod report_analyses_map;
mod report_metadata;
pub mod report_repository;
mod test_util;
mod user;
7 changes: 4 additions & 3 deletions backend/src/db/impls/nlp_analysis.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::db::db_error::DbError;
use crate::db::db_stored::DbStoredDependentlyInner;
use crate::db::from_db::FromDb;
use crate::db::model::{DbNlpAnalysis, DbNlpMetadata};
use crate::nlp::analysis::NlpAnalysisKind;
use crate::nlp::nlp_response::{NlpAnalysis, NlpMetadata};
use axum::async_trait;
use sqlx::{Error, PgPool, Postgres, Transaction};
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

#[async_trait]
impl DbStoredDependentlyInner for NlpAnalysis {
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, Error> {
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, DbError> {
let metadata_uuid = self.metadata.inner_save(tx).await?;
let id = sqlx::query!(
r#"
Expand All @@ -31,7 +32,7 @@ impl DbStoredDependentlyInner for NlpAnalysis {
#[async_trait]
impl FromDb for NlpAnalysis {
type DbModel = DbNlpAnalysis;
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, Error> {
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, DbError> {
let nlp_metadata = sqlx::query_as!(
DbNlpMetadata,
r#"SELECT * FROM nlp_metadata WHERE id = $1"#,
Expand Down
7 changes: 4 additions & 3 deletions backend/src/db/impls/nlp_metadata.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use crate::db::db_error::DbError;
use crate::db::db_stored::DbStoredDependentlyInner;
use crate::db::from_db::FromDb;
use crate::db::model::DbNlpMetadata;
use crate::nlp::nlp_response::NlpMetadata;
use axum::async_trait;
use sqlx::{Error, PgPool, Postgres, Transaction};
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

#[async_trait]
impl DbStoredDependentlyInner for NlpMetadata {
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, Error> {
async fn inner_save(&self, tx: &mut Transaction<Postgres>) -> Result<Uuid, DbError> {
let id = sqlx::query!(
r#"
INSERT INTO nlp_metadata (generated_in)
Expand All @@ -27,7 +28,7 @@ impl DbStoredDependentlyInner for NlpMetadata {
#[async_trait]
impl FromDb for NlpMetadata {
type DbModel = DbNlpMetadata;
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, Error> {
async fn from_db_model(model: Self::DbModel, pool: &PgPool) -> Result<Self, DbError> {
Ok(NlpMetadata {
generated_in: model.generated_in,
})
Expand Down
Loading

0 comments on commit eda66f8

Please sign in to comment.