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 parameter typing #102

Merged
merged 1 commit into from
Feb 24, 2025
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
14 changes: 14 additions & 0 deletions domain/src/agreement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use crate::error::Error;
use entity::agreements::Model;
pub use entity_api::agreement::{create, delete_by_id, find_by_id, update};
use entity_api::{agreement, IntoQueryFilterMap};
use sea_orm::DatabaseConnection;

pub async fn find_by(
db: &DatabaseConnection,
params: impl IntoQueryFilterMap,
) -> Result<Vec<Model>, Error> {
let agreements = agreement::find_by(db, params.into_query_filter_map()).await?;

Ok(agreements)
}
9 changes: 9 additions & 0 deletions domain/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
//! This module re-exports `IntoQueryFilterMap` and `QueryFilterMap` from the `entity_api` crate.
//!
//! The purpose of this re-export is to ensure that consumers of the `domain` crate do not need to
//! directly depend on the `entity_api` crate. By re-exporting these items, we provide a clear and
//! consistent interface for working with query filters within the domain layer, while encapsulating
//! the underlying implementation details remain in the `entity_api` crate.
pub use entity_api::{IntoQueryFilterMap, QueryFilterMap};

pub mod agreement;
pub mod coaching_session;
pub mod error;
pub mod jwt;
Expand Down
33 changes: 11 additions & 22 deletions entity_api/src/agreement.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use super::error::{EntityApiErrorKind, Error};
use crate::uuid_parse_str;
use crate::QueryFilterMap;
use entity::agreements::{self, ActiveModel, Entity, Model};
use entity::Id;
use sea_orm::{
entity::prelude::*,
ActiveValue::{Set, Unchanged},
DatabaseConnection, TryIntoModel,
DatabaseConnection, Iterable, TryIntoModel,
};
use std::collections::HashMap;

use log::*;

Expand Down Expand Up @@ -109,23 +108,13 @@ pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result<Option<Model>

pub async fn find_by(
db: &DatabaseConnection,
query_params: HashMap<String, String>,
query_filter_map: QueryFilterMap,
) -> Result<Vec<Model>, Error> {
let mut query = Entity::find();

for (key, value) in query_params {
match key.as_str() {
"coaching_session_id" => {
let coaching_session_id = uuid_parse_str(&value)?;

query = query.filter(agreements::Column::CoachingSessionId.eq(coaching_session_id));
}
_ => {
return Err(Error {
source: None,
error_kind: EntityApiErrorKind::InvalidQueryTerm,
});
}
for column in agreements::Column::iter() {
if let Some(value) = query_filter_map.get(&column.to_string()) {
query = query.filter(column.eq(value));
}
}

Expand All @@ -140,7 +129,7 @@ pub async fn find_by(
mod tests {
use super::*;
use entity::{agreements::Model, Id};
use sea_orm::{DatabaseBackend, MockDatabase, Transaction};
use sea_orm::{DatabaseBackend, MockDatabase, Transaction, Value};

#[tokio::test]
async fn create_returns_a_new_agreement_model() -> Result<(), Error> {
Expand Down Expand Up @@ -216,15 +205,15 @@ mod tests {
async fn find_by_returns_all_agreements_associated_with_coaching_session() -> Result<(), Error>
{
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
let mut query_params = HashMap::new();
let mut query_filter_map = QueryFilterMap::new();
let coaching_session_id = Id::new_v4();

query_params.insert(
query_filter_map.insert(
"coaching_session_id".to_owned(),
coaching_session_id.to_string(),
Some(Value::Uuid(Some(Box::new(coaching_session_id)))),
);

let _ = find_by(&db, query_params).await;
let _ = find_by(&db, query_filter_map).await;

assert_eq!(
db.into_transaction_log(),
Expand Down
84 changes: 83 additions & 1 deletion entity_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use chrono::{Days, Utc};
use password_auth::generate_hash;
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set, Value};
use std::collections::HashMap;

use entity::{coaching_relationships, coaching_sessions, organizations, users, Id};

Expand Down Expand Up @@ -28,6 +29,87 @@ pub(crate) fn naive_date_parse_str(date_str: &str) -> Result<chrono::NaiveDate,
})
}

/// `QueryFilterMap` is a data structure that serves as a bridge for translating filter parameters
/// between different layers of the application. It is essentially a wrapper around a `HashMap`
/// where the keys are filter parameter names (as `String`) and the values are optional `Value` types
/// from `sea_orm`.
///
/// This structure is particularly useful in scenarios where you need to pass filter parameters
/// from a web request down to the database query layer in a type-safe and organized manner.
///
/// # Example
///
/// ```
/// use sea_orm::Value;
/// use entity_api::QueryFilterMap;
///
/// let mut query_filter_map = QueryFilterMap::new();
/// query_filter_map.insert("coaching_session_id".to_string(), Some(Value::String(Some(Box::new("a_coaching_session_id".to_string())))));
/// let filter_value = query_filter_map.get("coaching_session_id");
/// ```
pub struct QueryFilterMap {
map: HashMap<String, Option<Value>>,
}

impl QueryFilterMap {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}

pub fn get(&self, key: &str) -> Option<Value> {
// HashMap.get returns an Option and so we need to "flatten" this to a single Option
self.map
.get(key)
.and_then(|inner_option| inner_option.clone())
}

pub fn insert(&mut self, key: String, value: Option<Value>) {
self.map.insert(key, value);
}
}

impl Default for QueryFilterMap {
fn default() -> Self {
Self::new()
}
}

/// `IntoQueryFilterMap` is a trait that provides a method for converting a struct into a `QueryFilterMap`.
/// This is particularly useful for translating data between different layers of the application,
/// such as from web request parameters to database query filters.
///
/// Implementing this trait for a struct allows you to define how the fields of the struct should be
/// mapped to the keys and values of the `QueryFilterMap`. This ensures that the data is passed
/// in a type-safe and organized manner.
///
/// # Example
///
/// ```
/// use entity_api::QueryFilterMap;
/// use entity_api::IntoQueryFilterMap;
///
/// #[derive(Debug)]
/// struct MyParams {
/// coaching_session_id: String,
/// }
///
/// impl IntoQueryFilterMap for MyParams {
/// fn into_query_filter_map(self) -> QueryFilterMap {
/// let mut query_filter_map = QueryFilterMap::new();
/// query_filter_map.insert(
/// "coaching_session_id".to_string(),
/// Some(sea_orm::Value::String(Some(Box::new(self.coaching_session_id)))),
/// );
/// query_filter_map
/// }
/// }
/// ```
pub trait IntoQueryFilterMap {
fn into_query_filter_map(self) -> QueryFilterMap;
}

pub async fn seed_database(db: &DatabaseConnection) {
let now = Utc::now();

Expand Down
14 changes: 5 additions & 9 deletions web/src/controller/agreement_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ use crate::controller::ApiResponse;
use crate::extractors::{
authenticated_user::AuthenticatedUser, compare_api_version::CompareApiVersion,
};
use crate::params::agreement::IndexParams;
use crate::{AppState, Error};
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use domain::agreement as AgreementApi;
use entity::{agreements::Model, Id};
use entity_api::agreement as AgreementApi;
use serde_json::json;
use service::config::ApiVersion;
use std::collections::HashMap;

use log::*;

Expand Down Expand Up @@ -122,7 +122,7 @@ pub async fn update(
path = "/agreements",
params(
ApiVersion,
("coaching_session_id" = Option<Id>, Query, description = "Filter by coaching_session_id")
("coaching_session_id" = Id, Query, description = "Filter by coaching_session_id")
),
responses(
(status = 200, description = "Successfully retrieved all Agreements", body = [entity::agreements::Model]),
Expand All @@ -139,15 +139,11 @@ pub async fn index(
// TODO: create a new Extractor to authorize the user to access
// the data requested
State(app_state): State<AppState>,
Query(params): Query<HashMap<String, String>>,
Query(params): Query<IndexParams>,
) -> Result<impl IntoResponse, Error> {
debug!("GET all Agreements");
debug!("Filter Params: {:?}", params);

info!("Params: {:?}", params);
let agreements = AgreementApi::find_by(app_state.db_conn_ref(), params).await?;

debug!("Found Agreements: {:?}", agreements);

Ok(Json(ApiResponse::new(StatusCode::OK.into(), agreements)))
}

Expand Down
Loading