From a5d11c29d15b321bb64384d0ed08ae9ba454db83 Mon Sep 17 00:00:00 2001 From: max funk Date: Sat, 9 Mar 2024 20:36:19 -0700 Subject: [PATCH 01/17] avoid null properties when printing config values with yq --- scripts/list-env-vars.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/list-env-vars.sh b/scripts/list-env-vars.sh index 9f8ee853..6612364d 100644 --- a/scripts/list-env-vars.sh +++ b/scripts/list-env-vars.sh @@ -2,4 +2,4 @@ PROJECT_CONF=project.yaml -yq '.. | select(has("set")) | .set | keys | .[]' $PROJECT_CONF | sort \ No newline at end of file +yq '.. | select(has("set")) | select(.set != null) | .set | keys | .[]' $PROJECT_CONF | sort \ No newline at end of file From 522457806bcdc3cb09c6058465f031322f6cde38 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:18:49 -0700 Subject: [PATCH 02/17] graphql convenience make targets --- services/graphql/makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/graphql/makefile b/services/graphql/makefile index 5db3a024..357405d3 100644 --- a/services/graphql/makefile +++ b/services/graphql/makefile @@ -6,6 +6,13 @@ start: @$(MAKE) get-secrets ENV=local nohup cargo watch --env-file $(ENV_FILE) -w src -w $(RELATIVE_PROJECT_ROOT_PATH)/crates -x run >> $(NOHUP_LOG) & +start-alone: + $(MAKE) start + tail -F $(NOHUP_LOG) + +stop: + $(MAKE) -C $(RELATIVE_PROJECT_ROOT_PATH) stop + open: open $(GRAPHQL_URI)/ From 29a89eac30cdc30c42a7f828cc38cc85f6daaa39 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:19:10 -0700 Subject: [PATCH 03/17] single rule router --- services/rule/src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/services/rule/src/main.rs b/services/rule/src/main.rs index 4934f945..ead94f1e 100644 --- a/services/rule/src/main.rs +++ b/services/rule/src/main.rs @@ -231,16 +231,14 @@ async fn main() { let readiness_check_path = env::var(READINESS_CHECK_PATH) .unwrap_or_else(|_| panic!("{READINESS_CHECK_PATH} variable assignment")); - let health_check_route = - Router::new().route(&readiness_check_path, get(|| async { StatusCode::OK })); - let conn_uri = DB::create_conn_uri_from_env_vars(); let pool = Arc::new(DB::new_pool(&conn_uri).await) as DynConnPool; - let service_route = Router::new().route("/", post(apply_rules)).with_state(pool); - - let app = Router::new().merge(health_check_route).merge(service_route); + let app = Router::new() + .route("/", post(apply_rules)) + .route(readiness_check_path.as_str(), get(|| async { StatusCode::OK })) + .with_state(pool); let hostname_or_ip = env::var("HOSTNAME_OR_IP").unwrap_or("0.0.0.0".to_string()); From a9f31c901a5ba2c425968cbe899bfed5a5f87d17 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:20:07 -0700 Subject: [PATCH 04/17] add vec functions to RuleInstances newtype --- crates/types/src/rule.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/types/src/rule.rs b/crates/types/src/rule.rs index 148cb27d..a76d32de 100644 --- a/crates/types/src/rule.rs +++ b/crates/types/src/rule.rs @@ -146,6 +146,14 @@ impl RuleInstances { pub fn add(&mut self, elem: RuleInstance) { self.0.push(elem) } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } #[cfg(test)] From e789e21f839cff5a4f162b5c97cfbfaddf2155e0 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:21:35 -0700 Subject: [PATCH 05/17] impl From trait on AccountProfile and vec newtype --- crates/types/src/account.rs | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/types/src/account.rs b/crates/types/src/account.rs index 81e769ec..89209c0d 100644 --- a/crates/types/src/account.rs +++ b/crates/types/src/account.rs @@ -3,7 +3,7 @@ use async_graphql::SimpleObject; use async_trait::async_trait; use serde::Deserialize; use std::error::Error; -use tokio_postgres::types::{FromSql, ToSql}; +use tokio_postgres::{Row, types::{FromSql, ToSql}}; #[async_trait] pub trait AccountTrait { @@ -49,6 +49,37 @@ impl AccountProfile { } } +impl From<&Row> for AccountProfile { + fn from(row: &Row) -> Self { + AccountProfile { + id: row.get(0), + account_name: row.get(1), + description: row.get(2), + first_name: row.get(3), + middle_name: row.get(4), + last_name: row.get(5), + country_name: row.get(6), + street_number: row.get(7), + street_name: row.get(8), + floor_number: row.get(9), + unit_number: row.get(10), + city_name: row.get(11), + county_name: row.get(12), + region_name: row.get(13), + state_name: row.get(14), + postal_code: row.get(15), + latlng: row.get(16), + email_address: row.get(17), + telephone_country_code: row.get(18), + telephone_area_code: row.get(19), + telephone_number: row.get(20), + occupation_id: row.get(21), + industry_id: row.get(22), + removal_time: row.get(23), + } + } +} + #[derive(Eq, PartialEq, Debug, Deserialize, FromSql, ToSql)] pub struct AccountProfiles(pub Vec); @@ -87,6 +118,16 @@ impl FromIterator for AccountProfiles { } } +impl From> for AccountProfiles { + fn from(rows: Vec) -> Self { + let mut profiles = AccountProfiles::new(); + for row in rows { + profiles.add(AccountProfile::from(&row)); + } + profiles + } +} + #[cfg(test)] mod tests { use super::*; From fc4664cf0926770f2f1ff19a493a0a6d165fee7e Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:23:16 -0700 Subject: [PATCH 06/17] fmt --- crates/types/src/account.rs | 5 ++++- services/rule/src/main.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/types/src/account.rs b/crates/types/src/account.rs index 89209c0d..7e555154 100644 --- a/crates/types/src/account.rs +++ b/crates/types/src/account.rs @@ -3,7 +3,10 @@ use async_graphql::SimpleObject; use async_trait::async_trait; use serde::Deserialize; use std::error::Error; -use tokio_postgres::{Row, types::{FromSql, ToSql}}; +use tokio_postgres::{ + types::{FromSql, ToSql}, + Row, +}; #[async_trait] pub trait AccountTrait { diff --git a/services/rule/src/main.rs b/services/rule/src/main.rs index ead94f1e..e66d6cd5 100644 --- a/services/rule/src/main.rs +++ b/services/rule/src/main.rs @@ -237,7 +237,10 @@ async fn main() { let app = Router::new() .route("/", post(apply_rules)) - .route(readiness_check_path.as_str(), get(|| async { StatusCode::OK })) + .route( + readiness_check_path.as_str(), + get(|| async { StatusCode::OK }), + ) .with_state(pool); let hostname_or_ip = env::var("HOSTNAME_OR_IP").unwrap_or("0.0.0.0".to_string()); From 2ee67b9b4cdcf78821b8546a329df83caea8872d Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 20:31:36 -0700 Subject: [PATCH 07/17] add sqls and queries --- crates/pg/src/model.rs | 275 ++++++++++++++++++++++++++++++++-- crates/pg/src/sqls/profile.rs | 32 +++- 2 files changed, 289 insertions(+), 18 deletions(-) diff --git a/crates/pg/src/model.rs b/crates/pg/src/model.rs index 70ce36b4..0c8e67f4 100644 --- a/crates/pg/src/model.rs +++ b/crates/pg/src/model.rs @@ -70,6 +70,10 @@ pub trait ModelTrait { &self, accounts: Vec, ) -> Result, Box>; + async fn select_account_profiles_by_account_names_query( + &self, + accounts: Vec, + ) -> Result>; async fn select_transaction_items_by_transaction_id_query( &self, transaction_id: i32, @@ -108,6 +112,25 @@ pub trait ModelTrait { &self, account_name: String, ) -> Result<(), Box>; + async fn select_approvers_query(&self, account: String) -> Result, Box>; + async fn select_rule_instance_by_type_role_state_query( + &self, + rule_type: String, + account_role: AccountRole, + state_name: String, + ) -> Result>; + async fn select_rule_instance_by_type_role_account_query( + &self, + rule_type: String, + account_role: AccountRole, + account_name: String, + ) -> Result>; + // uses above query + async fn select_approval_rule_instance_by_role_account_query( + &self, + account_role: AccountRole, + account_name: String, + ) -> Result>; } impl ModelTrait for DatabaseConnection { @@ -361,6 +384,26 @@ impl ModelTrait for DatabaseConnection { } } + async fn select_account_profiles_by_account_names_query( + &self, + accounts: Vec, + ) -> Result> { + let mut deduped = accounts; + deduped.sort_unstable(); + deduped.dedup(); + let table = crate::sqls::profile::AccountProfileTable::new(); + let sql = table.select_profiles_by_account_names_sql(deduped.len()); + let mut values: ToSqlVec = Vec::new(); + for a in deduped.into_iter() { + values.push(Box::new(a)) + } + let rows = self.query(sql.to_string(), values).await; + match rows { + Err(e) => Err(Box::new(e)), + Ok(rows) => Ok(AccountProfiles::from(rows)), + } + } + async fn select_transaction_items_by_transaction_id_query( &self, transaction_id: i32, @@ -624,6 +667,86 @@ impl ModelTrait for DatabaseConnection { Ok(_) => Ok(()), } } + + async fn select_approvers_query(&self, account: String) -> Result, Box> { + let sql = select_approvers(); + let values: ToSqlVec = vec![Box::new(account)]; + let rows = self.query(sql.to_string(), values).await; + match rows { + Err(e) => Err(Box::new(e)), + Ok(rows) => { + let approvers: Vec = rows.into_iter().map(|row| row.get(0)).collect(); + Ok(approvers) + } + } + } + + async fn select_rule_instance_by_type_role_state_query( + &self, + rule_type: String, + account_role: AccountRole, + state_name: String, + ) -> Result> { + let sql = select_rule_instance_by_type_role_state(); + let values: ToSqlVec = vec![ + Box::new(rule_type), + Box::new(account_role), + Box::new(state_name), + ]; + let rows = self.query(sql.to_string(), values).await; + match rows { + Err(e) => Err(Box::new(e)), + Ok(rows) => { + let rule_instances: RuleInstances = RuleInstances::from(rows); + Ok(rule_instances) + } + } + } + + async fn select_approval_rule_instance_by_role_account_query( + &self, + account_role: AccountRole, + account_name: String, + ) -> Result> { + let table = crate::sqls::rule_instance::RuleInstanceTable::new(); + let sql = table.select_rule_instance_by_type_role_account_sql(); + let values: ToSqlVec = vec![ + Box::new("approval".to_string()), + Box::new(account_role), + Box::new(account_name), + ]; + let rows = self.query(sql.to_string(), values).await; + match rows { + Err(e) => Err(Box::new(e)), + Ok(rows) => { + let rule_instances: RuleInstances = RuleInstances::from(rows); + Ok(rule_instances) + } + } + } + + async fn select_rule_instance_by_type_role_account_query( + &self, + rule_type: String, + account_role: AccountRole, + account_name: String, + ) -> Result> { + let table = crate::sqls::rule_instance::RuleInstanceTable::new(); + let sql = table.select_rule_instance_by_type_role_account_sql(); + let values: ToSqlVec = vec![ + Box::new(rule_type), + Box::new(account_role), + Box::new(account_name), + ]; + let rows = self.query(sql.to_string(), values).await; + match rows { + Err(e) => Err(Box::new(e)), + Ok(rows) => { + let rule_instances: RuleInstances = RuleInstances::from(rows); + Ok(rule_instances) + } + } + } } impl DatabaseConnection { @@ -681,22 +804,6 @@ impl DatabaseConnection { Ok(_) => Ok(()), } } - - pub async fn select_approvers_query( - &self, - account: String, - ) -> Result, Box> { - let sql = select_approvers(); - let values: ToSqlVec = vec![Box::new(account)]; - let rows = self.query(sql.to_string(), values).await; - match rows { - Err(e) => Err(Box::new(e)), - Ok(rows) => { - let approvers: Vec = rows.into_iter().map(|row| row.get(0)).collect(); - Ok(approvers) - } - } - } } fn parse_pg_int4(s: Option) -> Result, Box> { @@ -1108,6 +1215,83 @@ mod integration_tests { assert_eq!(got, want); } + #[cfg_attr(not(feature = "db_tests"), ignore)] + #[tokio::test] + async fn it_creates_a_select_account_profiles_by_account_names_query() { + _before_each(); + + let test_conn = _get_conn().await; + let api_conn = DatabaseConnection(test_conn); + + let test_accounts = vec![ + "JacobWebb".to_string(), + "JacobWebb".to_string(), // duplicate + "GroceryStore".to_string(), + ]; + + let got = api_conn + .select_account_profiles_by_account_names_query(test_accounts.clone().to_vec()) + .await + .unwrap(); + + let want = AccountProfiles(vec![ + AccountProfile { + id: Some(String::from("7")), + account_name: String::from("JacobWebb"), + description: Some(String::from("Soccer coach")), + first_name: Some(String::from("Jacob")), + middle_name: Some(String::from("Curtis")), + last_name: Some(String::from("Webb")), + country_name: String::from("United States of America"), + street_number: Some(String::from("205")), + street_name: Some(String::from("N Mccarran Blvd")), + floor_number: None, + unit_number: None, + city_name: String::from("Sparks"), + county_name: Some(String::from("Washoe County")), + region_name: None, + state_name: String::from("Nevada"), + postal_code: String::from("89431"), + latlng: Some(String::from("(39.534552,-119.737825)")), + email_address: String::from("jacob@address.xz"), + telephone_country_code: Some(String::from("1")), + telephone_area_code: Some(String::from("775")), + telephone_number: Some(String::from("5555555")), + occupation_id: Some(String::from("7")), + industry_id: Some(String::from("7")), + removal_time: None, + }, + AccountProfile { + id: Some(String::from("11")), + account_name: String::from("GroceryStore"), + description: Some(String::from("Sells groceries")), + first_name: Some(String::from("Grocery")), + middle_name: None, + last_name: Some(String::from("Store")), + country_name: String::from("United States of America"), + street_number: Some(String::from("8701")), + street_name: Some(String::from("Lincoln Blvd")), + floor_number: None, + unit_number: None, + city_name: String::from("Los Angeles"), + county_name: Some(String::from("Los Angeles County")), + region_name: None, + state_name: String::from("California"), + postal_code: String::from("90045"), + latlng: Some(String::from("(33.95805,-118.418388)")), + email_address: String::from("grocerystore@address.xz"), + telephone_country_code: Some(String::from("1")), + telephone_area_code: Some(String::from("310")), + telephone_number: Some(String::from("5555555")), + occupation_id: Some(String::from("11")), + industry_id: Some(String::from("11")), + removal_time: None, + }, + ]); + + assert_eq!(got, want); + } + #[cfg_attr(not(feature = "db_tests"), ignore)] #[tokio::test] async fn it_creates_a_select_transaction_items_by_transaction_id_query() { @@ -1417,6 +1601,65 @@ mod integration_tests { assert_eq!(rows.len(), 3); } + + #[cfg_attr(not(feature = "db_tests"), ignore)] + #[tokio::test] + async fn it_creates_a_select_rule_instance_by_type_role_state_query() { + _before_each(); + + let test_conn = _get_conn().await; + let api_conn = DatabaseConnection(test_conn); + + let rows = api_conn + .select_rule_instance_by_type_role_state_query( + "transaction_item".to_string(), + AccountRole::Creditor, + "California".to_string(), + ) + .await + .unwrap(); + + assert_eq!(rows.len(), 1); + } + + #[cfg_attr(not(feature = "db_tests"), ignore)] + #[tokio::test] + async fn it_creates_a_select_approval_rule_instance_by_role_account_query() { + _before_each(); + + let test_conn = _get_conn().await; + let api_conn = DatabaseConnection(test_conn); + + let rows = api_conn + .select_approval_rule_instance_by_role_account_query( + AccountRole::Creditor, + "MiriamLevy".to_string(), + ) + .await + .unwrap(); + + assert_eq!(rows.len(), 2); + } + + #[cfg_attr(not(feature = "db_tests"), ignore)] + #[tokio::test] + async fn it_creates_a_select_rule_instance_by_type_role_account_query() { + _before_each(); + + let test_conn = _get_conn().await; + let api_conn = DatabaseConnection(test_conn); + + let rows = api_conn + .select_rule_instance_by_type_role_account_query( + "approval".to_string(), + AccountRole::Creditor, + "MiriamLevy".to_string(), + ) + .await + .unwrap(); + + assert_eq!(rows.len(), 2); + } } pub type DynConnPool = Arc; diff --git a/crates/pg/src/sqls/profile.rs b/crates/pg/src/sqls/profile.rs index 43fda82c..b7ba502f 100644 --- a/crates/pg/src/sqls/profile.rs +++ b/crates/pg/src/sqls/profile.rs @@ -57,6 +57,7 @@ impl AccountProfileTable { .cast_column_as(Type::TEXT), self.get_column("occupation_id").cast_column_as(Type::TEXT), self.get_column("industry_id").cast_column_as(Type::TEXT), + self.get_column("removal_time"), ]) } @@ -163,6 +164,26 @@ impl AccountProfileTable { ) } + pub fn select_profiles_by_account_names_sql(&self, account_name_count: usize) -> String { + let columns = self.select_all_with_text_casting().join_with_casting(); + let in_values = create_params(account_name_count); + format!( + "{} {} {} {} {} {} {} ({}) {} {} {} {}", + SELECT, + columns, + FROM, + self.name(), + WHERE, + self.get_column("account_name").name(), + IN, + in_values, + AND, + self.get_column("removal_time").name(), + IS, + NULL + ) + } + // todo: add account_owner type and AccountOwnerTable pub fn select_approvers() -> String { let column_alias = "approver"; @@ -205,7 +226,7 @@ mod tests { #[test] fn it_creates_a_select_account_profile_by_account_sql() { let test_table = AccountProfileTable::new(); - let expected = "SELECT id::text, account_name, description, first_name, middle_name, last_name, country_name, street_number, street_name, floor_number, unit_number, city_name, county_name, region_name, state_name, postal_code, latlng::text, email_address, telephone_country_code::text, telephone_area_code::text, telephone_number::text, occupation_id::text, industry_id::text FROM account_profile WHERE account_name = $1;"; + let expected = "SELECT id::text, account_name, description, first_name, middle_name, last_name, country_name, street_number, street_name, floor_number, unit_number, city_name, county_name, region_name, state_name, postal_code, latlng::text, email_address, telephone_country_code::text, telephone_area_code::text, telephone_number::text, occupation_id::text, industry_id::text, removal_time FROM account_profile WHERE account_name = $1;"; assert_eq!(test_table.select_account_profile_by_account_sql(), expected); } @@ -219,13 +240,20 @@ mod tests { #[test] fn it_creates_a_select_account_profiles_by_db_cr_accounts_sql() { let test_table = AccountProfileTable::new(); - let expected = "SELECT id::text, account_name, description, first_name, middle_name, last_name, country_name, street_number, street_name, floor_number, unit_number, city_name, county_name, region_name, state_name, postal_code, latlng::text, email_address, telephone_country_code::text, telephone_area_code::text, telephone_number::text, occupation_id::text, industry_id::text FROM account_profile WHERE account_name = $1 OR account_name = $2;"; + let expected = "SELECT id::text, account_name, description, first_name, middle_name, last_name, country_name, street_number, street_name, floor_number, unit_number, city_name, county_name, region_name, state_name, postal_code, latlng::text, email_address, telephone_country_code::text, telephone_area_code::text, telephone_number::text, occupation_id::text, industry_id::text, removal_time FROM account_profile WHERE account_name = $1 OR account_name = $2;"; assert_eq!( test_table.select_account_profiles_by_db_cr_accounts_sql(), expected ); } + #[test] + fn it_creates_a_select_profiles_by_account_names_sql() { + let test_table = AccountProfileTable::new(); + let expected = "SELECT id::text, account_name, description, first_name, middle_name, last_name, country_name, street_number, street_name, floor_number, unit_number, city_name, county_name, region_name, state_name, postal_code, latlng::text, email_address, telephone_country_code::text, telephone_area_code::text, telephone_number::text, occupation_id::text, industry_id::text, removal_time FROM account_profile WHERE account_name IN ($1, $2) AND removal_time IS NULL"; + assert_eq!(test_table.select_profiles_by_account_names_sql(2), expected); + } + #[test] fn it_creates_a_select_profile_ids_by_account_names_sql() { let test_table = AccountProfileTable::new(); From 2c7941754a34295b19665fd3115f6787502b804b Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:36:28 -0700 Subject: [PATCH 08/17] add cognitoidp rust crate to workspace --- Cargo.toml | 1 + crates/cognitoidp/Cargo.toml | 12 ++++++ crates/cognitoidp/src/lib.rs | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 crates/cognitoidp/Cargo.toml create mode 100644 crates/cognitoidp/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index da145ce8..d39078c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "crates/cognitoidp", "crates/httpclient", "crates/pg", "crates/service", diff --git a/crates/cognitoidp/Cargo.toml b/crates/cognitoidp/Cargo.toml new file mode 100644 index 00000000..7da81f65 --- /dev/null +++ b/crates/cognitoidp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cognitoidp" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true + +[dependencies] +jsonwebtoken = "9.2.0" +reqwest = { version = "0.11.25", features = ["json"] } +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +thiserror = "1.0.57" diff --git a/crates/cognitoidp/src/lib.rs b/crates/cognitoidp/src/lib.rs new file mode 100644 index 00000000..c9f83241 --- /dev/null +++ b/crates/cognitoidp/src/lib.rs @@ -0,0 +1,71 @@ +use jsonwebtoken::{ + decode, errors, + jwk::{Jwk, JwkSet}, + Algorithm, DecodingKey, Validation, +}; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum IdpError { + #[error("zero json web keys returned from cognito")] + ZeroJsonWebKeysReturned, + #[error(transparent)] + RequestError(#[from] reqwest::Error), + #[error("json web key not found in cognito jwk set")] + JsonWebKeyNotFound, + #[error(transparent)] + DecodingKey(#[from] errors::Error), + #[error("api auth disabled")] + ApiAuthDisabled, +} + +#[derive(Deserialize, Debug)] +pub struct CognitoClaims { + // https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html + #[serde(rename = "cognito:username")] + cognito_username: String, +} + +#[derive(Deserialize, Debug)] +pub struct CognitoJwkSet(JwkSet); + +impl CognitoJwkSet { + pub async fn new(cognito_jwks_uri: &str) -> Result { + let response = reqwest::get(cognito_jwks_uri) + .await + .map_err(IdpError::RequestError)?; + let jwks = response.json::().await.unwrap(); + if jwks.keys.is_empty() { + return Err(IdpError::ZeroJsonWebKeysReturned); + } + Ok(Self(jwks)) + } + + pub fn match_jwk(&self, claimed_key_id: &str) -> Option<&Jwk> { + self.0.find(claimed_key_id) + } + + pub fn pub_key(&self, claimed_key_id: &str) -> Result { + let jwk = self + .match_jwk(claimed_key_id) + .ok_or(IdpError::JsonWebKeyNotFound)?; + DecodingKey::from_jwk(jwk).map_err(|_| IdpError::JsonWebKeyNotFound) + } + + pub fn test_token(&self, token: &str) -> Result { + let header = jsonwebtoken::decode_header(token).map_err(IdpError::DecodingKey)?; + let jwk = self + .match_jwk(&header.kid.unwrap()) + .ok_or(IdpError::JsonWebKeyNotFound)?; + let key = DecodingKey::from_jwk(jwk).map_err(|_| IdpError::JsonWebKeyNotFound)?; + // https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2 + let claims = decode::(token, &key, &Validation::new(Algorithm::RS256)) + .map_err(IdpError::DecodingKey)?; + Ok(claims.claims) + } + + pub fn cognito_user(&self, token: &str) -> Result { + self.test_token(token).map(|claims| claims.cognito_username) + } +} From 76be51c2db0ef25db32cf8553c88db3c229ab8b7 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:41:41 -0700 Subject: [PATCH 09/17] switch to sqls created from Table structs --- crates/pg/src/model.rs | 715 +---------------------------------------- 1 file changed, 10 insertions(+), 705 deletions(-) diff --git a/crates/pg/src/model.rs b/crates/pg/src/model.rs index 0c8e67f4..554bdede 100644 --- a/crates/pg/src/model.rs +++ b/crates/pg/src/model.rs @@ -1,25 +1,16 @@ #![allow(async_fn_in_trait)] -use crate::postgres::{ConnectionPool, DatabaseConnection, RowTrait, ToSqlVec}; +use crate::postgres::{DatabaseConnection, ToSqlVec}; use crate::sqls::common::TableTrait; -use crate::sqls::{ - profile::{select_account_profiles_by_db_cr_accounts, select_approvers}, - rule_instance::{ - select_rule_instance_by_type_role_account, select_rule_instance_by_type_role_state, - }, -}; -use async_trait::async_trait; use chrono::{DateTime, Utc}; use geo_types::Point; use rust_decimal::Decimal; -use std::vec; -use std::{error::Error, sync::Arc}; -use tokio_postgres::types::ToSql; +use std::{error::Error, vec}; use types::{ - account::{AccountProfile, AccountProfiles, AccountTrait}, + account::{AccountProfile, AccountProfiles}, account_role::AccountRole, approval::Approvals, balance::AccountBalances, - rule::{RuleInstance, RuleInstanceTrait, RuleInstances}, + rule::RuleInstances, time::TZTime, transaction::{Transaction, Transactions}, transaction_item::TransactionItems, @@ -125,12 +116,6 @@ pub trait ModelTrait { account_role: AccountRole, account_name: String, ) -> Result>; - // uses above query - async fn select_approval_rule_instance_by_role_account_query( - &self, - account_role: AccountRole, - account_name: String, - ) -> Result>; } impl ModelTrait for DatabaseConnection { @@ -165,7 +150,7 @@ impl ModelTrait for DatabaseConnection { match rows { Err(e) => Err(Box::new(e)), Ok(rows) => { - let balance: Decimal = rows[0].get_decimal("current_balance"); + let balance: Decimal = rows[0].get(0); Ok(format!("{:.FIXED_DECIMAL_PLACES$}", balance)) } } @@ -377,7 +362,7 @@ impl ModelTrait for DatabaseConnection { Ok(rows) => { let profile_ids: Vec<(String, String)> = rows .into_iter() - .map(|row| (row.get_string("id"), row.get_string("account_name"))) + .map(|row| (row.get(0), row.get(1))) .collect(); Ok(profile_ids) } @@ -669,7 +654,8 @@ impl ModelTrait for DatabaseConnection { } async fn select_approvers_query(&self, account: String) -> Result, Box> { - let sql = select_approvers(); + let table = crate::sqls::account::AccountOwnerTable::new(); + let sql = table.select_approvers_sql(); let values: ToSqlVec = vec![Box::new(account)]; let rows = self.query(sql.to_string(), values).await; match rows { @@ -687,7 +673,8 @@ impl ModelTrait for DatabaseConnection { account_role: AccountRole, state_name: String, ) -> Result> { - let sql = select_rule_instance_by_type_role_state(); + let table = crate::sqls::rule_instance::RuleInstanceTable::new(); + let sql = table.select_rule_instance_by_type_role_state_sql(); let values: ToSqlVec = vec![ Box::new(rule_type), Box::new(account_role), @@ -703,28 +690,6 @@ impl ModelTrait for DatabaseConnection { } } - async fn select_approval_rule_instance_by_role_account_query( - &self, - account_role: AccountRole, - account_name: String, - ) -> Result> { - let table = crate::sqls::rule_instance::RuleInstanceTable::new(); - let sql = table.select_rule_instance_by_type_role_account_sql(); - let values: ToSqlVec = vec![ - Box::new("approval".to_string()), - Box::new(account_role), - Box::new(account_name), - ]; - let rows = self.query(sql.to_string(), values).await; - match rows { - Err(e) => Err(Box::new(e)), - Ok(rows) => { - let rule_instances: RuleInstances = RuleInstances::from(rows); - Ok(rule_instances) - } - } - } - async fn select_rule_instance_by_type_role_account_query( &self, rule_type: String, @@ -1622,25 +1587,6 @@ mod integration_tests { assert_eq!(rows.len(), 1); } - #[cfg_attr(not(feature = "db_tests"), ignore)] - #[tokio::test] - async fn it_creates_a_select_approval_rule_instance_by_role_account_query() { - _before_each(); - - let test_conn = _get_conn().await; - let api_conn = DatabaseConnection(test_conn); - - let rows = api_conn - .select_approval_rule_instance_by_role_account_query( - AccountRole::Creditor, - "MiriamLevy".to_string(), - ) - .await - .unwrap(); - - assert_eq!(rows.len(), 2); - } - #[cfg_attr(not(feature = "db_tests"), ignore)] #[tokio::test] async fn it_creates_a_select_rule_instance_by_type_role_account_query() { @@ -1661,644 +1607,3 @@ mod integration_tests { assert_eq!(rows.len(), 2); } } -pub type DynConnPool = Arc; - -pub type DynDBConn = Arc; - -#[async_trait] -pub trait DBConnPoolTrait { - async fn get_conn(&self) -> DynDBConn; -} - -#[async_trait] -pub trait DBConnTrait: AccountTrait + RuleInstanceTrait {} -impl DBConnTrait for T {} - -#[async_trait] -impl DBConnPoolTrait for ConnectionPool { - async fn get_conn(&self) -> DynDBConn { - let conn = self.0.get_owned().await.unwrap(); // todo: handle error - Arc::new(Conn(Arc::new(DatabaseConnection(conn)))) - } -} - -// for dependency injection, wrap tokio-postgres as a trait object -// inside a Conn, then impl service traits on Conn -pub struct Conn(Arc); - -// impl dependency injection trait using tokio-postgres -#[async_trait] -impl DatabaseConnectionTrait for DatabaseConnection { - async fn query_account_profiles( - &self, - sql_stmt: String, - accounts: Vec, - ) -> Result>, tokio_postgres::Error> { - // https://github.com/sfackler/rust-postgres/issues/133#issuecomment-659751392 - let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new(); - - for a in accounts.iter() { - params.push(a) - } - - self.q(sql_stmt, ¶ms[..]).await - } - - async fn query_approvers( - &self, - sql_stmt: String, - account: String, - ) -> Result>, tokio_postgres::Error> { - self.q(sql_stmt, &[&account]).await - } - - async fn query_profile_state_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - state_name: String, - ) -> Result>, tokio_postgres::Error> { - self.q(sql_stmt, &[&"transaction_item", &account_role, &state_name]) - .await - } - - async fn query_rule_instances_by_type_role_account( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error> { - self.q(sql_stmt, &[&"transaction_item", &account_role, &account]) - .await - } - - async fn query_approval_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error> { - self.q(sql_stmt, &[&"approval", &account_role, &account]) - .await - } -} - -// dependency injection trait for tokio-postgres -#[async_trait] -trait DatabaseConnectionTrait { - async fn query_account_profiles( - &self, - sql_stmt: String, - accounts: Vec, - ) -> Result>, tokio_postgres::Error>; - - async fn query_approvers( - &self, - sql_stmt: String, - account: String, - ) -> Result>, tokio_postgres::Error>; - - async fn query_profile_state_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - state_name: String, - ) -> Result>, tokio_postgres::Error>; - - async fn query_rule_instances_by_type_role_account( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error>; - - async fn query_approval_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error>; -} - -// impl account service trait on Conn -#[async_trait] -impl AccountTrait for Conn { - async fn get_account_profiles( - &self, - accounts: Vec, - ) -> Result> { - let rows = self - .0 - .query_account_profiles(select_account_profiles_by_db_cr_accounts(), accounts) - .await; - match rows { - Err(rows) => Err(Box::new(rows)), - Ok(rows) => { - let account_profiles = from_account_profile_rows(rows); - Ok(account_profiles) - } - } - } - - async fn get_approvers_for_account(&self, account: String) -> Vec { - let rows = self - .0 - .query_approvers(select_approvers(), account) - .await - .unwrap(); // todo: handle error - let account_approvers: Vec = rows - .into_iter() - .map(|row| row.get_string("approver")) - .collect(); - account_approvers - } -} - -// impl rule instance service trait on Conn -#[async_trait] -impl RuleInstanceTrait for Conn { - async fn get_profile_state_rule_instances( - &self, - account_role: AccountRole, - state_name: String, - ) -> RuleInstances { - let rows = self - .0 - .query_profile_state_rule_instances( - select_rule_instance_by_type_role_state(), - account_role, - state_name, - ) - .await - .unwrap(); // todo: handle error - from_rule_instance_rows(rows) - } - - async fn get_rule_instances_by_type_role_account( - &self, - account_role: AccountRole, - account: String, - ) -> RuleInstances { - let rows = self - .0 - .query_rule_instances_by_type_role_account( - select_rule_instance_by_type_role_account(), - account_role, - account, - ) - .await - .unwrap(); // todo: handle error - from_rule_instance_rows(rows) - } - - async fn get_approval_rule_instances( - &self, - account_role: AccountRole, - account: String, - ) -> RuleInstances { - let rows = self - .0 - .query_approval_rule_instances( - select_rule_instance_by_type_role_account(), - account_role, - account, - ) - .await - .unwrap(); // todo: handle error - from_rule_instance_rows(rows) - } -} - -fn from_account_profile_row(row: Box) -> AccountProfile { - AccountProfile { - // cadet todo: add a schema module declaring statics for - // column names and arrays of column names for each table - id: row.get_opt_string("id"), - account_name: row.get_string("account_name"), - description: row.get_opt_string("description"), - first_name: row.get_opt_string("first_name"), - middle_name: row.get_opt_string("middle_name"), - last_name: row.get_opt_string("last_name"), - country_name: row.get_string("country_name"), - street_number: row.get_opt_string("street_number"), - street_name: row.get_opt_string("street_name"), - floor_number: row.get_opt_string("floor_number"), - unit_number: row.get_opt_string("unit_number"), - city_name: row.get_string("city_name"), - county_name: row.get_opt_string("county_name"), - region_name: row.get_opt_string("region_name"), - state_name: row.get_string("state_name"), - postal_code: row.get_string("postal_code"), - latlng: row.get_opt_string("latlng"), - email_address: row.get_string("email_address"), - telephone_country_code: row.get_opt_string("telephone_country_code"), - telephone_area_code: row.get_opt_string("telephone_area_code"), - telephone_number: row.get_opt_string("telephone_number"), - occupation_id: row.get_opt_string("occupation_id"), - industry_id: row.get_opt_string("industry_id"), - removal_time: row.get_opt_tztime("removal_time"), - } -} - -fn from_account_profile_rows(rows: Vec>) -> AccountProfiles { - rows.into_iter().map(from_account_profile_row).collect() -} - -fn from_rule_instance_row(row: Box) -> RuleInstance { - RuleInstance { - id: row.get_opt_string("id"), - rule_type: row.get_string("rule_type"), - rule_name: row.get_string("rule_name"), - rule_instance_name: row.get_string("rule_instance_name"), - variable_values: row.get_vec_string("variable_values"), - account_role: row.get_account_role("account_role"), - item_id: row.get_opt_string("item_id"), - price: row.get_opt_string("price"), - quantity: row.get_opt_string("quantity"), - unit_of_measurement: row.get_opt_string("unit_of_measurement"), - units_measured: row.get_opt_string("units_measured"), - account_name: row.get_opt_string("account_name"), - first_name: row.get_opt_string("first_name"), - middle_name: row.get_opt_string("middle_name"), - last_name: row.get_opt_string("last_name"), - country_name: row.get_opt_string("country_name"), - street_id: row.get_opt_string("street_id"), - street_name: row.get_opt_string("street_name"), - floor_number: row.get_opt_string("floor_number"), - unit_id: row.get_opt_string("unit_id"), - city_name: row.get_opt_string("city_name"), - county_name: row.get_opt_string("county_name"), - region_name: row.get_opt_string("region_name"), - state_name: row.get_opt_string("state_name"), - postal_code: row.get_opt_string("postal_code"), - latlng: row.get_opt_string("latlng"), - email_address: row.get_opt_string("email_address"), - telephone_country_code: row.get_opt_string("telephone_country_code"), - telephone_area_code: row.get_opt_string("telephone_area_code"), - telephone_number: row.get_opt_string("telephone_number"), - occupation_id: row.get_opt_string("occupation_id"), - industry_id: row.get_opt_string("industry_id"), - disabled_time: row.get_opt_tztime("disabled_time"), - removed_time: row.get_opt_tztime("removed_time"), - created_at: row.get_opt_tztime("created_at"), - } -} - -fn from_rule_instance_rows(rows: Vec>) -> RuleInstances { - rows.into_iter().map(from_rule_instance_row).collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::postgres::DB; - use serial_test::serial; // concurrency avoided while using static mut TEST_ARGS for shared test state - use std::{env, vec}; - use types::time::TZTime; - - fn account_profile_columns() -> Vec { - vec![ - String::from("id"), - String::from("account_name"), - String::from("description"), - String::from("first_name"), - String::from("middle_name"), - String::from("last_name"), - String::from("country_name"), - String::from("street_number"), - String::from("street_name"), - String::from("floor_number"), - String::from("unit_number"), - String::from("city_name"), - String::from("county_name"), - String::from("region_name"), - String::from("state_name"), - String::from("postal_code"), - String::from("latlng"), - String::from("email_address"), - String::from("telephone_country_code"), - String::from("telephone_area_code"), - String::from("telephone_number"), - String::from("occupation_id"), - String::from("industry_id"), - String::from("removal_time"), - ] - } - - fn rule_instance_columns() -> Vec { - vec![ - String::from("id"), - String::from("rule_type"), - String::from("rule_name"), - String::from("rule_instance_name"), - String::from("variable_values"), - String::from("account_role"), - String::from("item_id"), - String::from("price"), - String::from("quantity"), - String::from("unit_of_measurement"), - String::from("units_measured"), - String::from("account_name"), - String::from("first_name"), - String::from("middle_name"), - String::from("last_name"), - String::from("country_name"), - String::from("street_id"), - String::from("street_name"), - String::from("floor_number"), - String::from("unit_id"), - String::from("city_name"), - String::from("county_name"), - String::from("region_name"), - String::from("state_name"), - String::from("postal_code"), - String::from("latlng"), - String::from("email_address"), - String::from("telephone_country_code"), - String::from("telephone_area_code"), - String::from("telephone_number"), - String::from("occupation_id"), - String::from("industry_id"), - String::from("disabled_time"), - String::from("removed_time"), - String::from("created_at"), - ] - } - - #[test] - fn it_returns_a_conn_uri() { - env::set_var("PGUSER", "a"); - env::set_var("PGPASSWORD", "b"); - env::set_var("PGHOST", "c"); - env::set_var("PGPORT", "d"); - env::set_var("PGDATABASE", "e"); - let got = DB::create_conn_uri_from_env_vars(); - env::remove_var("PGUSER"); - env::remove_var("PGPASSWORD"); - env::remove_var("PGHOST"); - env::remove_var("PGPORT"); - env::remove_var("PGDATABASE"); - let want = String::from("postgresql://a:b@c:d/e"); - assert_eq!(got, want) - } - - #[test] - #[should_panic] - fn it_panics_from_unset_env_var() { - DB::get_env_var("NOT_SET"); - } - - static mut TEST_ARGS: Vec = vec![]; - - #[derive(Clone, Copy)] - struct TestRow; - - impl TestRow { - fn add(&self, arg: &str) { - // test code only - unsafe { TEST_ARGS.push(String::from(arg)) } - } - - fn clear(&self) { - // test code only - unsafe { TEST_ARGS.clear() } - } - } - - impl RowTrait for TestRow { - fn get_opt_string(&self, idx: &str) -> Option { - self.add(idx); - None - } - fn get_string(&self, idx: &str) -> String { - self.add(idx); - String::from("") - } - fn get_vec_string(&self, idx: &str) -> Vec { - self.add(idx); - vec![] - } - fn get_account_role(&self, idx: &str) -> AccountRole { - self.add(idx); - AccountRole::Creditor - } - fn get_opt_tztime(&self, idx: &str) -> Option { - self.add(idx); - None - } - - fn get_decimal(&self, idx: &str) -> Decimal { - self.add(idx); - Decimal::new(10, 1) - } - } - - #[test] - #[serial] - fn from_account_profile_row_called_with_args() { - let test_row = TestRow; - - from_account_profile_row(Box::new(test_row)); - - let mut unsorted_got = unsafe { TEST_ARGS.clone() }; - - unsorted_got.sort(); - - let got = unsorted_got.clone(); - - let mut unsorted_want = account_profile_columns(); - - unsorted_want.sort(); - - let want = unsorted_want.clone(); - - assert_eq!(got, want); - - test_row.clone().clear() - } - - #[test] - #[serial] - fn from_account_profile_rows_called_with_args() { - let test_row_1 = TestRow; - let test_row_2 = TestRow; - - let test_rows: Vec> = vec![Box::new(test_row_1), Box::new(test_row_2)]; - - from_account_profile_rows(test_rows); - - let mut unsorted_got = unsafe { TEST_ARGS.clone() }; - - unsorted_got.sort(); - - let got = unsorted_got.clone(); - - // add first set of account_profile_columns - let mut unsorted_want = account_profile_columns(); - - // add second set of account_profile_columns - unsorted_want.append(&mut account_profile_columns()); - - unsorted_want.sort(); - - let want = unsorted_want.clone(); - - assert_eq!(got, want); - - test_row_1.clone().clear(); - } - - #[test] - #[serial] - fn from_rule_instance_row_called_with_args() { - let test_row = TestRow; - - from_rule_instance_row(Box::new(test_row)); - - let mut unsorted_got = unsafe { TEST_ARGS.clone() }; - - unsorted_got.sort(); - - let got = unsorted_got.clone(); - - let mut unsorted_want = rule_instance_columns(); - - unsorted_want.sort(); - - let want = unsorted_want.clone(); - - assert_eq!(got, want); - - test_row.clone().clear() - } - - #[test] - #[serial] - fn from_rule_instance_rows_called_with_args() { - let test_row_1 = TestRow; - let test_row_2 = TestRow; - - let test_rows: Vec> = vec![Box::new(test_row_1), Box::new(test_row_2)]; - - from_rule_instance_rows(test_rows); - - let mut unsorted_got = unsafe { TEST_ARGS.clone() }; - - unsorted_got.sort(); - - let got = unsorted_got.clone(); - - // add first set of rule_instance_columns - let mut unsorted_want = rule_instance_columns(); - - // add second set of rule_instance_columns - unsorted_want.append(&mut rule_instance_columns()); - - unsorted_want.sort(); - - let want = unsorted_want.clone(); - - assert_eq!(got, want); - - test_row_1.clone().clear(); - } - - struct TestDB; - - #[async_trait] - impl DatabaseConnectionTrait for TestDB { - async fn query_account_profiles( - &self, - sql_stmt: String, - accounts: Vec, - ) -> Result>, tokio_postgres::Error> { - assert_eq!(sql_stmt, select_account_profiles_by_db_cr_accounts()); - assert_eq!(accounts, vec!["a".to_string(), "b".to_string()]); - Ok(vec![]) - } - - async fn query_approvers( - &self, - sql_stmt: String, - account: String, - ) -> Result>, tokio_postgres::Error> { - assert_eq!(sql_stmt, select_approvers()); - assert_eq!(account, "a".to_string()); - Ok(vec![]) - } - - async fn query_profile_state_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - state_name: String, - ) -> Result>, tokio_postgres::Error> { - assert_eq!(sql_stmt, select_rule_instance_by_type_role_state()); - assert_eq!(account_role, AccountRole::Creditor); - assert_eq!(state_name, "a".to_string()); - Ok(vec![]) - } - - async fn query_rule_instances_by_type_role_account( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error> { - assert_eq!(sql_stmt, select_rule_instance_by_type_role_account()); - assert_eq!(account_role, AccountRole::Creditor); - assert_eq!(account, "a".to_string()); - Ok(vec![]) - } - - async fn query_approval_rule_instances( - &self, - sql_stmt: String, - account_role: AccountRole, - account: String, - ) -> Result>, tokio_postgres::Error> { - assert_eq!(sql_stmt, select_rule_instance_by_type_role_account()); - assert_eq!(account_role, AccountRole::Creditor); - assert_eq!(account, "a".to_string()); - Ok(vec![]) - } - } - - #[test] - fn get_account_profiles_called_with_args() { - let test_conn = Conn(Arc::new(TestDB)); - let accounts = vec!["a".to_string(), "b".to_string()]; - let _ = test_conn.get_account_profiles(accounts); - } - - #[test] - fn get_approvers_for_account_called_with_args() { - let test_conn = Conn(Arc::new(TestDB)); - let account = "a".to_string(); - let _ = test_conn.get_approvers_for_account(account); - } - - #[test] - fn get_profile_state_rule_instances_called_with_args() { - let test_conn = Conn(Arc::new(TestDB)); - let account_role = AccountRole::Creditor; - let state_name = "a".to_string(); - let _ = test_conn.get_profile_state_rule_instances(account_role, state_name); - } - - #[test] - fn get_rule_instances_by_type_role_account_called_with_args() { - let test_conn = Conn(Arc::new(TestDB)); - let account_role = AccountRole::Creditor; - let account = "a".to_string(); - let _ = test_conn.get_rule_instances_by_type_role_account(account_role, account); - } - - #[test] - fn get_approval_rule_instances_called_with_args() { - let test_conn = Conn(Arc::new(TestDB)); - let account_role = AccountRole::Creditor; - let account = "a".to_string(); - let _ = test_conn.get_approval_rule_instances(account_role, account); - } -} From 64bb0d706559cbae0a3675c1439ad7db96e97c9f Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:42:17 -0700 Subject: [PATCH 10/17] remove traits and trait objects --- crates/pg/src/postgres.rs | 52 ++++++--------------------------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/crates/pg/src/postgres.rs b/crates/pg/src/postgres.rs index dd4a1ca3..884e6fc4 100644 --- a/crates/pg/src/postgres.rs +++ b/crates/pg/src/postgres.rs @@ -1,9 +1,7 @@ use bb8::{Pool, PooledConnection}; use bb8_postgres::PostgresConnectionManager; -use rust_decimal::Decimal; use std::env; use tokio_postgres::{types::ToSql, NoTls, Row}; -use types::{account_role::AccountRole, time::TZTime}; pub struct DB; @@ -32,55 +30,19 @@ impl DB { #[derive(Clone)] pub struct ConnectionPool(pub Pool>); -pub struct DatabaseConnection(pub PooledConnection<'static, PostgresConnectionManager>); - -pub trait RowTrait { - fn get_opt_string(&self, idx: &str) -> Option; - fn get_string(&self, idx: &str) -> String; - fn get_vec_string(&self, idx: &str) -> Vec; - fn get_account_role(&self, idx: &str) -> AccountRole; - fn get_opt_tztime(&self, idx: &str) -> Option; - fn get_decimal(&self, idx: &str) -> Decimal; -} - -impl RowTrait for Row { - fn get_opt_string(&self, idx: &str) -> Option { - self.get(idx) - } - fn get_string(&self, idx: &str) -> String { - self.get(idx) - } - fn get_vec_string(&self, idx: &str) -> Vec { - self.get(idx) - } - fn get_account_role(&self, idx: &str) -> AccountRole { - self.get(idx) - } - fn get_opt_tztime(&self, idx: &str) -> Option { - self.get(idx) - } - fn get_decimal(&self, idx: &str) -> Decimal { - self.get(idx) +impl ConnectionPool { + pub async fn get_conn(&self) -> DatabaseConnection { + let conn = self.0.get_owned().await.unwrap(); // todo: handle error + DatabaseConnection(conn) } } +pub struct DatabaseConnection(pub PooledConnection<'static, PostgresConnectionManager>); + pub type ToSqlVec = Vec>; -// isolate tokio-postgres query dependency in this impl +// tokio_postgres deps here impl DatabaseConnection { - // old - pub async fn q( - &self, - sql_stmt: String, - params: &[&(dyn ToSql + Sync)], - ) -> Result>, tokio_postgres::Error> { - self.0.query(sql_stmt.as_str(), params).await.map(|rows| { - rows.into_iter() - .map(|row| Box::new(row) as Box) - .collect() - }) - } - pub async fn query( &self, sql_stmt: String, From 6da060b2ae7c313a3ab0cc0f7ba7260a22dd410a Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:43:31 -0700 Subject: [PATCH 11/17] remove unused standalone sql functions --- crates/pg/src/sqls/profile.rs | 47 -------------------------- crates/pg/src/sqls/rule_instance.rs | 52 ----------------------------- 2 files changed, 99 deletions(-) diff --git a/crates/pg/src/sqls/profile.rs b/crates/pg/src/sqls/profile.rs index b7ba502f..64cd1568 100644 --- a/crates/pg/src/sqls/profile.rs +++ b/crates/pg/src/sqls/profile.rs @@ -264,50 +264,3 @@ mod tests { ); } } - -// todo: replace with above -pub fn select_approvers() -> String { - r#"SELECT DISTINCT coalesce(owner_account, '') || coalesce(owner_subaccount, '') as approver -FROM account_owner ao -WHERE ao.owned_account = $1 OR ao.owned_subaccount = $1;"# - .to_owned() -} - -fn select_account_profile_base() -> &'static str { - r#"SELECT - id::text, - account_name, - description, - first_name, - middle_name, - last_name, - country_name, - street_number, - street_name, - floor_number, - unit_number, - city_name, - county_name, - region_name, - state_name, - postal_code, - latlng::text, - email_address, - telephone_country_code::text, - telephone_area_code::text, - telephone_number::text, - occupation_id::text, - industry_id::text, - removal_time, - created_at -FROM account_profile -"# -} - -pub fn select_account_profile_by_account() -> String { - select_account_profile_base().to_owned() + "WHERE account_name = $1;" -} - -pub fn select_account_profiles_by_db_cr_accounts() -> String { - select_account_profile_base().to_owned() + "WHERE account_name = $1 OR account_name = $2;" -} diff --git a/crates/pg/src/sqls/rule_instance.rs b/crates/pg/src/sqls/rule_instance.rs index 477292e5..99f3a46c 100644 --- a/crates/pg/src/sqls/rule_instance.rs +++ b/crates/pg/src/sqls/rule_instance.rs @@ -204,55 +204,3 @@ mod tests { assert_eq!(test_table.insert_rule_instance_sql(), expected); } } - -// todo: replace with above -fn select_rule_instance_base() -> &'static str { - r#"SELECT - id::text, - rule_type, - rule_name, - rule_instance_name, - variable_values, - account_role, - item_id, - price::text, - quantity::text, - unit_of_measurement, - units_measured::text, - account_name, - first_name, - middle_name, - last_name, - country_name, - street_id, - street_name, - floor_number, - unit_id, - city_name, - county_name, - region_name, - state_name, - postal_code, - latlng::text, - email_address, - telephone_country_code::text, - telephone_area_code::text, - telephone_number::text, - occupation_id::text, - industry_id::text, - disabled_time, - removed_time, - created_at -FROM rule_instance -"# -} - -pub fn select_rule_instance_by_type_role_account() -> String { - select_rule_instance_base().to_owned() - + "WHERE rule_type = $1 AND account_role = $2 AND account_name = $3;" -} - -pub fn select_rule_instance_by_type_role_state() -> String { - select_rule_instance_base().to_owned() - + "WHERE rule_type = $1 AND account_role = $2 AND state_name = $3;" -} From 77274ff6568a8bdb005c586a17b232761bc5c3bf Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:43:50 -0700 Subject: [PATCH 12/17] remove unused pg crate dep --- crates/pg/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/pg/Cargo.toml b/crates/pg/Cargo.toml index e7dd54ea..442415f9 100644 --- a/crates/pg/Cargo.toml +++ b/crates/pg/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" bb8 = "0.8.0" bb8-postgres = "0.8.1" tokio = "1.26.0" -async-trait = "0.1.73" tokio-postgres = { version = "0.7.7", features = [ "with-chrono-0_4", "with-geo-types-0_7", From 27cc459fcaf1718eabddf201f1bee2da375be092 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:46:22 -0700 Subject: [PATCH 13/17] add cogntioidp crate to service crate --- crates/service/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/service/Cargo.toml b/crates/service/Cargo.toml index 2dcbbac3..fa4c43f3 100644 --- a/crates/service/Cargo.toml +++ b/crates/service/Cargo.toml @@ -9,3 +9,4 @@ types = { path = "../types" } rust_decimal = { version = "1.34.3", default-features = false } httpclient = { path = "../httpclient" } pg = { path = "../pg" } +cognitoidp = { path = "../cognitoidp" } From 1e3e9beba79fac36f01626f21e1ed6700ae9b292 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:46:51 -0700 Subject: [PATCH 14/17] add service functions --- crates/service/src/lib.rs | 139 ++++++++++++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 15 deletions(-) diff --git a/crates/service/src/lib.rs b/crates/service/src/lib.rs index efba2338..43141402 100644 --- a/crates/service/src/lib.rs +++ b/crates/service/src/lib.rs @@ -1,13 +1,15 @@ +use cognitoidp::CognitoJwkSet; use httpclient::HttpClient as Client; use pg::model::ModelTrait; use rust_decimal::Decimal; use std::{env, error::Error}; use types::{ - account::AccountProfile, + account::{AccountProfile, AccountProfiles}, account_role::AccountRole, approval::{ApprovalError, Approvals}, balance::AccountBalances, request_response::IntraTransaction, + rule::RuleInstances, time::TZTime, transaction::{Transaction, Transactions}, transaction_item::TransactionItems, @@ -40,7 +42,11 @@ impl Service { &self, account_profile: AccountProfile, ) -> Result> { - match self.conn.insert_account_profile_query(account_profile).await { + match self + .conn + .insert_account_profile_query(account_profile) + .await + { Ok(account) => Ok(account), Err(e) => Err(e), } @@ -52,7 +58,8 @@ impl Service { ) -> Result, Box> { match self .conn - .select_profile_ids_by_account_names_query(account_names).await + .select_profile_ids_by_account_names_query(account_names) + .await { Ok(profile_ids) => Ok(profile_ids), // [(id, account_name),...] Err(e) => Err(e), @@ -67,7 +74,8 @@ impl Service { ) -> Result<(), Box> { match self .conn - .insert_account_balance_query(account, balance, curr_tr_item_id).await + .insert_account_balance_query(account, balance, curr_tr_item_id) + .await { Ok(_) => Ok(()), Err(e) => Err(e), @@ -104,7 +112,9 @@ impl Service { &self, transaction_items: TransactionItems, ) -> Result<(), Box> { - self.conn.update_account_balances_query(transaction_items).await + self.conn + .update_account_balances_query(transaction_items) + .await } async fn test_sufficient_debitor_funds( @@ -138,7 +148,8 @@ impl Service { role: AccountRole, ) -> Result> { self.conn - .update_approvals_by_account_and_role_query(transaction_id, account, role).await + .update_approvals_by_account_and_role_query(transaction_id, account, role) + .await } // todo: convert to postgres function to avoid 3 db queries @@ -146,7 +157,10 @@ impl Service { &self, transaction_id: i32, ) -> Result> { - let mut transaction = self.conn.select_transaction_by_id_query(transaction_id).await?; + let mut transaction = self + .conn + .select_transaction_by_id_query(transaction_id) + .await?; let transaction_items = self .get_transaction_items_by_transaction_id(transaction_id) @@ -166,7 +180,8 @@ impl Service { ) -> Result> { let mut transactions = self .conn - .select_transactions_by_ids_query(transaction_ids.clone()).await?; + .select_transactions_by_ids_query(transaction_ids.clone()) + .await?; let transaction_items = self .get_transaction_items_by_transaction_ids(transaction_ids.clone()) @@ -251,10 +266,12 @@ impl Service { ) -> Result<(), Box> { let exists = self .conn - .select_approve_all_credit_rule_instance_exists_query(account_name.clone()).await?; + .select_approve_all_credit_rule_instance_exists_query(account_name.clone()) + .await?; if !exists { self.conn - .insert_approve_all_credit_rule_instance_query(account_name.clone()).await?; + .insert_approve_all_credit_rule_instance_query(account_name.clone()) + .await?; } Ok(()) } @@ -330,7 +347,10 @@ impl Service { account: String, n: i64, ) -> Result> { - let mut transactions = self.conn.select_last_n_transactions_query(account, n).await?; + let mut transactions = self + .conn + .select_last_n_transactions_query(account, n) + .await?; let transaction_ids = transactions.list_ids()?; if transaction_ids.is_empty() { return Ok(transactions); @@ -364,7 +384,8 @@ impl Service { transaction_id: i32, ) -> Result> { self.conn - .select_transaction_items_by_transaction_id_query(transaction_id).await + .select_transaction_items_by_transaction_id_query(transaction_id) + .await } pub async fn get_transaction_items_by_transaction_ids( @@ -372,7 +393,8 @@ impl Service { transaction_ids: Vec, ) -> Result> { self.conn - .select_transaction_items_by_transaction_ids_query(transaction_ids).await + .select_transaction_items_by_transaction_ids_query(transaction_ids) + .await } pub async fn get_approvals_by_transaction_id( @@ -380,7 +402,8 @@ impl Service { transaction_id: i32, ) -> Result> { self.conn - .select_approvals_by_transaction_id_query(transaction_id).await + .select_approvals_by_transaction_id_query(transaction_id) + .await } pub async fn get_approvals_by_transaction_ids( @@ -388,6 +411,92 @@ impl Service { transaction_ids: Vec, ) -> Result> { self.conn - .select_approvals_by_transaction_ids_query(transaction_ids).await + .select_approvals_by_transaction_ids_query(transaction_ids) + .await + } + + pub async fn get_json_web_key_set(&self) -> Result, Box> { + if env::var("ENABLE_API_AUTH") == Ok("true".to_string()) { + let uri = env::var("COGNITO_JWKS_URI").expect("msg: COGNITO_JWKS_URI not set"); + let jwks = CognitoJwkSet::new(uri.as_str()).await; + match jwks { + Ok(jwks) => Ok(Some(jwks)), + Err(e) => Err(Box::new(e)), + } + } else { + Ok(None) + } + } + + pub fn get_auth_account( + &self, + jwks: CognitoJwkSet, + token: &str, + temp_account: &str, + ) -> Result> { + if env::var("ENABLE_API_AUTH") == Ok("true".to_string()) { + let account = jwks.cognito_user(token)?; + Ok(account) + } else { + Ok(temp_account.to_string()) + } + } + + pub async fn get_account_approvers( + &self, + account: String, + ) -> Result, Box> { + self.conn.select_approvers_query(account).await + } + + pub async fn get_tr_item_rule_instances_by_role_account( + &self, + account_role: AccountRole, + account_name: String, + ) -> Result> { + self.conn + .select_rule_instance_by_type_role_account_query( + "transaction_item".to_string(), + account_role, + account_name, + ) + .await + } + + pub async fn get_account_profiles( + &self, + account_names: Vec, + ) -> Result> { + self.conn + .select_account_profiles_by_account_names_query(account_names) + .await + } + + pub async fn get_state_tr_item_rule_instances( + &self, + account_role: AccountRole, + state_name: String, + ) -> Result> { + self.conn + .select_rule_instance_by_type_role_state_query( + "transaction_item".to_string(), + account_role, + state_name, + ) + .await + } + + pub async fn get_approval_rule_instances( + &self, + account_role: AccountRole, + approver_account: String, + ) -> Result> { + self.conn + .select_rule_instance_by_type_role_account_query( + "approval".to_string(), + account_role, + approver_account, + ) + .await } } From a72527b8c936387975fa1f24eb5005bb83d5c2b7 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:47:58 -0700 Subject: [PATCH 15/17] switch rule to service crate --- services/rule/Cargo.toml | 10 +- services/rule/src/main.rs | 644 ++------------------------------------ 2 files changed, 33 insertions(+), 621 deletions(-) diff --git a/services/rule/Cargo.toml b/services/rule/Cargo.toml index 853bf7b9..306b53f7 100644 --- a/services/rule/Cargo.toml +++ b/services/rule/Cargo.toml @@ -6,16 +6,18 @@ publish = false [dependencies] axum = "0.6.2" -serde = { version = "1.0.189", features = ["derive"] } -tokio = { version = "1.33.0", features = ["full"] } -tower = "0.4.13" -tower-http = "0.4.4" +tokio = { version = "1.33.0", default-features = false, features = [ + "rt-multi-thread", + "macros", + "signal" +] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } types = { path = "../../crates/types" } pg = { path = "../../crates/pg" } chrono = "0.4.23" nanoid = "0.4.0" +service = { path = "../../crates/service" } [dev-dependencies] regex = "1.0.0" diff --git a/services/rule/src/main.rs b/services/rule/src/main.rs index e66d6cd5..e95bd48a 100644 --- a/services/rule/src/main.rs +++ b/services/rule/src/main.rs @@ -4,12 +4,10 @@ use axum::{ routing::{get, post}, Router, }; -use pg::{ - model::{DynConnPool, DynDBConn}, - postgres::DB, -}; +use pg::postgres::{ConnectionPool, DatabaseConnection, DB}; use rule::{create_response, expected_values, label_approved_transaction_items}; -use std::{env, net::ToSocketAddrs, sync::Arc}; +use service::Service; +use std::{env, net::ToSocketAddrs}; use tokio::signal; use types::approval::{Approval, Approvals}; use types::{ @@ -24,13 +22,13 @@ mod rules; const READINESS_CHECK_PATH: &str = "READINESS_CHECK_PATH"; async fn apply_transaction_item_rules( - conn: DynDBConn, + svc: &Service, role_sequence: RoleSequence, transaction_items: &TransactionItems, ) -> TransactionItems { let accounts = transaction_items.list_accounts(); - let initial_account_profiles = conn.get_account_profiles(accounts).await.unwrap(); + let initial_account_profiles = svc.get_account_profiles(accounts).await.unwrap(); let mut response: TransactionItems = TransactionItems::default(); @@ -54,9 +52,10 @@ async fn apply_transaction_item_rules( current_tr_item.set_profile_id(role, account_profile.clone().id.unwrap()); // todo: handle missing id error // get rules matching state in account profile - let state_rules = conn - .get_profile_state_rule_instances(role, account_profile.state_name.to_string()) - .await; + let state_rules = svc + .get_state_tr_item_rule_instances(role, account_profile.state_name.to_string()) + .await + .unwrap(); // apply state rules to transaction item for rule_instance in state_rules.clone().0.iter() { @@ -70,9 +69,10 @@ async fn apply_transaction_item_rules( } // get rules matching account and rule - let account_rules = conn - .get_rule_instances_by_type_role_account(role, account.clone()) - .await; + let account_rules = svc + .get_tr_item_rule_instances_by_role_account(role, account.clone()) + .await + .unwrap(); // apply account rules to transaction item for rule_instance in account_rules.clone().0.iter() { @@ -91,7 +91,7 @@ async fn apply_transaction_item_rules( if !rule_added.0.is_empty() { let added_accounts = rule_added.list_accounts(); - let added_profiles = conn.get_account_profiles(added_accounts).await.unwrap(); + let added_profiles = svc.get_account_profiles(added_accounts).await.unwrap(); // add account profile ids to rule added transaction items // todo: some profiles may be previously fetched when @@ -110,7 +110,7 @@ async fn apply_transaction_item_rules( } async fn apply_approval_rules( - conn: DynDBConn, + svc: &Service, role_sequence: RoleSequence, transaction_items: &mut TransactionItems, approval_time: &TZTime, @@ -128,7 +128,7 @@ async fn apply_approval_rules( let account = tr_item.get_account_by_role(role); // query account owners (approvers) of debitor or credit account - let approvers = conn.get_approvers_for_account(account).await; + let approvers = svc.get_account_approvers(account).await.unwrap(); // loop through list of approvers for approver in approvers { @@ -148,9 +148,10 @@ async fn apply_approval_rules( }; // query approval rules available for current approver - let approval_rules = conn + let approval_rules = svc .get_approval_rule_instances(role, approver.clone()) - .await; + .await + .unwrap(); // loop through each approval rule and apply for rule_instance in approval_rules.0.iter() { @@ -175,7 +176,7 @@ async fn apply_approval_rules( } async fn apply_rules( - State(pool): State, + State(pool): State, transaction_items: Json, ) -> Result, StatusCode> { if !expected_values(&transaction_items) { @@ -199,16 +200,19 @@ async fn apply_rules( } // get connection from pool - let conn = pool.get_conn().await as DynDBConn; + let conn = pool.get_conn().await; + + // create service with conn + let svc = Service::new(conn); let mut rule_applied_tr_items = - apply_transaction_item_rules(conn.clone(), role_sequence, &transaction_items).await; + apply_transaction_item_rules(&svc, role_sequence, &transaction_items).await; // create an approval time to be used for all automated approvals let approval_time = TZTime::now(); apply_approval_rules( - conn, + &svc, role_sequence, &mut rule_applied_tr_items, &approval_time, @@ -233,7 +237,7 @@ async fn main() { let conn_uri = DB::create_conn_uri_from_env_vars(); - let pool = Arc::new(DB::new_pool(&conn_uri).await) as DynConnPool; + let pool = DB::new_pool(&conn_uri).await; let app = Router::new() .route("/", post(apply_rules)) @@ -292,597 +296,3 @@ async fn shutdown_signal() { println!("signal received, starting graceful shutdown"); } - -#[cfg(test)] -mod tests { - use super::*; - use axum::async_trait; - use chrono::{DateTime, Utc}; - use std::error::Error; - use types::{ - account::{AccountProfile, AccountProfiles, AccountTrait}, - account_role::{AccountRole, DEBITOR_FIRST}, - rule::{RuleInstance, RuleInstanceTrait, RuleInstances}, - transaction_item::TransactionItem, - }; - struct DBConnStub(); - const TEST_TAX_APPROVERS: &[&str] = &["BenRoss", "DanLee", "MiriamLevy"]; - - #[async_trait] - impl AccountTrait for DBConnStub { - async fn get_account_profiles( - &self, - _accounts: Vec, - ) -> Result> { - Ok(AccountProfiles(vec![ - AccountProfile { - id: Some(String::from("7")), - account_name: String::from("JacobWebb"), - description: Some(String::from("Soccer coach")), - first_name: Some(String::from("Jacob")), - middle_name: Some(String::from("Curtis")), - last_name: Some(String::from("Webb")), - country_name: String::from("United States of America"), - street_number: Some(String::from("205")), - street_name: Some(String::from("N Mccarran Blvd")), - floor_number: None, - unit_number: None, - city_name: String::from("Sparks"), - county_name: Some(String::from("Washoe County")), - region_name: None, - state_name: String::from("Nevada"), - postal_code: String::from("89431"), - latlng: Some(String::from("(39.534552,-119.737825)")), - email_address: String::from("jacob@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("775")), - telephone_number: Some(String::from("5555555")), - occupation_id: Some(String::from("7")), - industry_id: Some(String::from("7")), - removal_time: None, - }, - AccountProfile { - id: Some(String::from("11")), - account_name: String::from("GroceryStore"), - description: Some(String::from("Sells groceries")), - first_name: Some(String::from("Grocery")), - middle_name: None, - last_name: Some(String::from("Store")), - country_name: String::from("United States of America"), - street_number: Some(String::from("8701")), - street_name: Some(String::from("Lincoln Blvd")), - floor_number: None, - unit_number: None, - city_name: String::from("Los Angeles"), - county_name: Some(String::from("Los Angeles County")), - region_name: None, - state_name: String::from("California"), - postal_code: String::from("90045"), - latlng: Some(String::from("(33.958050,-118.418388)")), - email_address: String::from("grocerystore@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("310")), - telephone_number: Some(String::from("5555555")), - occupation_id: None, - industry_id: Some(String::from("8")), - removal_time: None, - }, - AccountProfile { - id: Some(String::from("27")), - account_name: String::from("StateOfCalifornia"), - description: Some(String::from("State of California")), - first_name: None, - middle_name: None, - last_name: None, - country_name: String::from("United States of America"), - street_number: Some(String::from("450")), - street_name: Some(String::from("N St")), - floor_number: None, - unit_number: None, - city_name: String::from("Sacramento"), - county_name: Some(String::from("Sacramento County")), - region_name: None, - state_name: String::from("California"), - postal_code: String::from("95814"), - latlng: Some(String::from("(38.5777292,-121.5027026)")), - email_address: String::from("stateofcalifornia@address.xz"), - telephone_country_code: Some(String::from("1")), - telephone_area_code: Some(String::from("916")), - telephone_number: Some(String::from("5555555")), - occupation_id: None, - industry_id: Some(String::from("11")), - removal_time: None, - }, - ])) - } - async fn get_approvers_for_account(&self, account: String) -> Vec { - match account.as_str() { - "StateOfCalifornia" => { - let mut approvers: Vec = vec![]; - for a in TEST_TAX_APPROVERS { - approvers.push(a.to_string()) - } - approvers - } - _ => vec![account], - } - } - } - - #[async_trait] - impl RuleInstanceTrait for DBConnStub { - async fn get_profile_state_rule_instances( - &self, - account_role: AccountRole, - _state_name: String, - ) -> RuleInstances { - if account_role == AccountRole::Debitor { - return RuleInstances(vec![]); - } - RuleInstances(vec![RuleInstance { - id: Some(String::from("1")), - rule_type: String::from("transaction_item"), - rule_name: String::from("multiplyItemValue"), - rule_instance_name: String::from("NinePercentSalesTax"), - variable_values: vec![ - String::from("ANY"), - String::from("StateOfCalifornia"), - String::from("9% state sales tax"), - String::from("0.09"), - ], - account_role: AccountRole::Creditor, - item_id: None, - price: None, - quantity: None, - unit_of_measurement: None, - units_measured: None, - account_name: None, - first_name: None, - middle_name: None, - last_name: None, - country_name: None, - street_id: None, - street_name: None, - floor_number: None, - unit_id: None, - city_name: None, - county_name: None, - region_name: None, - state_name: Some(String::from("California")), - postal_code: None, - latlng: None, - email_address: None, - telephone_country_code: None, - telephone_area_code: None, - telephone_number: None, - occupation_id: None, - industry_id: None, - disabled_time: None, - removed_time: None, - created_at: Some(TZTime( - DateTime::parse_from_rfc3339("2023-02-28T04:21:08.363Z") - .unwrap() - .with_timezone(&Utc), - )), - }]) - } - async fn get_rule_instances_by_type_role_account( - &self, - _account_role: AccountRole, - _account: String, - ) -> RuleInstances { - RuleInstances(vec![]) - } - async fn get_approval_rule_instances( - &self, - _account_role: AccountRole, - account: String, - ) -> RuleInstances { - if TEST_TAX_APPROVERS.contains(&account.as_str()) { - return RuleInstances(vec![RuleInstance { - id: Some(String::from("1")), - rule_type: String::from("approval"), - rule_name: String::from("approveAnyCreditItem"), - rule_instance_name: String::from("ApproveAllCaliforniaCredit"), - variable_values: vec![ - String::from("StateOfCalifornia"), - String::from("creditor"), - account, - ], - account_role: AccountRole::Creditor, - item_id: None, - price: None, - quantity: None, - unit_of_measurement: None, - units_measured: None, - account_name: None, - first_name: None, - middle_name: None, - last_name: None, - country_name: None, - street_id: None, - street_name: None, - floor_number: None, - unit_id: None, - city_name: None, - county_name: None, - region_name: None, - state_name: None, - postal_code: None, - latlng: None, - email_address: None, - telephone_country_code: None, - telephone_area_code: None, - telephone_number: None, - occupation_id: None, - industry_id: None, - disabled_time: None, - removed_time: None, - created_at: Some(TZTime( - DateTime::parse_from_rfc3339("2023-02-28T04:21:08.363Z") - .unwrap() - .with_timezone(&Utc), - )), - }]); - } else { - return RuleInstances(vec![]); - } - } - } - - #[tokio::test] - async fn it_applies_transaction_item_rules() { - let db_conn_stub = Arc::new(DBConnStub()) as DynDBConn; - let tr_items = TransactionItems(vec![ - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("bread"), - price: String::from("3.000"), - quantity: String::from("2"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("milk"), - price: String::from("4.000"), - quantity: String::from("3"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - ]); - - // test function - let got = apply_transaction_item_rules(db_conn_stub, DEBITOR_FIRST, &tr_items).await; - - // assert #1: - // save length of transaction items vec - let got_length = got.clone().0.len(); - // want length of transaction items vec to be 4 (started with 2) - let want_length = 4; - assert_eq!( - got_length, want_length, - "got {}, want {}", - got_length, want_length - ); - - // assert #2: - // init float32 accumulator - let mut got_total: f32 = 0.0; - // loop through transaction items vec - for tr_item in got.0.into_iter() { - // parse price - let price: f32 = tr_item.price.clone().parse().unwrap(); - // parse quantity - let quantity: f32 = tr_item.quantity.clone().parse().unwrap(); - // add price * quantity from each transaction item to accumulator - got_total = got_total + (price * quantity); - } - // want total price across transaction items vec to be 19.63 (includes rule added taxes) - let want_total = 19.62; - assert_eq!( - got_total, want_total, - "got {}, want {}", - got_length, want_total - ); - } - - #[tokio::test] - async fn it_applies_approval_rules() { - let test_approval_time = TZTime::now(); - let db_conn_stub = Arc::new(DBConnStub()) as DynDBConn; - let mut got_tr_items = TransactionItems(vec![ - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("bread"), - price: String::from("3.000"), - quantity: String::from("2"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("milk"), - price: String::from("4.000"), - quantity: String::from("3"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("9% state sales tax"), - price: String::from("0.270"), - quantity: String::from("2.000"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("StateOfCalifornia"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("9% state sales tax"), - price: String::from("0.360"), - quantity: String::from("3.000"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("StateOfCalifornia"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - ]); - - // test function - apply_approval_rules( - db_conn_stub, - DEBITOR_FIRST, - &mut got_tr_items, - &test_approval_time, - ) - .await; - - // assert #1 - // save length of tax item approvals - let got_length = got_tr_items - .0 - .clone() - .into_iter() - .nth(3) - .unwrap() - .approvals - .unwrap() - .0 - .len(); - // want length of approvals vec on tax transaction item to be 4 (started with 0) - let want_length: usize = 4; - assert_eq!( - got_length, want_length, - "got {}, want {}", - got_length, want_length - ); - - // assert #2 - // save approval time from first approval - let got_approval_time = got_tr_items - .0 - .into_iter() - .nth(3) - .unwrap() - .approvals - .unwrap() - .0 - .into_iter() - .nth(3) - .unwrap() - .approval_time - .unwrap(); - // want approval time - let want_approval_time = test_approval_time.clone(); - assert_eq!( - got_approval_time, want_approval_time, - "got {:?}, want {:?}", - got_approval_time, want_approval_time - ); - } - - #[tokio::test] - async fn it_applies_rules() { - use axum::extract::{Json, State}; - use pg::model::DBConnPoolTrait; - let test_tr_items = TransactionItems(vec![ - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("bread"), - price: String::from("3.000"), - quantity: String::from("2"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - TransactionItem { - id: None, - transaction_id: None, - item_id: String::from("milk"), - price: String::from("4.000"), - quantity: String::from("3"), - debitor_first: Some(false), - rule_instance_id: None, - rule_exec_ids: Some(vec![]), - unit_of_measurement: None, - units_measured: None, - debitor: String::from("JacobWebb"), - creditor: String::from("GroceryStore"), - debitor_profile_id: None, - creditor_profile_id: None, - debitor_approval_time: None, - creditor_approval_time: None, - debitor_rejection_time: None, - creditor_rejection_time: None, - debitor_expiration_time: None, - creditor_expiration_time: None, - approvals: None, - }, - ]); - - struct DBPoolStub(); - - #[async_trait] - impl DBConnPoolTrait for DBPoolStub { - async fn get_conn(&self) -> DynDBConn { - return Arc::new(DBConnStub()); - } - } - - let db_pool_stub = Arc::new(DBPoolStub()); - - // test function - let got_response = apply_rules(State(db_pool_stub), Json(test_tr_items)) - .await - .unwrap(); - let got_transaction = got_response.0.transaction.clone(); - - // assert #1: - // save length of transaction items vec - let got_length = got_transaction.clone().transaction_items.0.len(); - let want_length = 4; - - // want length of transaction items vec to be 4 (started with 2) - assert_eq!( - got_length, want_length, - "got {}, want {}", - got_length, want_length - ); - - // assert #2 - let mut got_tax_tr_item_count = 0; - for tr_item in got_transaction.clone().transaction_items.0.iter() { - if tr_item.item_id == "9% state sales tax".to_string() { - got_tax_tr_item_count = got_tax_tr_item_count + 1 - } - } - let want_tax_tr_item_count = 2; - // want 2 rule added tax transaction items - assert_eq!( - got_tax_tr_item_count, want_tax_tr_item_count, - "got {}, want {}", - got_tax_tr_item_count, want_tax_tr_item_count - ); - - // assert #3 - let mut got_creditor_approval_count = 0; - for tr_item in got_transaction.clone().transaction_items.0.iter() { - if tr_item.creditor_approval_time.is_some() - && tr_item.item_id == "9% state sales tax".to_string() - { - got_creditor_approval_count = got_creditor_approval_count + 1 - } - } - let want_creditor_approval_count = 2; - // want 2 rule added creditor approvals - assert_eq!( - got_creditor_approval_count, want_creditor_approval_count, - "got {}, want {}", - got_creditor_approval_count, want_creditor_approval_count - ); - - // todo: more tests - } -} From 8109014971f5cb33bfc55a7479362484c2f8aab7 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 22:48:17 -0700 Subject: [PATCH 16/17] cargo lock --- Cargo.lock | 91 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4736bf2e..cc019b6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1147,6 +1147,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "cognitoidp" +version = "0.1.0" +dependencies = [ + "jsonwebtoken", + "reqwest", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "combine" version = "3.8.1" @@ -1709,8 +1720,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -2272,6 +2285,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.5", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2499,6 +2527,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2638,6 +2677,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.5", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2703,7 +2752,6 @@ dependencies = [ name = "pg" version = "0.1.0" dependencies = [ - "async-trait", "bb8", "bb8-postgres", "chrono", @@ -3120,9 +3168,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" dependencies = [ "base64 0.21.5", "bytes", @@ -3142,9 +3190,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -3208,10 +3258,8 @@ dependencies = [ "nanoid", "pg", "regex", - "serde", + "service", "tokio", - "tower", - "tower-http 0.4.4", "tracing", "tracing-subscriber", "types", @@ -3400,9 +3448,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -3429,9 +3477,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -3537,6 +3585,7 @@ dependencies = [ name = "service" version = "0.1.0" dependencies = [ + "cognitoidp", "httpclient", "pg", "rust_decimal", @@ -3595,6 +3644,18 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -3769,20 +3830,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", From b5ea5e9ae8f98bc115eb27e9bc968800a9ac7129 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 12 Mar 2024 23:14:13 -0700 Subject: [PATCH 17/17] vendor openssl in rust rule service --- Cargo.lock | 1 + services/rule/Cargo.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cc019b6b..06026106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3256,6 +3256,7 @@ dependencies = [ "axum 0.6.9", "chrono", "nanoid", + "openssl", "pg", "regex", "service", diff --git a/services/rule/Cargo.toml b/services/rule/Cargo.toml index 306b53f7..0a02a951 100644 --- a/services/rule/Cargo.toml +++ b/services/rule/Cargo.toml @@ -21,3 +21,6 @@ service = { path = "../../crates/service" } [dev-dependencies] regex = "1.0.0" + +[target.x86_64-unknown-linux-musl.dependencies] +openssl = { version = "0.10", features = ["vendored"] } \ No newline at end of file